基于BitMap的工作日间隔计算

背景问题

在我们实际开发过程中,时常会遇到日期的间隔计算,即计算多少工作日之后的日期,在不考虑法定节假日的情况下也不是那么复杂,毕竟周六、周日是相对固定的,Java语言也提供了丰富的类来处理此问题。
然而,当考虑法定节假日,原先的工作日也许变成了休息日,同样原先的休息日变成了工作日,再加上大多数客户是内网环境,节假日信息不得不维护到数据库,所以复杂度立马提升了N个档次。
由此,这里提供一些思路仅供参考。

准备工作

我们可以通过如下网址获取法定节假日的信息法定节假日。返回数据如下,这里只截取部分数据

{
  "2024-01-01": {
    "date": "2024-01-01",
    "name": "元旦",
    "isOffDay": true
  },
  "2024-02-04": {
    "date": "2024-02-04",
    "name": "春节",
    "isOffDay": false
  },
  "2024-02-10": {
    "date": "2024-02-10",
    "name": "春节",
    "isOffDay": true
  },
  "2024-02-11": {
    "date": "2024-02-11",
    "name": "春节",
    "isOffDay": true
  }
}

在这份数据中,列举了全部的法定节假日调休信息。isOffDaytrue,表示和节假日相关的周六日、非周六周日休假;比如2024-01-01为周一,这里为true,表示休假;再比如2024-02-10,为周六,也表示休假。isOffDayfalse,表示周六周日照常上班(也就是我们说的调休)。

解决思路(此章节不是重点,可掠过)

对于此问题,我觉得可以从数据库的设计入手。数据库设计有如下几个思路:

  • 数据库保存特殊日期的数据。比如本应该工作的日期变成了节假日,本应该休息的日期变成了工作日。
    • 入库逻辑。就拿上面的数据,如果isOffDay为false,我们肯定全部入库。如果isOffDay为true,还需要判断日期是否为周六日,如果不是需要入库。
    • 计算工作日。需要针对每一天都要判断是否异常,首先按照正常逻辑处理,然后查询数据库,如果异常(数据库存在),将结果取反。比如查询2024-4-28以后10个工作日的日期,首先查看2024-4-29是否为周末,这里是周一。然后查询数据库,数据库不存在。所以为工作日,计1天,由此向后推10个工作日。
  • 数据库保存放假的数据
    • 入库逻辑。首先通过Java提供的日期类,计算出周六周日的日期列表。然后根据接口提供的数据,如果isOffDayfalse,将此日期在集合中移除;如果isOffDaytrue,判断是否为周六日,如果不是,加入到集合中。最后将集合保存到数据库。
    • 计算工作日。针对每天,需要查询数据库。如果数据库不存在,则工作日+1,否则不变。这里也可以将数据一次性读取,在内存中处理。
  • 数据库保存工作日数据
    • 入库逻辑。这个和存放放假数据相反。
    • 查询工作日。这里可以通过sql就可以查询。比如查询2024-04-28后10个工作日日期。
      select * from t_work_date where f_date > '2024-04-28' order by f_date limit 10
      
      最后一条数据就是指定的工作日。

当然,也可以将所有的数据存放到数据库。增加一个是否工作日的标识。同样可以通过sql搞定。

基于BitMap

上面思路仅供我们了解,不是这次重点。下面我们重点说明BitMap怎么计算工作日指定天数后的日期。我们知道,对于一个日期,它要么是工作,要么休息,我们很容易想到0和1。我们可以将1代表工作日,将0代表休息日。所以针对一年的数据,我们只用365(或者366)个0和1表示就行。接下来,我们同样按照入库逻辑和计算工作日两个方面说明此问题。

入库逻辑

数据库设计

这里我们创建一张表包含两个字段,f_year和f_data。 这里基于postgresql存储,sql语句如下:

create table t_date(
	f_year int2,
	data BYTEA
);

主要逻辑

由于下面代码注释很全面,这里就不写处理逻辑了。需要说明的是这里用到了Java提供的BitSet类。
这个类和其他数组一样,索引也是从0开始的

/**
 * 填充数据。
 * @param year 计算的年份
 */
private static BitSet fillData(Integer year){
	  //返回一年多少天
	  int days = Year.of(year).length();
	  //初始化这一年多少天。
	  BitSet bitSet = new BitSet(days);
	  //默认0,这里反转,全部变为1
	  bitSet.flip(0,days);
	  //计算当前年第一个周六的日期
	  LocalDate firstSaturday = LocalDate.of(year, 1, 1)
	          .with(TemporalAdjusters.nextOrSame(DayOfWeek.SATURDAY));
	  //计算第一个周六在这个月是第几天       
	  int dayOfMonth = firstSaturday.getDayOfMonth();
	  //如果是第7天,说明1月1日是周日。所以先将第一天放假。
	  if(dayOfMonth == 7){
	      bitSet.set(0,false);
	  }
	  //当前周六,7天往后循环加,知道当期年最后一天。
	  for (int i = dayOfMonth; i <= days; i=i+7) {
	      //由于索引从0开始,所以这里-1,
	      //周六放假
	      bitSet.set(i-1,false);
	      //周日放假
	      bitSet.set(i,false);
	  }
	  
	  //解析接口的数据为JSON。这里需要自行调用接口获取json数据
	  JSONObject jsonObject = JSONObject.parseObject(json);
	  jsonObject.forEach((k,v)->{
	  	  //k为日期,v:日期信息{"date": "2024-01-01","name": "元旦","isOffDay": true}
	      LocalDate k1 = LocalDate.parse(k);
	      //获取当前日期在年份是第几天
	      int dayOfYear = k1.getDayOfYear();
	      JSONObject dataInfo = (JSONObject) v;
	      //当前日期是否放假。true:放假。false:不放假
	      Boolean isOffDay = dataInfo.getBoolean("isOffDay");
	      //由于bitSet索引是从0开始,所以这里要减1.
	      //我们这里存储的刚好和是否放假相反,所以这里取反
	      bitSet.set(dayOfYear-1,!isOffDay);
	  });
	  return bitSet;
}

这里我们计算得到的BitMap数据,并将其打印:

private static void printBitSet(BitSet bitSet){
    for (int i = 0; i < bitSet.length(); i++) {
        if(i % 8 == 0){
            System.out.println();
        }else if(i % 4 == 0){
            System.out.print(" ");
        }
        System.out.print(bitSet.get(i)?1:0);
    }
}

截取部分数据,下面双斜线后面的不是输出内容。

0111 1001 //1.8
1111 0011 //1.16
1110 0111 //1.24
1100 1111 //2.1
1011 1111 //2.9
0000 0000 //2.17
1111 1100  //2.25

我们拿到结果,那么怎么将数据存放的数据库呢?只要将BitSet转换为二进制就可以:

byte[] byteArray = bitSet.toByteArray();

这里我们顺便看一下长度:46,也就是46个字节。

计算工作日

加载到JVM缓存中

//byteArray为数据库查询到的f_data二进制数据
BitSet bitSet  = BitSet.valueOf(byteArray);

计算工作日

/**
 * 计算指定日期的工作日
 * @param currentDate 当前日期
 * @param workDay 工作日长度
 * @param bitSet 计算数据
 * @return 计算结果
 */
private static LocalDate calWorkData(String currentDate,int workDay,BitSet bitSet){
    LocalDate parse = LocalDate.parse(currentDate);
    //获取当前日在在一年的第几天
    int begin = parse.getDayOfYear();
    //将计算结果先赋值当前日期
    int last = begin;
    //workDay个工作日,这里循环workDay此
    //对于其他的算法,这里循环的次数为工作日的次数+放假的次数
    for (int i = 0; i < workDay; i++) {
        //找到下一天后的第一个设置为1的位置。
        //注意nextSetBit这个方法,从索引值(包括索引值)开始计算,所以这里要先+1。
        //还有一个方法nextClearBit,表示下一个0的位置。
        last = bitSet.nextSetBit(++last);
    }
    //last就是索引位置,用最后的索引位置-开始的索引的位置,然后将当前日期推后此天数,就是要计算的日期。
    LocalDate localDate = parse.plusDays(last - (long)begin);
    System.out.println(localDate);
    return localDate;
}

存在问题

这里并没有考虑到跨年,有一种思路。由于次年的法定节假日一般是在当年的11月份左右发布。所以在计算下一年记录的时候,将下一年的数据追加到2024年后面。这样,每一条数据的长度就变成92个字节,按照utf8编码,也就是30来个汉字,我们是可以接受的。

存储以及计算复杂度分析

通过上面提供的几种思路,所占用数据库的大小,这里我们做一个对比:

  • BitMap:上面我们也计算了。f_year为2个字节,f_data的92个字节 ,共 94个字节。
  • 数据库保存特殊日期:一个日期记录是2024-04-04,为10个字节,特殊日期这里至少11个。再加上各种调休。按照平均20天算,需要220个字节。
  • 数据库保存放假的数据:周六日(52*2) + 11 = 115天,那么存放字节:115 * 10 = 1150个字节。
  • 数据库保存工作日的数据:(365-115) * 10 = 2500个字节

虽然数据库保存特殊日期BitMap差不多,保存放假数据保存工作日数据存储上分别是BitMap的10倍和20倍。针对计算工作日复杂度,我觉得数据库保存工作日的数据通过一条sql语句搞定,算是最简单的,另外也没有BitMap跨年的问题。

总结

  • BitMap无论在存储和计算工作日的复杂度上都占有明显的优势。
  • 数据库保存工作日的数据方式,虽然占用空间是BitMap的20多倍,2000个字节也可以忽略不计,由于它计算工作日算是最简单的,也不失为采纳的思路。

思考

上面休假与工作的最小单位为一天,如果为半天,上面又该如何计算求取?

相关推荐

  1. 基于BitMap工作日间隔计算

    2024-07-19 00:22:01       22 阅读
  2. Bitmap 基本原理

    2024-07-19 00:22:01       27 阅读
  3. 【OpenCV】基于opencv视频间隔抽帧脚本

    2024-07-19 00:22:01       38 阅读
  4. bash计算时间差 时间间隔

    2024-07-19 00:22:01       36 阅读
  5. oracle sql--计算某一日期到当前日期间隔天数

    2024-07-19 00:22:01       29 阅读
  6. 间隔采样视频代码

    2024-07-19 00:22:01       27 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-07-19 00:22:01       67 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-07-19 00:22:01       71 阅读
  3. 在Django里面运行非项目文件

    2024-07-19 00:22:01       58 阅读
  4. Python语言-面向对象

    2024-07-19 00:22:01       69 阅读

热门阅读

  1. c语言(7.17)

    2024-07-19 00:22:01       25 阅读
  2. UFS协议

    2024-07-19 00:22:01       22 阅读
  3. 透过三星Galaxy Z Fold6,看见高效生活的未来图景

    2024-07-19 00:22:01       20 阅读
  4. 设计模式之观察者模式

    2024-07-19 00:22:01       20 阅读
  5. 微服务拆分流程 (黑马商城拆分商品服务)

    2024-07-19 00:22:01       19 阅读
  6. C# 邮件发送

    2024-07-19 00:22:01       23 阅读
  7. Repl.it: 在线的集成开发环境

    2024-07-19 00:22:01       25 阅读
  8. js基础知识

    2024-07-19 00:22:01       26 阅读