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

现在就可以在返回方法上定义一个注解,实现静默式的修改了

posted @ 2023-03-13 14:18  Mars.wang  阅读(408)  评论(0编辑  收藏  举报