目录
本文收录在《如何开发一个自己的笔记软件》系列中,该系列源码均可在 Blossom 笔记软件 仓库中查看。仓库地址:
如何实现日历中计划安排的排序
在日历中,我们常常看到在一个日历中添加多个计划时,每条计划会占据单独的一行,并且每行之间不会重叠,而且每一行并不是顺序排列的,而是穿插在一起的,例如下图
那么这种排序是如何实现的呢?
首先在新增时,一条计划的数据跨度为几天,那么数据就为几条,例如如下数据,相同的groupId代表同一条计划
[{
"id":1,
"groupId": 1,
"date": "2023-09-01"
}, {
"id":2,
"groupId": 1, // 注意,该条与第一条是统一条计划
"date": "2023-09-02"
}, {
"id":3,
"groupId": 2,
"date": "2023-09-02" // 注意,与第二条是同一天的不同计划
}]
那么在 Java 中表示该数据即为:
// 保存着计划的集合
List<PlanDay> plans
然后将数据转换为按天分组的数据
private static TreeMap<Date, List<PlanDayRes>> byDay(List<PlanDayRes> plans) {
TreeMap<Date, List<PlanDayRes>> map = new TreeMap<>();
for (PlanDayRes plan : plans) {
List<PlanDayRes> list = map.getOrDefault(plan.getPlanDate(), new ArrayList<>());
list.add(plan);
map.put(plan.getPlanDate(), list);
}
}
再将数据转换为按groupId分组的数据,注意,尽管是两个 map,但是所保存的对象引用都是相同的。也就是说,一个 map 中的数据发生变更,另一个 map 中的数据也会发生变更。
private static TreeMap<Long, List<PlanDayRes>> byGroupId(List<PlanDayRes> plans) {
TreeMap<Long, List<PlanDayRes>> map = new TreeMap<>();
for (PlanDayRes plan : plans) {
List<PlanDayRes> list = map.getOrDefault(plan.getGroupId(), new ArrayList<>());
list.add(plan);
map.put(plan.getGroupId(), list);
}
return map;
}
拥有了上述的两个分组后,我们正式进行排序,首先对第一天的数据进行排序,也就是遍历 byDay 方法获取的 map。
第一天开始遍历时,所有的数据都是未进行排序的。此时可以把第一天的第一条计划的顺序设置为1
相应的,我们要把与他是同一条计划,但不同天的数据的排序都设置为1,也就是说当分组中的一条决定了排序后,改组所有数据都使用该排序。并且此时第二条数据也确认排在了第一天的顺序2。如下图:
此时如果第二天又有了新的计划,由于第二天的排序1已经被占用了,所以第二天的第二条计划也就只能位于排序2了,如下图绿色计划:
此时可以看到,第三天的排序1是空着的,那么当遍历第三天的数据时,如果有绿色计划之外的其他安排,就可以放置在排序1了。以上就是全部思路了,其实逻辑很简单,下面是排序代码:
public static List<PlanDayRes> sort(List<PlanDayRes> plans) {
if (CollUtil.isEmpty(plans)) {
return plans;
}
Map<Date, List<PlanDayRes>> byDay = byDay(plans);
Map<Long, List<PlanDayRes>> byGroupId = byGroupId(plans);
// 先为所有计划设置默认排序
plans.forEach(p -> p.setSort(-1));
byDay.forEach((date, list) -> {
for (PlanDayRes plan : list) {
if (plan.getSort() == null) {
plan.setSort(-1);
}
// 排序如果不为 -1,相当于该计划已经被安排好排序了,则跳过
if (plan.getSort() > -1) {
continue;
}
// 遍历当天中所有的计划
for (int i = 0; i < list.size(); i++) {
// 从当天的 排序1 开始
final int targetSort = i;
// 如果该排序已经被占用,则跳过
boolean targetSortHasUsed = list.stream().anyMatch(p -> p.getSort() == targetSort && p.getId() > 0);
if (targetSortHasUsed) {
continue;
}
// 否则将该计划以及计划所在组的所有计划都设置为该排序
for (PlanDayRes groupPlan : byGroupId.get(plan.getGroupId())) {
groupPlan.setSort(targetSort);
}
}
}
// 这是将空的位置设置一个占位的对象,这样方便遍历,例如上图中,第三天的绿色计划排序为2,但排序1的位置是没有计划的。
// 占位对象就是填充在了这个位置,方便遍历。
for (int i = 0; i < list.stream().mapToInt(PlanDayRes::getSort).max().getAsInt(); i++) {
final int targetSort = i;
if (list.stream().noneMatch(p -> p.getSort() == targetSort)) {
plans.add(getHolderPlan(date, targetSort));
}
}
});
return plans;
}
我还写了一个调试工具,可以查看排序后的结果,例如数据如下:
public static void main(String[] args) {
List<PlanDayRes> plans = new ArrayList<>();
PlanDayRes p1_1 = new PlanDayRes();
p1_1.setId(1L);
p1_1.setGroupId(1L);
p1_1.setPlanDate(DateUtils.parse("2023-09-01", DateUtils.PATTERN_YYYYMMDD));
plans.add(p1_1);
PlanDayRes p1_2 = new PlanDayRes();
p1_2.setId(2L);
p1_2.setGroupId(1L);
p1_2.setPlanDate(DateUtils.parse("2023-09-02", DateUtils.PATTERN_YYYYMMDD));
plans.add(p1_2);
PlanDayRes p2 = new PlanDayRes();
p2.setId(3L);
p2.setGroupId(2L);
p2.setPlanDate(DateUtils.parse("2023-09-01", DateUtils.PATTERN_YYYYMMDD));
plans.add(p2);
PlanDayRes p3_1 = new PlanDayRes();
p3_1.setId(4L);
p3_1.setGroupId(3L);
p3_1.setPlanDate(DateUtils.parse("2023-09-02", DateUtils.PATTERN_YYYYMMDD));
plans.add(p3_1);
PlanDayRes p3_2 = new PlanDayRes();
p3_2.setId(4L);
p3_2.setGroupId(3L);
p3_2.setPlanDate(DateUtils.parse("2023-09-03", DateUtils.PATTERN_YYYYMMDD));
plans.add(p3_2);
sortToTreeMap(plans, true);
}
输出结果如下,日期下的数字为 groupId,可以看到确实如预想的进行了排序
2023-09-01|2023-09-02|2023-09-03|
2| 3| 3|
1| 1| |
| | |
本文的全部源码,以及页面样式和前端代码可在如下地址查看,源码所在项目是一个作者维护的双链笔记软件,也希望大家能给个 star