当前位置: 首页 > news >正文

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

一个金额分摊的算法,将折扣分摊按比例(细单实收在总体的占比)到各个细单中。
此算法需要达到以下要求:

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


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

用例:
细单1:8.91
细单2:21.09
细单3:0.01
三个细单总和是 30.01
折扣金额:30
按比例分摊后,应该只有一项是 0.01

废话不多,直接上代码:
细单对象:

	/*** 细单类*/@Datapublic static class Detail {/*** 用来标识记录*/private Long id;/*** 总额*/private BigDecimal money;}

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

    /*** 分摊** @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;}

测试类:

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来分摊。

/*** 分摊** @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;}

测试类:

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的情况少点。而且需要进行从小到大排序。

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

http://www.lryc.cn/news/393169.html

相关文章:

  • Matlab中collectPlaneWave函数的应用
  • Linux系统的基础知识和常用命令
  • 三相异步电动机的起动方法
  • 【LinuxC语言】手撕Http协议之accept_request函数实现(一)
  • Redis Cluster 模式 的具体实施细节是什么样的?
  • 基于大模型的机器人控制
  • 在 PostgreSQL 中,如何处理数据的版本控制?
  • Rust 组织管理
  • vb.netcad二开自学笔记1:万里长征第一步Hello CAD!
  • Vue的学习之数据与方法
  • 刷题——在二叉树中找到最近公共祖先
  • nginx(三)—从Nginx配置熟悉Nginx功能
  • Python轮子:文件比较器——filecmp
  • uni-app组件 子组件onLoad、onReady事件无效
  • leetcode力扣_排序问题
  • 在 .NET 8 Web API 中实现弹性
  • linux下高级IO模型
  • 掌握Mojolicious会话管理:构建安全、持久的Web应用
  • 24西安电子科技大学马克思主义学院—考研录取情况
  • 12--RabbitMQ消息队列
  • VMware替换关键技术:核心业务系统中,访存密集型应用的性能优化
  • [单master节点k8s部署]20.监控系统构建(五)Alertmanager
  • 用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
  • 磁盘就是一个超大的Byte数组,操作系统是如何管理的?
  • 14-28 剑和诗人2 - 高性能编程Bend和Mojo
  • Stable Diffusion:最全详细图解
  • Apache Seata分布式事务之Seata-Client原理及流程详解
  • Linux wget报未找到命令
  • 38条Web测试经验分享
  • TCP报文校验和(checksum)计算