订单折扣金额分摊算法|代金券分摊|收银系统|积分分摊|分摊|精度问题|按比例分配|钱分摊|钱分配

一个金额分摊 的算法,将折扣分摊按比例(细单实收在总体的占比 )到各个细单中。

此算法需要达到以下要求:

  1. 折扣金额接近细单总额,甚至折扣金额等于细单金额,某些时候甚至超过细单总额,要保证实收不为负数。
  2. 复杂度O(n)

...

写这个算法的初衷,就是因为现在网上的分摊算法,都没有考虑到最后一项不够减、只循环一次、折扣金额接近总额...

用例:

细单1:8.91

细单2:21.09

细单3:0.01

三个细单总和是 30.01

折扣金额:30

按比例分摊后,应该只有一项是 0.01

废话不多,直接上代码:

细单对象:

java 复制代码
	/**
     * 细单类
     */
    @Data
    public static class Detail {

        /**
         * 用来标识记录
         */
        private Long id;

        /**
         * 总额
         */
        private BigDecimal money;
    }

分摊算法:(忽略了金额从小到大排序,后续补上)

java 复制代码
    /**
     * 分摊
     *
     * @param detailList    细单
     * @param discountMoney 折扣
     * @return 新的细单集合
     */
    public static List<Detail> allocateDiscountMoney(List<Detail> detailList, BigDecimal discountMoney) {

        // 分摊总金额
        BigDecimal allocatedAmountTotal = discountMoney;
        // 剩余分摊金额
        BigDecimal leftAllocatedAmount = allocatedAmountTotal;
        // 订单总实收
        BigDecimal orderTotalAmount = detailList.stream().map(Detail::getMoney).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
        // 结果集
        List<Detail> resultList = new ArrayList<>();
        for (int i = 0; i < detailList.size(); i++) {
            // 结果
            Detail resultDetail = new Detail();
            BeanUtils.copyProperties(detailList.get(i), resultDetail);
            BigDecimal money = resultDetail.getMoney();
            // 占比比例=自身实收/实收总额
            BigDecimal proportion = money.divide(orderTotalAmount, 10, RoundingMode.UP);
            // 分摊金额 = 总分摊金额*占比比例
            BigDecimal allocatedMoney = allocatedAmountTotal.multiply(proportion);
            // 折扣分摊金额向上取整,将精度差异提前吸收,此举使得最后一项足够吸收剩余折扣金额
            allocatedMoney = allocatedMoney.setScale(2, RoundingMode.UP);
            // 是否该订单最后一条商品 或者 已经不够分摊
            if (i == detailList.size() - 1 || leftAllocatedAmount.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) <= 0) {
                allocatedMoney = leftAllocatedAmount;
            }
            // 防止订单金额负数(若最后一项执行此逻辑,则导致总金额有误)
            if (money.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) < 0) {
                allocatedMoney = money;
            }
            // 单个商品分摊后的金额
            BigDecimal goodsActualMoneyAfterAllocated = money.subtract(allocatedMoney);
            // 累减已分摊金额
            leftAllocatedAmount = leftAllocatedAmount.subtract(allocatedMoney);
            resultDetail.setMoney(goodsActualMoneyAfterAllocated);
            resultList.add(resultDetail);
        }
        return resultList;
    }

测试类:

java 复制代码
    public static void main1() {
        List<Detail> detailList = new ArrayList<>();
        //
        Detail detail = new Detail();
        detail.setId(1L);
        detail.setMoney(new BigDecimal("8.91"));
        detailList.add(detail);
        //
        Detail detail2 = new Detail();
        detail2.setId(2L);
        detail2.setMoney(new BigDecimal("21.07"));
        detailList.add(detail2);
        //
        Detail detail3 = new Detail();
        detail3.setId(3L);
        detail3.setMoney(new BigDecimal("0.01"));
        detailList.add(detail3);

        System.out.println("分摊前:" + JSON.toJSONString(detailList));

        List<Detail> allocated = allocateDiscountMoney(detailList, new BigDecimal("30"));

        System.out.println("分摊后:" + JSON.toJSONString(allocated));
    }

问题:为什么每一项算分摊金额都是向上取整?

答:除最后一项外的每一项的折扣分摊算多了,最后一项就分摊得少,保证最后一项一定够分摊,前面的项在迭代时可以做金额如果不够分摊的兜底处理。而如果这么做,前面的不先兜底,后面的如果不够分摊是需要再往前找项来帮忙分摊的,复杂度就比较高。

~~

折扣金额的分摊,是反向 的,其实正向 的分摊也一并适用,并且逻辑是等价的。

例如:

细单1:8.91

细单2:21.09

细单3:0.01

三个细单总和是 30.01

折扣金额:30

我们也可以看做最终金额为 0.01,用0.01来分摊。

java 复制代码
/**
     * 分摊
     *
     * @param detailList    细单
     * @param tgtTotalMoney 待分摊的目标总金额
     * @return 新的细单集合
     */
    public static List<Detail> allocateTgtTotalMoney(List<Detail> detailList, BigDecimal tgtTotalMoney) {

        // 分摊总金额
        BigDecimal allocatedAmountTotal = tgtTotalMoney;
        // 剩余分摊金额
        BigDecimal leftAllocatedAmount = allocatedAmountTotal;
        // 订单总实收
        BigDecimal orderTotalAmount = detailList.stream().map(Detail::getMoney).reduce(BigDecimal::add).orElse(BigDecimal.ZERO);
        // 结果集
        List<Detail> resultList = new ArrayList<>();
        for (int i = 0; i < detailList.size(); i++) {
            // 结果
            Detail resultDetail = new Detail();
            BeanUtils.copyProperties(detailList.get(i), resultDetail);
            BigDecimal money = resultDetail.getMoney();
            // 占比比例=自身实收/实收总额
            BigDecimal proportion = money.divide(orderTotalAmount, 10, RoundingMode.UP);
            // 分摊金额 = 总分摊金额*占比比例
            BigDecimal allocatedMoney = allocatedAmountTotal.multiply(proportion);
            // 折扣分摊金额向上取整,将精度差异提前吸收,此举使得最后一项足够吸收剩余折扣金额
            allocatedMoney = allocatedMoney.setScale(2, RoundingMode.UP);
            // 是否该订单最后一条商品 或者 已经不够分摊
            if (i == detailList.size() - 1 || leftAllocatedAmount.subtract(allocatedMoney).compareTo(BigDecimal.ZERO) <= 0) {
                allocatedMoney = leftAllocatedAmount;
            }
            // 累减已分摊金额
            leftAllocatedAmount = leftAllocatedAmount.subtract(allocatedMoney);
            resultDetail.setMoney(allocatedMoney);
            resultList.add(resultDetail);
        }
        return resultList;
    }

测试类:

java 复制代码
    public static void main2() {
        List<Detail> detailList = new ArrayList<>();
        //
        Detail detail = new Detail();
        detail.setId(1L);
        detail.setMoney(new BigDecimal("8.91"));
        detailList.add(detail);
        //
        Detail detail2 = new Detail();
        detail2.setId(2L);
        detail2.setMoney(new BigDecimal("21.07"));
        detailList.add(detail2);
        //
        Detail detail3 = new Detail();
        detail3.setId(3L);
        detail3.setMoney(new BigDecimal("0.01"));
        detailList.add(detail3);

        System.out.println("分摊前:" + JSON.toJSONString(detailList));

        List<Detail> allocated = allocateTgtTotalMoney(detailList, new BigDecimal("0.1"));

        System.out.println("分摊后:" + JSON.toJSONString(allocated));
    }

算法缺点(隐患):在折扣分摊的算法中,在需要保证每一项细单金额大于0的场景下,此算法需要谨慎使用,因为可能会把金额分摊为0元,但其实这也很难避免,因为总价30.01,折扣30,有很多项都会是0,只能说还有改进的空间,可以进行改动(例如向上取整)以保证在概率上出现0的情况少点。而且需要进行从小到大排序。

对你有帮助的话,点赞、收藏、评论、关注,谢谢各位大佬了~

相关推荐
喵叔哟4 分钟前
重构代码中引入外部方法和引入本地扩展的区别
java·开发语言·重构
尘浮生10 分钟前
Java项目实战II基于微信小程序的电影院买票选座系统(开发文档+数据库+源码)
java·开发语言·数据库·微信小程序·小程序·maven·intellij-idea
不是二师兄的八戒33 分钟前
本地 PHP 和 Java 开发环境 Docker 化与配置开机自启
java·docker·php
怀澈12236 分钟前
高性能服务器模型之Reactor(单线程版本)
linux·服务器·网络·c++
nuclear201144 分钟前
使用Python 在Excel中创建和取消数据分组 - 详解
python·excel数据分组·创建excel分组·excel分类汇总·excel嵌套分组·excel大纲级别·取消excel分组
爱编程的小生1 小时前
Easyexcel(2-文件读取)
java·excel
chnming19871 小时前
STL关联式容器之set
开发语言·c++
Lucky小小吴1 小时前
有关django、python版本、sqlite3版本冲突问题
python·django·sqlite
带多刺的玫瑰1 小时前
Leecode刷题C语言之统计不是特殊数字的数字数量
java·c语言·算法
爱敲代码的憨仔1 小时前
《线性代数的本质》
线性代数·算法·决策树