项目中遇到一个计算百分比的场景,产品要求保留整数,分项加和必须等于100,之前使用的方式是先计算出各项的百分比,再通过对期中一项的增减总数与100的差值来实现,会出现调整过大导致数据异常的情况。百度搜索并没有找到很好的答案查了google,找到了最大余额法,这里补充一个代码示例。
算法的洞见是每一个分项的误差引入都是由于精度变化引入的,所以每个分项的偏差不会大于1,上面提到的对其中一项的调整会导致多项的偏差累积到一项上,是上面方法出现数据异常的根本原因。通过对余数的排序,对多项进行处理分散了这种偏差,结果也相对合理。
public class PercentageUtil {
@Data
private static class IdDecimalTuple{
private Optionid;
private BigDecimaldecimal;
IdDecimalTuple(Option id, BigDecimal decimal) {
this.id = id;
this.decimal = decimal;
}
}
@Data
public static class Option{
private IntegervoteCount;
private Integerpercentage;
}
public static ListcomputePercentage(List src) {
Integer total = src.stream().map(Option::getVoteCount).reduce(0, Integer::sum);
BigDecimal totalBigDecimal = BigDecimal.valueOf(total);
BigDecimal hundredBigDecimal = BigDecimal.valueOf(100);
List decimalPartList =new LinkedList<>();
src.forEach(votable -> {
BigDecimal percentage = BigDecimal.valueOf(votable.getVoteCount())
.divide(totalBigDecimal,6,RoundingMode.FLOOR).multiply(hundredBigDecimal);
votable.setPercentage(percentage.setScale(0, BigDecimal.ROUND_FLOOR).intValue());
decimalPartList.add(new IdDecimalTuple(votable, percentage.remainder(BigDecimal.ONE)));
});
Integer totalPercentage = src.stream().map(Option::getPercentage).reduce(0, Integer::sum);
if (Objects.equals(totalPercentage, 100)) {
return src;
}
List sortedEntry = decimalPartList.stream().sorted(Comparator.comparing(
IdDecimalTuple::getDecimal).reversed()).collect(
Collectors.toList());
for(int i=0;i<100 - totalPercentage;i++){
Option option = sortedEntry.get(i).getId();
option.setPercentage(option.getPercentage() +1);
}
return src;
}
}