看到這個題目大家可能會一頭霧水,本人表達能力有限,無法用簡單語言描述該算法,標題湊合一下唄。工作後才發現,處理很多生活中業務問題就是在,就像再處理數學題。
用一個案例和大家描述一下吧。總得來說就是總數想等的情況下,如何如何將多對多(n對n)的對應關係拆分成一對一(1對1)的關係,java實現。
假設有
三張付款單(單號,金額) 總數300 : (FK01,100) (FK02, 100) (FK03, 100)
兩張發票(單號,金額) 總數300 : (FP01, 100),(FP02, 200)。
我們在進行賬務覈銷的時候,通常是一張付款單,對應一直髮票金額一致,類似的案例應該還有很多。
方案一(先進先出)
按照先進先出的原則,先拿第一張付款,用第一張發票與他匹配,多還少補,多餘的發票用於下一張付款單,多餘的付款單去下一張發票的金額補上,直到補全爲止,依次類推。
(FK01,FP01, 100)、(FK02,FP02,100)、(FK03,FP02,100)
方案二(按比例拆分、尾差倒擠保證總額一致)
第一步:按比例拆分付款單,倒擠尾差。拆分付款單
(FK01,100,33%) (FK02, 100,33%) (FK03, 100,1-33%-33% )
第二步:按付款單各張單所展的比例拆分,發票金額,餘下的付款單金額,尾差倒擠到最後一張發票。
(FK01,100,33%,FP01,33)
(FK01,100,33%,FP02,(100-33)=67)
(FK02,100,33%,FP01,33)
(FK02,100,33%,FP02,(100-33)=67)
(FK03,100,34%,FP01,34)
(FK03,100,34%,FP02,66)
最終付款單總額
FK01 = 33+67=100
FK02 = 33+67=100
FK03 = 34+66=100
最終發票總額
FP01 = 33+33+34 = 100
FP02 = 66+67+67= 200
案例一代碼實現:
/**
* 先近先出原則 依次匹配每一單
* 金額溢出分配到下一單, 金額不足從後面一單取出來
*建立每張付款對應每張發票的取值關係 N 對 N
* @param mapPay
* @param mapInvoice
* @return
*/
private static Map<String,BigDecimal> getResultByOrder(Map<String,String> mapPay,Map<String,String> mapInvoice){
Map<String,BigDecimal> payInvoiveResult =new HashMap<>();
//需要移除的發票集合
List<String> listRemove =new ArrayList<>();
for(Map.Entry<String,String> entryPay :mapPay.entrySet()){
BigDecimal payMoney = new BigDecimal(entryPay.getValue());
//移除 已匹配過的發票
listRemove.stream().forEach(t->{
mapInvoice.remove(t);
});
listRemove.clear();
for(Map.Entry<String,String> entryInvoice :mapInvoice.entrySet()){
BigDecimal invoiceMoney = new BigDecimal(entryInvoice.getValue());
//當付款金額 正好等於 某張發票餘下的金額 直接取該發票並從集合中移除
if(payMoney.compareTo(invoiceMoney)==0){
payInvoiveResult.put(entryPay.getKey() +"==" +entryInvoice.getKey(),invoiceMoney);
listRemove.add(entryInvoice.getKey());
break;
//當付款金額 小於 某張發票餘下的金額時 直接取該發票,並將發票中的金額改成 扣除改付款單金額後的值
}else if(payMoney.compareTo(invoiceMoney)==-1){
payInvoiveResult.put(entryPay.getKey() +"==" +entryInvoice.getKey(),payMoney);
BigDecimal reduceMoney = invoiceMoney.subtract(payMoney);
mapInvoice.put(entryInvoice.getKey(),reduceMoney.toString());
break;
//當付款金額 大於某張發票餘下的金額 直接取該發票 ,餘下不足的金額 繼續往後遍歷 並移除該發票
}else{
payInvoiveResult.put(entryPay.getKey() +"==" +entryInvoice.getKey(),invoiceMoney);
listRemove.add(entryInvoice.getKey());
payMoney = payMoney.subtract(invoiceMoney);
}
}
}
return payInvoiveResult;
}
案例二代碼實現
/**
* 獲取各條記錄所佔的比重 ,最後一條尾差倒擠
* @param mapPay
* @return
*/
private static Map<String,BigDecimal> getPayRadio(Map<String,String> mapPay){
BigDecimal total = new BigDecimal("0");
//計算總和
for (Map.Entry<String,String> entry:mapPay.entrySet()) {
total =total.add(new BigDecimal(entry.getValue()));
}
Map<String,BigDecimal> resultRaido =new HashMap<>();
// 付款單總數 用於比對是否是最後一張付款單
long countall = mapPay.size();
long count=0;
//已遍歷付款單 所佔比重的和
BigDecimal radioSum =new BigDecimal("0");
for(Map.Entry<String,String> entry:mapPay.entrySet()){
count =count+1;
//當前金額
BigDecimal money = new BigDecimal(entry.getValue());
BigDecimal radio = money.divide(total,2,RoundingMode.HALF_UP);
resultRaido.put(entry.getKey(),radio);
//最後一條記錄 直接尾差倒擠 用1減去 已遍歷付款單 所佔比重的和 (1-radioSum) 確保總數一致
if(count == countall){
resultRaido.put(entry.getKey(),new BigDecimal("1").subtract(radioSum));
}
radioSum = radioSum.add(radio);
}
return resultRaido;
}
/**
* 按比例拆分
* @param mapPay 付款單對應金額
* @param mapPayRadio 每張付款單對應金額所佔比重 尾差倒擠
* @param mapInvoice 發票對應金額
* @return
*/
private static Map<String,BigDecimal> getSplitResult(Map<String,String> mapPay,
Map<String,BigDecimal> mapPayRadio,Map<String,String> mapInvoice){
Map<String,BigDecimal> mapPayInvoiceSplit = new HashMap<>();
for(Map.Entry<String,BigDecimal> entryPayRadio:mapPayRadio.entrySet()){
//每張付款單 佔總數的權重
BigDecimal payRadio = entryPayRadio.getValue();
//每張付款單金額
BigDecimal payMoney = new BigDecimal(mapPay.get(entryPayRadio.getKey()));
//發票總數量,用於判斷是否是最後一張 是否倒擠尾差
long countAll = mapInvoice.size();
//當前已遍歷的發票數量 最後一張倒擠該張付款單的尾差 到最後一張發票 (注 發票總額 與 付款單總額是相等的)
long count =0;
//
BigDecimal invoiceSum =new BigDecimal("0");
for(Map.Entry<String ,String> entryInvoice : mapInvoice.entrySet()){
//當前已遍歷的發票數量 最後一張倒擠該張付款單的尾差 到最後一張發票 (注 發票總額 與 付款單總額是相等的)
count = count+1;
//每張發票的金額
BigDecimal invoiceMoney = new BigDecimal(entryInvoice.getValue());
//獲取每張發票 對應比重的金額
BigDecimal invoiceSplit = payRadio.multiply(invoiceMoney).setScale(2,RoundingMode.HALF_UP);
//最後一條尾差 倒擠 該付款單 分配到最後一單 餘下的金額 因爲總數是一致的
if(count ==countAll){
invoiceSplit = payMoney.subtract(invoiceSum);
}
//建立對應關係
mapPayInvoiceSplit.put(entryPayRadio.getKey()+"_"+payRadio+"=="+entryInvoice.getKey(),invoiceSplit);
//計算 每張付款單 已分配出去的金額彙總
invoiceSum =invoiceSum.add(invoiceSplit);
}
}
return mapPayInvoiceSplit;
}
結果展示
public static void main(String[] args) {
Map<String,String> mapPay =new HashMap<>();
mapPay.put("CD1","280");
mapPay.put("CD2","20");
mapPay.put("CD3","300");
Map<String,String> mapInvoice =new HashMap<>();
mapInvoice.put("FP1","100");
mapInvoice.put("FP2","200");
mapInvoice.put("FP3","300");
Map<String,BigDecimal> mapPayRadio= getPayRadio(mapPay);
Map<String,BigDecimal> mapPayInvoiceSplit = getSplitResult(mapPay,mapPayRadio,mapInvoice);
System.out.println("按比例拆分");
printlnByCode(mapPayInvoiceSplit);
System.out.println("先進先出");
mapPayInvoiceSplit = getResultByOrder(mapPay,mapInvoice);
printlnByCode(mapPayInvoiceSplit);
}
// 排序輸出 便於查看結果
private static void printlnByCode(Map<String,BigDecimal> map) {
List<Map.Entry<String, BigDecimal>> list = new ArrayList<Map.Entry<String, BigDecimal>>(map.entrySet());
Collections.sort(list, new Comparator<Map.Entry<String, BigDecimal>>() {
@Override
public int compare(Map.Entry<String, BigDecimal> o1, Map.Entry<String, BigDecimal> o2) {
return o1.getKey().compareTo(o2.getKey());
}
});
list.forEach(entry->{
System.out.println(entry.getKey()+" : "+entry.getValue());
});
}
目前的方案一,還可優化升級,根據規則,如何拆分出最優解,總數一致的情況,使得拆分出來的單據最少,是否可以通過排序,控制單據的先後關係。