基于BitMap的工作日间隔计算

背景问题

在我们实际开发过程中,时常会遇到日期的间隔计算,即计算多少工作日之后的日期,在不考虑法定节假日的情况下也不是那么复杂,毕竟周六、周日是相对固定的,Java语言也提供了丰富的类来处理此问题。

然而,当考虑法定节假日,原先的工作日也许变成了休息日,同样原先的休息日变成了工作日,再加上大多数客户是内网环境,节假日信息不得不维护到数据库,所以复杂度立马提升了N个档次。

由此,这里提供一些思路仅供参考。

准备工作

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

json 复制代码
{
  "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个工作日日期。

      sql 复制代码
      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语句如下:

sql 复制代码
create table t_date(
	f_year int2,
	data BYTEA
);

主要逻辑

由于下面代码注释很全面,这里就不写处理逻辑了。需要说明的是这里用到了Java提供的BitSet类。

这个类和其他数组一样,索引也是从0开始的

java 复制代码
/**
 * 填充数据。
 * @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数据,并将其打印:

java 复制代码
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转换为二进制就可以:

java 复制代码
byte[] byteArray = bitSet.toByteArray();

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

计算工作日

加载到JVM缓存中

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

计算工作日

java 复制代码
/**
 * 计算指定日期的工作日
 * @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个字节也可以忽略不计,由于它计算工作日算是最简单的,也不失为采纳的思路。

思考

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

相关推荐
夜泉_ly1 小时前
MySQL -安装与初识
数据库·mysql
qq_529835352 小时前
对计算机中缓存的理解和使用Redis作为缓存
数据库·redis·缓存
月光水岸New4 小时前
Ubuntu 中建的mysql数据库使用Navicat for MySQL连接不上
数据库·mysql·ubuntu
狄加山6755 小时前
数据库基础1
数据库
我爱松子鱼5 小时前
mysql之规则优化器RBO
数据库·mysql
chengooooooo5 小时前
苍穹外卖day8 地址上传 用户下单 订单支付
java·服务器·数据库
Rverdoser6 小时前
【SQL】多表查询案例
数据库·sql
Galeoto6 小时前
how to export a table in sqlite, and import into another
数据库·sqlite
人间打气筒(Ada)6 小时前
MySQL主从架构
服务器·数据库·mysql
leegong231116 小时前
学习PostgreSQL专家认证
数据库·学习·postgresql