I write a method to calculate QoQ generally.
My idea is to iterate all the fields and calculate if it's long , integer, float or double and set the field name and the result to a map.
It was easy to write this code, but I found it's too ugly :
public static <T> Map<String, String> calculateQoq(final T now, final T before) {
final Field[] declaredFields = now.getClass().getDeclaredFields();
if (ArrayUtils.isEmpty(declaredFields)) {
return Collections.emptyMap();
}
final Map<String, String> map = new HashMap<>(declaredFields.length, 1);
for (final Field f : now.getClass().getDeclaredFields()) {
try {
f.setAccessible(true);
final Object a = f.get(before);
if (a instanceof Integer) {
final Integer beforeNum = (Integer)a;
if (beforeNum == null || beforeNum == 0) {
map.put(f.getName(), ZERO);
continue;
}
final Integer nowNum = (Integer) f.get(now);
if (nowNum == null) {
map.put(f.getName(), ZERO);
continue;
}
map.put(f.getName(), formatTwoFraction((nowNum - beforeNum) * 1.0 / beforeNum));
} else if (a instanceof Long) {
final Long beforeNum = (Long)a;
if (beforeNum == null || beforeNum == 0) {
map.put(f.getName(), ZERO);
continue;
}
final Long nowNum = (Long) f.get(now);
if (nowNum == null) {
map.put(f.getName(), ZERO);
continue;
}
map.put(f.getName(), formatTwoFraction((nowNum - beforeNum) * 1.0 / beforeNum));
} else if (a instanceof Double) {
final Double beforeNum = (Double)a;
if (beforeNum == null || beforeNum == 0) {
map.put(f.getName(), ZERO);
continue;
}
final Double nowNum = (Double) f.get(now);
if (nowNum == null) {
map.put(f.getName(), ZERO);
continue;
}
map.put(f.getName(), formatTwoFraction((nowNum - beforeNum) / beforeNum));
} else if (a instanceof Float) {
final Float beforeNum = (Float)a;
if (beforeNum == null || beforeNum == 0) {
map.put(f.getName(), ZERO);
continue;
}
final Float nowNum = (Float) f.get(now);
if (nowNum == null) {
map.put(f.getName(), ZERO);
continue;
}
map.put(f.getName(), formatTwoFraction((nowNum - beforeNum) / beforeNum));
}
} catch (final Exception e) {
LOG.error("calculateQoq - get field failed - " + f.getName(), e);
}
}
return map;
}
I just repeat the nearly same logic four times, I try to use something like <T extends Number> void doXXX(T before, T now)
But Number can't be calculated.
And Integer, Long and others don't have some common interface like NumberEquals(the default implementation of equals do type checking) or Divideable...
There is no macros in java too...
I tried some times emmmm but no solution yet.
So I wonder is there any way to do abstraction and reduce this logic.
I would suggest isolating the problem into a separate conversion method that takes Number as argument and returns a single primitive type that can be handled uniformly. For example:
private static int toInt(Number number) {
// domain-specific conversion logic
}
Then the code can be simplified to avoid switching on the exact type by having a single case:
if(a instanceof Number) {
int beforeNum = toInt((Number)a);
if(beforeNum == 0) {
map.put(f.getName(), ZERO);
continue;
}
// and so on
The crux of the matter is that how the conversion gets done in detail will be domain-specific (i.e., will depend on how the numbers are interpreted). Assuming the numbers represent currency, then it might be safer to multiply by 100 (or whatever fractional unit is used), and use integer arithmetic. In any case, this code can leverage the intValue() (or other similar) method on Number to again avoid switching. As an illustration:
private static int toInt(Number number) {
if( number == null ) {
return 0;
}
return ((int) (number.doubleValue() * 100.0)); // Example only
}
Here's a solution which uses a Map to look up a handler for a class. Long, Integer, Double, and Float are all handled by the same NumberPercentDifferenceCalculator class. BigInteger and BigDecimal have their own handlers.
public class QoQCalculator {
private static final Map<Class<?>, AbstractPercentDifferenceCalculator> HANDLERS;
static {
HANDLERS = new HashMap<>();
NumberPercentDifferenceCalculator npdc = new NumberPercentDifferenceCalculator();
HANDLERS.put(Integer.class, npdc);
HANDLERS.put(Long.class, npdc);
HANDLERS.put(Float.class, npdc);
HANDLERS.put(Double.class, npdc);
BigDecimalPercentDifferenceCalculator bdpc = new BigDecimalPercentDifferenceCalculator();
HANDLERS.put(BigDecimal.class, bdpc);
HANDLERS.put(BigInteger.class, new BigIntegerPercentDifferenceCalculator(bdpc));
}
public static <T> Map<String, String> calculateQoq(final T now, final T before) {
final Field[] declaredFields = now.getClass().getDeclaredFields();
if (declaredFields.length == 0) {
return Collections.emptyMap();
}
final Map<String, String> map = new HashMap<>(declaredFields.length, 1);
for (final Field f : now.getClass().getDeclaredFields()) {
try {
f.setAccessible(true);
final Object a = f.get(before);
AbstractPercentDifferenceCalculator calculator = HANDLERS.get(a.getClass());
if (calculator != null) {
map.put(f.getName(), calculator.getPercentDifference(f.get(now), a));
} else {
System.out.println("No handler for " + a.getClass());
}
} catch (final Exception e) {
e.printStackTrace(System.out);
}
}
return map;
}
}
The common base class which requires sub-classes to handle the specific type:
abstract class AbstractPercentDifferenceCalculator<T> {
private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#.##%");
private static final String ZERO = DECIMAL_FORMAT.format(0);
public final String getPercentDifference(Object now, Object before) {
if (returnZeroForNow(cast(now)) || returnZeroForBefore(cast(before))) {
return ZERO;
}
return DECIMAL_FORMAT.format(calculatePercentDifference(cast(now), cast(before)));
}
protected abstract double calculatePercentDifference(T now, T before);
protected abstract T cast(Object o);
protected boolean returnZeroForNow(T now) {
return now == null;
}
protected boolean returnZeroForBefore(T before) {
return before == null;
}
}
And the implementations which cast to their specific type, and know how to calculate the percent difference for that type:
class NumberPercentDifferenceCalculator extends AbstractPercentDifferenceCalculator<Number> {
@Override
protected double calculatePercentDifference(Number now, Number before) {
return (now.doubleValue() - before.doubleValue()) / before.doubleValue();
}
@Override
protected Number cast(Object o) {
return (Number) o;
}
@Override
protected boolean returnZeroForBefore(Number before) {
return super.returnZeroForBefore(before) || before.doubleValue() == 0D;
}
}
class BigDecimalPercentDifferenceCalculator extends AbstractPercentDifferenceCalculator<BigDecimal> {
protected double calculatePercentDifference(BigDecimal now, BigDecimal before) {
return now.subtract(before).divide(before, BigDecimal.ROUND_CEILING).doubleValue();
}
@Override
protected BigDecimal cast(Object o) {
return (BigDecimal) o;
}
@Override
protected boolean returnZeroForBefore(BigDecimal before) {
return super.returnZeroForBefore(before) || before.compareTo(BigDecimal.ZERO) == 0;
}
}
class BigIntegerPercentDifferenceCalculator extends AbstractPercentDifferenceCalculator<BigInteger> {
public final BigDecimalPercentDifferenceCalculator delegate;
public BigIntegerPercentDifferenceCalculator(BigDecimalPercentDifferenceCalculator delegate) {
this.delegate = delegate;
}
@Override
protected BigInteger cast(Object o) {
return (BigInteger) o;
}
@Override
protected double calculatePercentDifference(BigInteger now, BigInteger before) {
return delegate.calculatePercentDifference(new BigDecimal(now), new BigDecimal(before));
}
@Override
protected boolean returnZeroForBefore(BigInteger before) {
return delegate.returnZeroForBefore(new BigDecimal(before));
}
}
Here's a simple runner:
public class QoQRunner {
public static void main(String[] args) {
Holder before = new Holder(1, 2.0, 3L, BigDecimal.valueOf(4.4), BigInteger.valueOf(5));
Holder now = new Holder(10, 12.0, 13L, BigDecimal.valueOf(2.2), BigInteger.valueOf(15));
Map<String, String> diff = QoQCalculator.calculateQoq(now, before);
System.out.println(diff);
}
@Data // from lombok
@AllArgsConstructor // from lombok
public static class Holder {
private final Integer i;
private final Double d;
private final Long l;
private final BigDecimal bd;
private final BigInteger bi;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With