> 文章列表 > Java模拟rank() over()函数获取分组排名的方法设计及实现

Java模拟rank() over()函数获取分组排名的方法设计及实现

Java模拟rank() over()函数获取分组排名的方法设计及实现

背景

考试批次 班级 姓名 语文
202302 三年一班 张小明 130.00
202302 三年一班 王二小 128.00
202302 三年一班 谢春花 136.00
202302 三年二班 冯世杰 129.00
202302 三年二班 马功成 130.00
202302 三年二班 魏翩翩 136.00

假设我们有如上数据,现在有一个需求需要统计各学生语文单科成绩在班级中的排名和全年段排名,你会如何实现?

很容易的我们想到了 rank() over() 实现

over()是分析函数,可以和 rank()、 dense_rank() 、 row_number() 配合使用。
复制代码

使用语法如下:

RANK() OVER(PARTITION BY COLUMN ORDER BY COLUMN)
dense_rank() OVER(PARTITION BY COLUMN ORDER BY COLUMN)
ROW_NUMBER() OVER(PARTITION BY COLUMN ORDER BY COLUMN)
复制代码

解释:partition by用于给结果集分组,如果没有指定那么它把整个结果集作为一个分组。

  1. rank()涵数主要用于排序,并给出序号 ,对于排序并列的数据给予相同序号,并空出并列所占的名次。
  2. dense_rank() 功能同rank()一样,区别在于不空出并列所占的名次
  3. row_number()涵数则是按照顺序依次使用 ,不考虑并列

rank 结果为 1,2,2,4 dense_rank 结果为 1,2,2,3 row_number 结果为 1,2,3,4

实际应用中,会存在数据从其他外部系统接入且数据量不大等多种情况,那么使用Java代码的方式实现分组排名的功能则显得更加方便。

详细设计及实现

排序定义类 OrderBy

public class OrderBy {private String orderByEL;/* 是否升序*/private boolean ascend;public OrderBy(){//默认升序this.ascend = true;}public String orderByEL(){return this.orderByEL;}public OrderBy orderByEL(String orderByEL){this.orderByEL = orderByEL;return this;}public OrderBy ascend(boolean ascend){this.ascend = ascend;return this;}public boolean ascend(){return this.ascend;}
}
复制代码

该类定义了如下属性:

  1. 排序的fileld
  2. 是否升序

获取排名方法

该方法定义如下:

<T> void rankOver(List<T> dataList, String[] partitionByFields, List<OrderBy> orderByList, String resultField, int rankType);
复制代码

该方法提供了5个入参:

  1. dataList 排序的数据集
  2. partitionByFields 分组field的数组
  3. orderByList 排序字段集合
  4. resultField 排名结果存放的字段
  5. rankType 排名方式
    • 1:不考虑并列(row_number 结果为 1,2,3,4)
    • 2:考虑并列,空出并列所占的名次(rank 结果为 1,2,2,4)
    • 3:考虑并列,不空出并列所占的名次(dense_rank 1,2,2,3)

该方法具体实现如下

    public static <T> void rankOver(List<T> dataList, String[] partitionByFields, List<OrderBy> orderByList, String resultField, int rankType) {if (CollectionUtils.isEmpty(orderByList)) {return;}//STEP_01 剔除掉不参与排名的数据List<T> tempList = new ArrayList<>();for (T data : dataList) {boolean part = true;for (OrderBy rptOrderBy : orderByList) {Object o1 = executeSpEL(rptOrderBy.orderByEL(), data);if (o1 == null) {//参与排序的值为null的话则不参与排名part = false;break;}}if (part) {tempList.add(data);}}if (CollectionUtils.isEmpty(tempList)) {return;}//STEP_02 分组Map<String, List<T>> groupMap = group(tempList, null, partitionByFields);for (List<T> groupDataList : groupMap.values()) {order(orderByList, groupDataList);if (rankType == 1) {int rank = 1;for (T temp : groupDataList) {setFieldValue(temp, resultField, rank);rank++;}} else {int prevRank = Integer.MIN_VALUE;int size = groupDataList.size();for (int i = 0; i < size; i++) {T current = groupDataList.get(i);if (i == 0) {//第一名setFieldValue(current, resultField, 1);prevRank = 1;} else {T prev = groupDataList.get(i - 1);boolean sameRankWithPrev = true;//并列排名for (OrderBy rptOrderBy : orderByList) {Object o1 = executeSpEL(rptOrderBy.orderByEL(), current);Object o2 = executeSpEL(rptOrderBy.orderByEL(), prev);if (!o1.equals(o2)) {sameRankWithPrev = false;break;}}if (sameRankWithPrev) {setFieldValue(current, resultField, getFieldValue(prev, resultField));if (rankType == 2) {++prevRank;}} else {setFieldValue(current, resultField, ++prevRank);}}}}}}
复制代码

使用案例

定义一个学生类:

public class Student {private String batch;private String banji;private String name;private Double yuwen;//extraprivate Integer rank1;private Integer rank2;public Student(String batch, String banji, String name, Double yuwen) {this.batch = batch;this.banji = banji;this.name = name;this.yuwen = yuwen;}
}复制代码

我们写一个方法,返回如下数据:

public List<Student> getDataList() {List<Student> dataList = new ArrayList<>();dataList.add(new Student("202302", "三年一班", "张小明", 130.0));dataList.add(new Student("202302", "三年一班", "王二小", 128.0));dataList.add(new Student("202302", "三年一班", "谢春花", 136.0));dataList.add(new Student("202302", "三年二班", "冯世杰", 129.0));dataList.add(new Student("202302", "三年二班", "马功成", 130.0));dataList.add(new Student("202302", "三年二班", "魏翩翩", 136.0));return dataList;
}
复制代码

获取学生语文成绩的班级排名和年段排名,排名采用并列并空出并列所占用名次的方式。

List<Student> dataList = getDataList();
List<OrderBy> orderByList = new ArrayList<>();
orderByList.add(new OrderBy().orderByEL("yuwen").ascend(false));
//获取全校排名
DataProcessUtil.rankOver(dataList, new String[]{"batch"}, orderByList, "rank1", 2);
//获取班级排名
DataProcessUtil.rankOver(dataList, new String[]{"batch", "banji"}, orderByList, "rank2", 2);
log("语文单科成绩排名情况如下:");Map<String, List<Student>> groupMap = DataProcessUtil.group(dataList, null, new String[]{"batch"});
for (Map.Entry<String, List<Student>> entry : groupMap.entrySet()) {log("考试批次:" + entry.getKey());for (Student s : entry.getValue()) {log(String.format("班级:%s 学生:%s 语文成绩:%s 班级排名:%s 全校排名:%s", s.getBanji(), s.getName(), s.getYuwen(), s.getRank2(), s.getRank1()));}log("");
}
复制代码

结果如下:

语文单科成绩排名情况如下:
考试批次:202302
班级:三年一班 学生:张小明 语文成绩:130.0 班级排名:2 全校排名:3
班级:三年一班 学生:王二小 语文成绩:128.0 班级排名:3 全校排名:6
班级:三年一班 学生:谢春花 语文成绩:136.0 班级排名:1 全校排名:1
班级:三年二班 学生:冯世杰 语文成绩:129.0 班级排名:3 全校排名:5
班级:三年二班 学生:马功成 语文成绩:130.0 班级排名:2 全校排名:3
班级:三年二班 学生:魏翩翩 语文成绩:136.0 班级排名:1 全校排名:1
复制代码

可以看到全校排名中 有两个并列第一名 两个并列第三名,且空出了并列所占用的名次2 和 名次4