最近在处理费率和保底费的优惠及标准区间。问题本质就是:标准合作区间是一个【时间段+标准值】,优惠合作区间是多段【时间段+标准值】,并且各个时间段的开始和结束日期可以随意指定,优惠区间和标准区间重合部分按照优惠值计算,非重合部分按照标准区间值计算。
问题场景
上游对时间段的限制基本没有,唯一的限制就是多段优惠区间是连续的。
优惠期结束晚于合作期
优惠期开始早于合作期
优惠期大于合作期
优惠期和合作期完全不沾边
由于各种可能行都存在,用if else条件判断起来太麻烦,可能性太多了
解决方法
当问题复杂的时候不妨把问题抽象看待:由于费率和保底费的计算都是以天为最小单位的,所以我们可以把每段时间都拆成一天一天的,放到一个集合里,然后以标准的时间值Map为基准,匹配优惠的,匹配到就修改该最小单元对应的值为优惠值,匹配不到则继续匹配,直到匹配结束。匹配完成后我们再依据值的连续性,将连续相同值的最小单元输出为时间段和值
1 最小单元拆分
第一步:分别将连续的优惠区间和标准区间进行最小单元化处理:
/** * 将时间段和值拆分为天和值 * * @param beginDate the begin date * @param endDate the end date * @param fee the fee * @return the tree map */ public TreeMap<LocalDate, BigDecimal> Build(LocalDate beginDate, LocalDate endDate, BigDecimal fee) { TreeMap<LocalDate, BigDecimal> feeMap = new TreeMap<>(); for (LocalDate date = beginDate; date.isBefore(endDate) || date.isEqual(endDate); date = date.plusDays(1)) { date = DateUtil.stringToLocalDate(date.format(fmt), DateUtil.DATE_FORMAT); feeMap.put(date, fee); } return feeMap; }
2 最小单元染色
第二步:对最小单元区间进行Merge处理:基于标准区间,对重合部分的优惠区间重新赋值
/** * 以标准值时间段为基准,相同时间天的按优惠值 * * @param standard the standard * @param discount the discount */ public void Merge(TreeMap<LocalDate, BigDecimal> standard, TreeMap<LocalDate, BigDecimal> discount) { Set<LocalDate> days = standard.keySet(); for (LocalDate day : days) { if (discount.containsKey(day)) { standard.put(day, discount.get(day)); } } }
3 最小单元区间合并
第三步:将处理好的最小单元和值Map进行连续值时间区间合并
/** * 将最小单元再转为时间段和值 * * @param full the full * @return the list */ public List<DateAndValueInfo> Output(TreeMap<LocalDate, BigDecimal> full) { List<DateAndValueInfo> dateAndValueInfos = new ArrayList<>(); Set<LocalDate> days = full.keySet(); BigDecimal fee = new BigDecimal("0"); LocalDate start = null; LocalDate end = null; int count = 0; for (LocalDate day : days) { count++; if (fee.toPlainString().equals(ZERO) && start == null) { fee = full.get(day); start = day; end = day; continue; } if (!fee.equals(full.get(day))) { // 相同值连续阶段结束时,将本段连续值添加到时间段,最后一阶段也添加 Date beforeStartDate = DateUtil.localDateToDate(start); Date beforeEndDate = DateUtil.localDateToDate(end); DateAndValueInfo dateAndValueInfo = new DateAndValueInfo(beforeStartDate, beforeEndDate, fee); dateAndValueInfos.add(dateAndValueInfo); start = day; fee = full.get(day); } end = day; if (count == days.size()) { // 最后一阶段也添加 Date beforeStartDate = DateUtil.localDateToDate(start); Date beforeEndDate = DateUtil.localDateToDate(end); DateAndValueInfo dateAndValueInfo = new DateAndValueInfo(beforeStartDate, beforeEndDate, fee); dateAndValueInfos.add(dateAndValueInfo); } } return dateAndValueInfos; }
整体的代码调用如下:
/** * 合并处理时间段和值 * * @param disCountDateAndValueInfo the dis count date and value info * @param standardDateAndValueInfo the standard date and value info * @return the list */ public List<DateAndValueInfo> handleDateAndValues(List<DateAndValueInfo> disCountDateAndValueInfo, DateAndValueInfo standardDateAndValueInfo) { TreeMap<LocalDate, BigDecimal> disCounts = new TreeMap<>(); for (DateAndValueInfo dateAndValueInfo : disCountDateAndValueInfo) { TreeMap<LocalDate, BigDecimal> disCount = Build(DateUtil.dateToLocalDate(dateAndValueInfo.getFromDate()), DateUtil.dateToLocalDate(dateAndValueInfo.getToDate()), dateAndValueInfo.getValue()); disCounts.putAll(disCount); } TreeMap<LocalDate, BigDecimal> standard = Build(DateUtil.dateToLocalDate(standardDateAndValueInfo.getFromDate()), DateUtil.dateToLocalDate(standardDateAndValueInfo.getToDate()), standardDateAndValueInfo.getValue()); Merge(standard, disCounts); return Output(standard); }
用最小单元染色法解决甚至可以处理这样毫无规律的时间段merge
拓展处理
还可以解决这类问题:当不需要处理标准段而只需要用标准区间卡一个时间范围的话:
/** * 获取优惠段和合作区间merge的时间 * * @param disCountDateAndValueInfo the dis count date and value info * @param standardDateAndValueInfo the standard date and value info * @return the list */ private List<DateAndValueInfo> handleValidateInfo(List<DateAndValueInfo> disCountDateAndValueInfo, DateAndValueInfo standardDateAndValueInfo) { // 所有优惠期统一划分为细粒度值-时间 TreeMap<LocalDate, BigDecimal> disCountInfos = new TreeMap<>(); for (DateAndValueInfo dateAndValueInfo : disCountDateAndValueInfo) { TreeMap<LocalDate, BigDecimal> disCount = Build(DateUtil.dateToLocalDate(dateAndValueInfo.getFromDate()), DateUtil.dateToLocalDate(dateAndValueInfo.getToDate()), dateAndValueInfo.getValue()); disCountInfos.putAll(disCount); } LocalDate realStartDate = disCountInfos.firstKey().isBefore(DateUtil.dateToLocalDate(standardDateAndValueInfo.getFromDate())) ? DateUtil.dateToLocalDate(standardDateAndValueInfo.getFromDate()) : disCountInfos.firstKey(); LocalDate realEndDate = disCountInfos.lastKey().isBefore(DateUtil.dateToLocalDate(standardDateAndValueInfo.getToDate())) ? DateUtil.dateToLocalDate(standardDateAndValueInfo.getToDate()) : disCountInfos.lastKey(); // 不在卡死时间段内的值干掉 Set<LocalDate> days = disCountInfos.keySet(); for (LocalDate day : days) { if (day.isBefore(realStartDate) || day.isAfter(realEndDate)) { disCountInfos.remove(day); } } return Output(disCountInfos); }
总结一下
大道至简,当问题复杂时,一定是因为思维陷入到了复杂的逻辑里,要学会思考如何抽象看待问题,透过现象看本质。