基于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个字节也可以忽略不计,由于它计算工作日算是最简单的,也不失为采纳的思路。

思考

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

相关推荐
Hsu_kk2 分钟前
MySQL 批量删除海量数据的几种方法
数据库·mysql
编程学无止境2 分钟前
第02章 MySQL环境搭建
数据库·mysql
knight-n13 分钟前
MYSQL库的操作
数据库·mysql
包饭厅咸鱼1 小时前
QML----复制指定下标的ListModel数据
开发语言·数据库
生命几十年3万天1 小时前
redis时间优化
数据库·redis·缓存
Elastic 中国社区官方博客1 小时前
释放专利力量:Patently 如何利用向量搜索和 NLP 简化协作
大数据·数据库·人工智能·elasticsearch·搜索引擎·自然语言处理
力姆泰克1 小时前
看电动缸是如何提高农机的自动化水平
大数据·运维·服务器·数据库·人工智能·自动化·1024程序员节
力姆泰克1 小时前
力姆泰克电动缸助力农业机械装备,提高农机的自动化水平
大数据·服务器·数据库·人工智能·1024程序员节
Lionhacker1 小时前
网络工程师这个行业可以一直干到退休吗?
网络·数据库·网络安全·黑客·黑客技术
eternal__day2 小时前
MySQL_聚合函数&分组查询
数据库·mysql