java实现一组数据计算同比环比
做数据统计的时候经常会有这样的需求,给你一组历史数据,包含两列,一列是日期,一列是数值,让你计算出同比环比。
其实这样的需求很多,而且不复杂,但是用SQL并不好实现。
一、自定义子类版
我尝试用java实现一下,下面是一个日期生成器,方便后面造案例数据
public class DateUtils { /** * 最近30天生成器,不包含当天 * * @param today * @param interval=30 * @return */ public static List<LocalDate> generateLast30Days(LocalDate today, int interval) { List<LocalDate> localDates = new ArrayList<>(); for (int i = interval; i > 0; i--) { LocalDate localDate = today.minusDays(i); localDates.add(localDate); } return localDates; } /** * 最近12个月生成器,不包括本月 * * @param today * @param interval=12 * @return */ public static List<LocalDate> generateLast12Months(LocalDate today, int interval) { List<LocalDate> localDates = new ArrayList<>(); for (int i = interval; i > 0; i--) { LocalDate localDate = today.minusMonths(i); localDates.add(localDate.withDayOfMonth(1)); } return localDates; } /** * 指定时间单元前一个时间点的值 * 比如:today=2022-10-01, * timeUnit=Days=>2022-09-30 * timeUnit=MONTHS=>2022-09-01 * timeUnit=YEARS=>2021-10-01 * @param today * @param timeUnit * @return */ public static LocalDate previousDate(LocalDate today, ChronoUnit timeUnit) { switch (timeUnit) { case DAYS: return today.minusDays(1); case MONTHS: return today.minusMonths(1); case YEARS: return today.minusYears(1); default: throw new IllegalArgumentException("不支持的时间区间类型!"); } } public static LocalDate nextDate(LocalDate today, ChronoUnit timeUnit) { switch (timeUnit) { case DAYS: return today.plusDays(1); case MONTHS: return today.plusMonths(1); case YEARS: return today.plusYears(1); default: throw new IllegalArgumentException("不支持的时间区间类型!"); } } }
接下来,我们定义一个模型,只有两个属性
public class Model { private String dt; private Double num; public Model(String dt, Double num) { this.dt = dt; this.num = num; } public String getDt() { return dt; } public Double getNum() { return num; } @Override public String toString() { return "Model{" + "dt='" + dt + '\'' + ", num=" + num + '}'; } }
接下来是要输出的模型,在上面的基础上,增加了环比、同比
import java.text.DecimalFormat; /** * @Author : wangbin * @Date : 2023/3/13 13:30 * @Description: */ public class ModelPlus extends Model{ private static final DecimalFormat decimalFormat = new DecimalFormat("#.##"); private Double basisRate; private Double loopRate; public ModelPlus(String dt, Double num) { super(dt, num); } public static ModelPlus build(Model model){ return new ModelPlus(model.getDt(), model.getNum()); } public Double getBasisRate() { return basisRate; } public void setBasisRate(Double basisRate) { this.basisRate = basisRate; } public Double getLoopRate() { return loopRate; } public void setLoopRate(Double loopRate) { this.loopRate = loopRate; } @Override public String toString() { return "ModelPlus{" + "dt="+this.getDt()+ ",num="+decimalFormat.format(this.getNum())+ ",basisRate=" + decimalFormat.format(basisRate) + ", loopRate=" + decimalFormat.format(loopRate) + '}'; } }
最后是我们的核心逻辑,先随机生成近18个月的数据,然后计算出环比、同比
import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; import java.util.List; import java.util.Map; import java.util.Random; import java.util.stream.Collectors; public class Main { private final static DateTimeFormatter formats=DateTimeFormatter.ofPattern("yyyy-MM"); private final static Random rand=new Random(); public static void main(String[] args) { List<Model> models = build(); Map<String, Double> map = models.stream().collect(Collectors.toMap(Model::getDt, Model::getNum)); List<ModelPlus> list = models.stream().map(model -> { ModelPlus plus = ModelPlus.build(model); String dt = plus.getDt(); Double num = plus.getNum(); String lastM = getLastMonth(dt); String lastYM = getLastYearMonth(dt); Double lastMV = map.get(lastM); Double lastYMV = map.get(lastYM); Double basisRate = basisRate(lastYMV, num); plus.setBasisRate(basisRate); Double loopRate = basisRate(lastMV, num); plus.setLoopRate(loopRate); return plus; }).collect(Collectors.toList()); for (ModelPlus modelPlus : list) { System.out.println(modelPlus); } } //同比或环比 public static Double basisRate(Double lastVal,Double thisVal){ if(lastVal==null||lastVal==0d){ return 0d; } return 100*(thisVal-lastVal)/lastVal; } //上月 public static String getLastMonth(String month){ DateTimeFormatter format = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM") .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); LocalDate parse = LocalDate.from(format.parse(month)); LocalDate lastMonth = parse.minusMonths(1); return format.format(lastMonth); } //去年同月 public static String getLastYearMonth(String month){ DateTimeFormatter format = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM") .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); LocalDate parse = LocalDate.from(format.parse(month)); LocalDate lastMonth = parse.minusYears(1); return format.format(lastMonth); } public static List<Model> build() { List<LocalDate> localDates = DateUtils.generateLast12Months(LocalDate.now(), 18); return localDates.stream().map(localDate -> { String dt = formats.format(localDate); double v = 100 * rand.nextDouble(); return new Model(dt, v); }).collect(Collectors.toList()); } }
输出:
ModelPlus{dt=2021-09,num=47.67,basisRate=0, loopRate=0} ModelPlus{dt=2021-10,num=22.03,basisRate=0, loopRate=-53.79} ModelPlus{dt=2021-11,num=57.69,basisRate=0, loopRate=161.89} ModelPlus{dt=2021-12,num=85.7,basisRate=0, loopRate=48.56} ModelPlus{dt=2022-01,num=29.54,basisRate=0, loopRate=-65.53} ModelPlus{dt=2022-02,num=74.51,basisRate=0, loopRate=152.27} ModelPlus{dt=2022-03,num=57.86,basisRate=0, loopRate=-22.35} ModelPlus{dt=2022-04,num=7.75,basisRate=0, loopRate=-86.6} ModelPlus{dt=2022-05,num=87.68,basisRate=0, loopRate=1030.71} ModelPlus{dt=2022-06,num=25.71,basisRate=0, loopRate=-70.68} ModelPlus{dt=2022-07,num=84.05,basisRate=0, loopRate=226.9} ModelPlus{dt=2022-08,num=31.43,basisRate=0, loopRate=-62.61} ModelPlus{dt=2022-09,num=85.92,basisRate=80.24, loopRate=173.38} ModelPlus{dt=2022-10,num=69.99,basisRate=217.74, loopRate=-18.55} ModelPlus{dt=2022-11,num=85.9,basisRate=48.9, loopRate=22.73} ModelPlus{dt=2022-12,num=31.91,basisRate=-62.77, loopRate=-62.86} ModelPlus{dt=2023-01,num=74.53,basisRate=152.34, loopRate=133.59} ModelPlus{dt=2023-02,num=21.13,basisRate=-71.65, loopRate=-71.65}
功能是基本实现了,但是还是有很多的代码,下一步需要把它进行优化。
二、动态生成bean版
有时候,我们不想为了增加两个属性给加一个类,那么这个时候可以利用cglib的动态生成bean功能
对上面的方法进行改造
public class Main { private static final DecimalFormat decimalFormat = new DecimalFormat("#.##"); private final static DateTimeFormatter formats = DateTimeFormatter.ofPattern("yyyy-MM"); private final static Random rand = new Random(); public static void main(String[] args) { List<Model> models = build(); Map<String, Double> map = models.stream().collect(Collectors.toMap(Model::getDt, Model::getNum)); List<Object> list = models.stream().map(model -> { Map<String, Double> newMap = new HashMap<>(); String dt = model.getDt(); Double num = model.getNum(); String lastM = getLastMonth(dt); String lastYM = getLastYearMonth(dt); Double lastMV = map.get(lastM); Double lastYMV = map.get(lastYM); newMap.put("basisRate",basisRate(lastYMV,num)); newMap.put("loopRate",basisRate(lastMV,num)); return generateObject(model,newMap); }).collect(Collectors.toList()); for (Object o : list) { System.out.println(JSON.toJSONString(o)); } } public static Object generateObject(Object model,Map<String,Double> newProperties) { DynamicObject dynamicBean = new DynamicObject(); Map<String, Class<?>> allPropertyType; Map<String, Object> allPropertyValue; //1、在原来的对象新增属性 try { allPropertyType = dynamicBean.getAllPropertyType(model); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } try { allPropertyValue = dynamicBean.getAllPropertyValue(model); } catch (ClassNotFoundException | IllegalAccessException e) { throw new RuntimeException(e); } for (Map.Entry<String, Double> entry : newProperties.entrySet()) { allPropertyType.put(entry.getKey(), entry.getValue().getClass()); allPropertyValue.put(entry.getKey(), entry.getValue()); } dynamicBean.addProperty(allPropertyType, allPropertyValue); return dynamicBean.getObject(); } //同比或环比 public static Double basisRate(Double lastVal, Double thisVal) { if (lastVal == null || lastVal == 0d) { return 0d; } double v = 100 * (thisVal - lastVal) / lastVal; String format = decimalFormat.format(v); return Double.parseDouble(format); } //上月 public static String getLastMonth(String month) { DateTimeFormatter format = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM") .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); LocalDate parse = LocalDate.from(format.parse(month)); LocalDate lastMonth = parse.minusMonths(1); return format.format(lastMonth); } //去年同月 public static String getLastYearMonth(String month) { DateTimeFormatter format = new DateTimeFormatterBuilder() .appendPattern("yyyy-MM") .parseDefaulting(ChronoField.DAY_OF_MONTH, 1) .toFormatter(); LocalDate parse = LocalDate.from(format.parse(month)); LocalDate lastMonth = parse.minusYears(1); return format.format(lastMonth); } public static List<Model> build() { List<LocalDate> localDates = DateUtils.generateLast12Months(LocalDate.now(), 18); return localDates.stream().map(localDate -> { String dt = formats.format(localDate); double v = 100 * rand.nextDouble(); String format = decimalFormat.format(v); double v1 = Double.parseDouble(format); return new Model(dt,v1 ); }).collect(Collectors.toList()); } }
这样不需要有ModelPlus 类了,只需要一个Model类,查看下输出
{"basisRate":0.0,"dt":"2021-09","loopRate":0.0,"num":24.92} {"basisRate":0.0,"dt":"2021-10","loopRate":17.42,"num":29.26} {"basisRate":0.0,"dt":"2021-11","loopRate":5.57,"num":30.89} {"basisRate":0.0,"dt":"2021-12","loopRate":-14.31,"num":26.47} {"basisRate":0.0,"dt":"2022-01","loopRate":226.26,"num":86.36} {"basisRate":0.0,"dt":"2022-02","loopRate":-36.1,"num":55.18} {"basisRate":0.0,"dt":"2022-03","loopRate":79.61,"num":99.11} {"basisRate":0.0,"dt":"2022-04","loopRate":-68.11,"num":31.61} {"basisRate":0.0,"dt":"2022-05","loopRate":-60.3,"num":12.55} {"basisRate":0.0,"dt":"2022-06","loopRate":529.16,"num":78.96} {"basisRate":0.0,"dt":"2022-07","loopRate":3.77,"num":81.94} {"basisRate":0.0,"dt":"2022-08","loopRate":12.51,"num":92.19} {"basisRate":149.68,"dt":"2022-09","loopRate":-32.51,"num":62.22} {"basisRate":238.96,"dt":"2022-10","loopRate":59.4,"num":99.18} {"basisRate":-13.34,"dt":"2022-11","loopRate":-73.01,"num":26.77} {"basisRate":104.0,"dt":"2022-12","loopRate":101.72,"num":54.0} {"basisRate":-79.28,"dt":"2023-01","loopRate":-66.87,"num":17.89} {"basisRate":-88.78,"dt":"2023-02","loopRate":-65.4,"num":6.19}
功能是实现了,只是看起来不是很优雅,还需要进一步优化!
还有一种取巧的办法,就是不自动生成bean,而是把对象转成beanMap,然后再map里添加数据,
这种方式实现起来比较简单,但是改变了方法的返回值。还不是很优雅。
三、动态生成子类版
前面的两种实现都改变了返回值类型,如果能不改变类型,那么我们就可以用AOP的方式来一个切面
搜了一下,还真能实现,万能的CGLIB啊!
先上一个工具类,可以基于旧对象生成新对象,而且新对象的类是旧对象类的子类
import com.alibaba.fastjson.JSON; import net.sf.cglib.beans.BeanGenerator; import net.sf.cglib.beans.BeanMap; import org.apache.commons.beanutils.PropertyUtilsBean; import java.beans.PropertyDescriptor; import java.util.HashMap; import java.util.Map; /** * @Author : wangbin * @Date : 2023/3/13 16:04 * @Description: 从一个旧对象生成一个新对象 */ public class DynamicBean { /** * 目标对象 */ private Object target; /** * 属性集合 */ private BeanMap beanMap; public DynamicBean(Class superclass, Map<String, Class> propertyMap) { this.target = generateBean(superclass, propertyMap); this.beanMap = BeanMap.create(this.target); } /** * bean 添加属性和值 * * @param property * @param value */ public void setValue(String property, Object value) { beanMap.put(property, value); } /** * 获取属性值 * * @param property * @return */ public Object getValue(String property) { return beanMap.get(property); } /** * 获取对象 * * @return */ public Object getTarget() { return this.target; } /** * 根据属性生成对象 * * @param superclass * @param propertyMap * @return */ private Object generateBean(Class superclass, Map<String, Class> propertyMap) { BeanGenerator generator = new BeanGenerator(); if (null != superclass) { generator.setSuperclass(superclass); } BeanGenerator.addProperties(generator, propertyMap); return generator.create(); } public static Object getTarget(Object dest, Map<String, Object> addProperties) { // 获取属性map PropertyUtilsBean propertyUtilsBean = new PropertyUtilsBean(); PropertyDescriptor[] descriptors = propertyUtilsBean.getPropertyDescriptors(dest); Map<String, Class> propertyMap = new HashMap<>(); for (PropertyDescriptor d : descriptors) { if (!"class".equalsIgnoreCase(d.getName())) { propertyMap.put(d.getName(), d.getPropertyType()); } } // 添加新属性 addProperties.forEach((k, v) -> propertyMap.put(k, v.getClass())); // 创建动态bean DynamicBean dynamicBean = new DynamicBean(dest.getClass(), propertyMap); // 给bean添加旧数据 propertyMap.forEach((k, v) -> { try { if (!addProperties.containsKey(k)) { dynamicBean.setValue(k, propertyUtilsBean.getNestedProperty(dest, k)); } } catch (Exception e) { e.printStackTrace(); } }); // 添加新数据 addProperties.forEach((k, v) -> { try { dynamicBean.setValue(k, v); } catch (Exception e) { e.printStackTrace(); } }); return dynamicBean.getTarget(); } public static void main(String[] args) { User entity = new User(); entity.setName("eee"); entity.setAge(222); Map<String, Object> addProperties = new HashMap<>() {{ put("newKey", "newVal"); }}; User finalPicBaseReqVo = (User) getTarget(entity, addProperties); System.out.println(JSON.toJSONString(finalPicBaseReqVo)); } }
最终的逻辑
public class Main { private static final DecimalFormat decimalFormat = new DecimalFormat("#.##"); private final static DateTimeFormatter formats = DateTimeFormatter.ofPattern("yyyy-MM"); private final static Random rand = new Random(); public static void main(String[] args) { List<Model> models = build(); Map<String, Double> map = models.stream().collect(Collectors.toMap(Model::getDt, Model::getNum)); List<Model> list = models.stream().map(model -> { Map<String, Object> newMap = new HashMap<>(); String dt = model.getDt(); Double num = model.getNum(); String lastM = getLastMonth(dt); String lastYM = getLastYearMonth(dt); Double lastMV = map.get(lastM); Double lastYMV = map.get(lastYM); newMap.put("yearOnYearRate",basisRate(lastYMV,num)); newMap.put("monthOnMonthRate",basisRate(lastMV,num)); return (Model) DynamicBean.getTarget(model, newMap); }).collect(Collectors.toList()); for (Model o : list) { System.out.println(JSON.toJSONString(o)); } } }
输出:
{"dt":"2021-09","monthOnMonthRate":0.0,"num":37.57,"yearOnYearRate":0.0} {"dt":"2021-10","monthOnMonthRate":8.86,"num":40.9,"yearOnYearRate":0.0} {"dt":"2021-11","monthOnMonthRate":89.29,"num":77.42,"yearOnYearRate":0.0} {"dt":"2021-12","monthOnMonthRate":2.96,"num":79.71,"yearOnYearRate":0.0} {"dt":"2022-01","monthOnMonthRate":-6.06,"num":74.88,"yearOnYearRate":0.0} {"dt":"2022-02","monthOnMonthRate":-1.35,"num":73.87,"yearOnYearRate":0.0} {"dt":"2022-03","monthOnMonthRate":-96.18,"num":2.82,"yearOnYearRate":0.0} {"dt":"2022-04","monthOnMonthRate":3263.12,"num":94.84,"yearOnYearRate":0.0} {"dt":"2022-05","monthOnMonthRate":-74.66,"num":24.03,"yearOnYearRate":0.0} {"dt":"2022-06","monthOnMonthRate":-16.02,"num":20.18,"yearOnYearRate":0.0} {"dt":"2022-07","monthOnMonthRate":204.26,"num":61.4,"yearOnYearRate":0.0} {"dt":"2022-08","monthOnMonthRate":-78.52,"num":13.19,"yearOnYearRate":0.0} {"dt":"2022-09","monthOnMonthRate":285.6,"num":50.86,"yearOnYearRate":35.37} {"dt":"2022-10","monthOnMonthRate":-21.18,"num":40.09,"yearOnYearRate":-1.98} {"dt":"2022-11","monthOnMonthRate":131.93,"num":92.98,"yearOnYearRate":20.1} {"dt":"2022-12","monthOnMonthRate":-99.03,"num":0.9,"yearOnYearRate":-98.87} {"dt":"2023-01","monthOnMonthRate":9418.89,"num":85.67,"yearOnYearRate":14.41} {"dt":"2023-02","monthOnMonthRate":-67.05,"num":28.23,"yearOnYearRate":-61.78} Process finished with exit code 0
现在就可以在返回方法上定义一个注解,实现静默式的修改了