1、问题背景
有电商领域,用户对订单支付完成,我们会对订单进行分账,会按照预定的规则与比例,将订单金额分账到:供应商账户、平台账户、营销人员佣金账户,这就涉及到对金额按照比例分配的问题。对金额进行分配,我们要解决除不尽的问题,金额不能除不尽,否则,账户会不平。
例如,对1.00按照3、3、3的比例分配,则会得到结果0.34、0.33、0.33
2、源码
package cn.vetech.charge.cloud.modules.utils.number;
import java.math.BigDecimal;
import java.math.RoundingMode;
import org.apache.commons.lang3.ArrayUtils;
/**
* 金额工具类,对金额进行分配,解决除不尽,一分钱的问题,单元测试:MoneyUtilTest
* @author LiQiao
* @date 2022/03/07
*/
public final class MoneyUtil {
private MoneyUtil() {}
/**
* 将amount等分为targets等份并保留两位
*
* 将金额尽可能平均分配成targets份。如果不能平均分配尽,则将零头放到开始的若干份中。分配运算能够确保不会丢失金额零头。
*
* @param amount 待拆分金额
* @param targets 待分配的份数
* @return 拆分金额数组, 拆分金额数组的长度与分配份数相同,数组元素从大到小排列,金额最多只相差1分。
*/
public static BigDecimal[] allocate(BigDecimal amount, int targets) {
return allocate(amount, targets, 2);
}
/**
* 将amount等分为targets等份并保留scale位小数
*
* 将金额尽可能平均分配成targets份。如果不能平均分配尽,则将零头放到开始的若干份中。分配运算能够确保不会丢失金额零头。
*
* @param amount 待拆分金额
* @param targets 待分配的份数
* @param scale 保留多少位小数
* @return 拆分金额数组, 拆分金额数组的长度与分配份数相同,数组元素从大到小排列,金额最多只相差1分。
*/
public static BigDecimal[] allocate(BigDecimal amount, int targets, int scale) {
long[] ratios = newLongArray(targets);
return allocate(amount, ratios, scale);
}
/**
* 将amount按照规定的比例分配成若干份并保留2位小数
*
* 将金额尽可能平均分配成targets份。如果不能平均分配尽,则将零头放到开始的若干份中。分配运算能够确保不会丢失金额零头。
*
* @param amount 待拆分金额
* @param ratios 分配比例
* @return 拆分金额数组, 拆分金额数组的长度与分配比例数组相同,数组元素从大到小排列。
*/
public static BigDecimal[] allocate(BigDecimal amount, long[] ratios) {
return allocate(amount, ratios, 2);
}
/**
* 将amount按照规定的比例分配成若干份并保留scale位小数
*
* 将金额尽可能平均分配成targets份。如果不能平均分配尽,则将零头放到开始的若干份中。分配运算能够确保不会丢失金额零头。
*
* @param amount 待拆分金额
* @param ratios 分配比例
* @param scale 保留多少位小数
* @return 拆分金额数组, 拆分金额数组的长度与分配比例数组相同,数组元素从大到小排列。
*/
public static BigDecimal[] allocate(BigDecimal amount, long[] ratios, int scale) {
long[] results = new long[ratios.length];
long sumOfRatios = 0;
for (int i = 0; i <= ratios.length - 1; i++) {
sumOfRatios += ratios[i];
}
long centOfAll = amount.movePointRight(scale).setScale(0, RoundingMode.HALF_EVEN).longValue();
long remainder = centOfAll;
for (int i = 0; i <= results.length - 1; i++) {
long cent = (centOfAll * ratios[i]) / sumOfRatios;
results[i] = cent;
remainder -= results[i];
}
for (int i = 0; i < remainder; i++) {
results[i]++;
}
return convert(results, scale);
}
private static BigDecimal[] convert(long[] longArray, int scale) {
if (ArrayUtils.isEmpty(longArray)) {
return null;
}
BigDecimal[] bigDecimalArray = new BigDecimal[longArray.length];
for (int i = 0; i <= longArray.length - 1; i++) {
bigDecimalArray[i] = BigDecimal.valueOf(longArray[i], scale);
}
return bigDecimalArray;
}
private static long[] newLongArray(int targets) {
long[] longArray = new long[targets];
for (int i = 0; i <= targets - 1; i++) {
longArray[i] = 1;
}
return longArray;
}
}
3、单元测试
package cn.vetech.charge.cloud.modules.utils.number;
import java.math.BigDecimal;
import java.util.Arrays;
import org.junit.Assert;
import org.junit.Test;
/**
* MoneyUtil单元测试
* @author LiQiao
* @date 2022/02/18
*/
public class MoneyUtilTest {
/**
*
* 将1.1分成3等份,保留4位小数,期望结果为:[0.3667, 0.3667, 0.3666]
* @author LiQiao
* @throws InterruptedException
* @date 2022年1月17日 下午2:40:07
*/
@Test
public void allocateTest() {
BigDecimal amount = new BigDecimal("1.1");
BigDecimal[] moneyArray = MoneyUtil.allocate(amount, 3,4);
System.err.println(Arrays.toString(moneyArray));
Assert.assertTrue(moneyArray.length == 3);
Assert.assertTrue(moneyArray[0].equals(new BigDecimal("0.3667")));
Assert.assertTrue(moneyArray[1].equals(new BigDecimal("0.3667")));
Assert.assertTrue(moneyArray[2].equals(new BigDecimal("0.3666")));
}
}