/* * The Kuali Financial System, a comprehensive financial management system for higher education. * * Copyright 2005-2014 The Kuali Foundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.kuali.kfs.module.cam.util; import java.math.BigDecimal; import org.kuali.rice.core.api.util.type.KualiDecimal; /** * Utility class that provides methods to allocate money into a number of targets according to certain policies. * Note: It's assumed that the currency we use here is US dollar. */ public class KualiDecimalUtils { private static org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(KualiDecimalUtils.class); /** * Allocates evenly a sum of money amongst a number of targets. * Note: * 1. This method assumes that the divisor is a positive integer. Validation of the the passed in parameters shall be the caller's responsibility. * 2. If the sum can't be evenly divided due to limited accuracy, the last few elements will be adjusted (each by 1c) to reflect the round-up difference. * * @param amount the total amount to allocate * @param divisor the number of targets to allocate to * @return an array of allocated amounts */ public static KualiDecimal[] allocateByQuantity(KualiDecimal amount, int divisor) { if (amount == null || divisor == 0) { return amount == null ? null : new KualiDecimal[] { amount }; } // calculate evenly divided amount KualiDecimal dividedAmount = amount.divide(new KualiDecimal(divisor)); // set the allocated amount into array KualiDecimal[] amountsArray = new KualiDecimal[divisor]; for (int i = 0; i < divisor; i++) { amountsArray[i] = dividedAmount; } // compute the difference between the original total amount and the allocated total amount, due to round up error in division KualiDecimal allocatedAmount = dividedAmount.multiply(new KualiDecimal(divisor)); KualiDecimal reminderAmount = amount.subtract(allocatedAmount); /* Note: * We choose to distribute 1c to each of the last N elements, instead of putting the total difference into the last one element, because: * 1. This way the allocation is more even; otherwise the last element may end up taking too much or too little (even negative). * For ex, suppose amount = $99, and divisor = 10000. So the first 9999 elements would each get 1c, while the last one would get -99c, * if we dump all of the roundup error into the last element. * 2. The round up error for each element is less than 1c (in fact, it's less than 0.5c, as KualiDecimal uses ROUND_HALF_UP), * thus the total error is less than 1c * divisor, which means we can safely distribute 1c to the last N elements, where N < divisor. */ // since KualiDecimal has 2 digits after decimal point, multiply it by 100 guarantees that we get an integer number of cents int n = reminderAmount.abs().multiply(new KualiDecimal(100)).intValue(); KualiDecimal cent = reminderAmount.isPositive() ? new KualiDecimal(0.01) : new KualiDecimal(-0.01); // compensate the difference by offsetting the last N elements, each by 1c; here N = reminderAmount * 100 for (int i = divisor - 1; i >= divisor - n ; i--) { amountsArray[i] = dividedAmount.add(cent); } return amountsArray; } /** * Allocates by ratio a sum of money amongst a number of targets. * Note: * 1. This method assumes that 0 < ratio < 1 and all ratios sum up to 1. Validation of the the passed in parameters shall be the caller's responsibility. * 2. If the allocated amount doesn't sum up to exactly the total amount due to limited accuracy, the last few elements will be adjusted (each by 1c) to reflect the round-up difference. * * @param amount the total amount to allocate * @param ratios an array of ratios according to which the total amount is to be allocated * @return an array of allocated amounts */ public static KualiDecimal[] allocateByRatio(KualiDecimal amount, double[] ratios) { if (ratios == null || ratios.length == 0 || amount == null) { return amount == null ? null : new KualiDecimal[] { amount }; } // allocate amounts into array according to the ratios KualiDecimal[] amountsArray = new KualiDecimal[ratios.length]; KualiDecimal allocatedAmount = KualiDecimal.ZERO; for (int i = 0; i < ratios.length; i++) { KualiDecimal currAmount = new KualiDecimal(amount.bigDecimalValue().multiply(new BigDecimal(ratios[i]))); amountsArray[i] = currAmount; allocatedAmount = allocatedAmount.add(currAmount); } // compute the difference between the original total amount and the allocated total amount, due to round up error in division KualiDecimal reminderAmount = amount.subtract(allocatedAmount); /* Note: * We choose to distribute 1c to each of the last N elements, instead of putting the total difference into the last one element, because: * 1. This way the allocation is more even; otherwise the last element may end up taking too much or too little (even negative). * For ex, suppose amount = $99, and ratio[0-9999] = 0.0001. So the first 9999 elements would each get 1c, while the last one would get -99c, * if we dump all of the roundup error into the last element. * 2. The round up error for each element is less than 1c (in fact, it's less than 0.5c, as KualiDecimal uses ROUND_HALF_UP), * thus the total error is less than 1c * ratio.length, which means we can safely distribute 1c to the last N elements, where N < ratio.length. */ // since KualiDecimal has 2 digits after decimal point, multiply it by 100 guarantees that we get an integer number of cents int n = reminderAmount.abs().multiply(new KualiDecimal(100)).intValue(); KualiDecimal cent = reminderAmount.isPositive() ? new KualiDecimal(0.01) : new KualiDecimal(-0.01); // If the ratios sum up to 1, then we should have N < ratios.length; // However, if that's not the case, it's possible that N > ratio.length; in this case, // the best we can do is to chop N to length to avoid ArrayIndexOutOfBoundary exception n = n > ratios.length ? ratios.length : n; // compensate the difference by offsetting the last N elements, each by 1c; here N = reminderAmount * 100 for (int i = ratios.length - 1; i >= ratios.length - n ; i--) { amountsArray[i] = amountsArray[i].add(cent); } return amountsArray; } /** * Makes sure no null pointer exception occurs on fields that can accurately be null when multiplying. If either field are null * the value is returned. * * @param value * @param multiplier * @return */ public static KualiDecimal safeMultiply(KualiDecimal value, double multiplier) { if (value == null) { return value; } else { return new KualiDecimal(value.bigDecimalValue().multiply(new BigDecimal(multiplier))); } } /** * Makes sure no null pointer exception occurs on fields that can accurately be null when subtracting. If either field are null * the value is returned. * * @param value * @param subtrahend * @return */ public static KualiDecimal safeSubtract(KualiDecimal value, KualiDecimal subtrahend) { if (subtrahend == null || value == null) { return value; } return value.subtract(subtrahend); } }