一個金額分攤的算法,將折扣分攤按比例(細單實收在總體的占比)到各個細單中。
此算法需要達到以下要求:
- 折扣金額接近細單總額,甚至折扣金額等于細單金額,某些時候甚至超過細單總額,要保證實收不為負數。
- 復雜度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));}
對你有幫助的話,點贊、收藏、評論、關注,謝謝各位大佬了~