BigDecimal|BigDecimal 拆分 Java实现微信发红包算法

原代码地址
http://18810098265.iteye.com/blog/2369857;
需要实现一个小功能, 就是把一个BigDecimal对象拆分成若干个,保证总和一致,最好能指定精度(几位小数).
想了想这不就是一个类似微信发红包的功能么,于是就顺着这个思路找了找,最后找到博主的这个.注释清楚,实现易懂,稍微修改一下就可以支持任意精度,不仅限于发红包了,修改后的代码如下:

import com.google.common.collect.Lists; import lombok.extern.slf4j.Slf4j; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Collections; import java.util.List; @Slf4j public class RedBagUtils {/** * 每个红包最小金额,单位为与scale有关,例如scale为2,单位为分 */ private static final int MIN_MONEY = 1; /** * 红包金额的离散程度,值越大红包金额越分散 */ private static final double DISPERSE = 1; /** * 根据剩余的总量和总个数,获取一个随机的量 * * @param amount 总量 * @param count总个数 * @return 随机量 */ public static BigDecimal getOneRedBag(BigDecimal amount, int count, int scale) { //pow函数是个计算 10的scale次方的函数 int money = amount.setScale(scale, RoundingMode.HALF_UP).multiply(BigDecimalUtils.pow(10, scale)).intValue(); if (money < MIN_MONEY * count) { log.error("amount={}, count={},最小值设置过大", amount.toPlainString(), count); throw new RuntimeException("最小值设置过大"); }//最大值 = 均值*离散程度 int max = (int) (money * DISPERSE / count); //最大值不能大于总金额 max = max > money ? money : max; return new BigDecimal(randomBetweenMinAndMax(money, count, MIN_MONEY, max)).divide(BigDecimalUtils.pow(10, scale), scale, RoundingMode.HALF_UP); }/** * 在最小值和最大值之间随机产生一个 * * @param money * @param count * @param min: 最小量 * @param max: 最大量 * @return */ public static int randomBetweenMinAndMax(int money, int count, int min, int max) { //最后一个直接返回 if (count == 1) { return money; } //最小和最大金额一样,返最小和最大值都行 if (min == max) { return min; } //最小值 == 均值, 直接返回最小值 if (min == money / count) { return min; } //min<=随机数bag<=max int bag = ((int) Math.rint(Math.random() * (max - min) + min)); //剩余的均值 int avg = (money - bag) / (count - 1); //比较验证剩余的还够不够分(均值>=最小值 是必须条件),不够分的话就是最大值过大 if (avg < MIN_MONEY) { /* * 重新随机一个,最大值改成本次生成的量 * 由于 min<=本次金额bag<=max, 所以递归时bag是不断减小的。 * bag在减小到min之间一定有一个值是合适的,递归结束。 * bag减小到和min相等时,递归也会结束,所以这里不会死递归。 */ return randomBetweenMinAndMax(money, count, min, bag); } else { return bag; } }public static void main(String[] args) { //总量 BigDecimal amount = new BigDecimal(1); //总个数 int count = 10; //最后这个数要和amount一致才对 BigDecimal total = new BigDecimal(0); List list = Lists.newArrayList(); for (int i = 0; i < count; i++) { BigDecimal tem = getOneRedBag(amount.subtract(total), count - i, 4); total = total.add(tem); list.add(tem); System.out.println("第" + (count - i) + "个红包的金额是:" + tem + "元"); } //总金额是否相等 System.out.println("总计金额是否相等:" + (total.compareTo(amount) == 0)); System.out.println("红包个数:" + list.size()); System.out.println("红包金额明细:" + list); Collections.sort(list); System.out.println("排序后的红包明细:" + list); }}

说下大体思路:
  1. 定义两个常量, 最小值和离散系数.最小值很好理解,离散系数干嘛用的呢?离散系数用来算随机时的最大值, 最大值=离散系数*平均值.
  2. 把BigDecimal对象都转成int去做计算比较,这样更直观易懂.
  3. 既然确定了最大值最小值, 那就每次在此区间随机出一个值即可,唯一需要担心的是此次随机出来的值是否过大,后面还够不够分,这里作者采用递归的方式来控制.
  4. 最后一次直接返回剩余的即可.
【BigDecimal|BigDecimal 拆分 Java实现微信发红包算法】最后我在此基础上又改了一下, 可以将一个BigDecimal 切分成指定个数指定精度指定范围的若干个数
/** * 将一个BigDecimal 切分成指定个数指定精度指定范围的若干个数,如果范围设置不当,前面随机的数较小,可能会导致最后一个数过大超出范围 * @param amount 总数量 * @param count 总个数 * @param scale 精度 * @param min 最大值 * @param max 最小值 * @return */ public static List splitBigDecimalFromRange(BigDecimal amount, int count, int scale, BigDecimal min, BigDecimal max) { BigDecimal total = new BigDecimal(0); List list = Lists.newArrayList(); for (int i = 0; i < count; i++) { BigDecimal tem = getOneRedBag(amount.subtract(total), count - i, scale, min, max); total = total.add(tem); list.add(tem); } return list; }private static BigDecimal getOneRedBag(BigDecimal amount, int count, int scale, BigDecimal min, BigDecimal max) { // 转成int做运算 int amountInt = amount.setScale(scale, RoundingMode.HALF_UP).multiply(BigDecimalUtils.pow(10, scale)).intValue(); int minInt = min.setScale(scale, RoundingMode.HALF_UP).multiply(BigDecimalUtils.pow(10, scale)).intValue(); int maxInt = max.setScale(scale, RoundingMode.HALF_UP).multiply(BigDecimalUtils.pow(10, scale)).intValue(); if (amountInt < minInt * count) { throw new RuntimeException("最小值设置过大"); } if (minInt > maxInt){ throw new RuntimeException("最大值小于最小值"); } if (maxInt * count < amountInt){ throw new RuntimeException("最大值设置过小"); } //最大值不能大于总金额 maxInt = maxInt > amountInt ? amountInt : maxInt; //randomBetweenMinAndMax()上面有这个方法 return new BigDecimal(randomBetweenMinAndMax(amountInt, count, minInt, maxInt)).divide(BigDecimalUtils.pow(10, scale), scale, RoundingMode.HALF_UP); }

    推荐阅读