如果你讨厌一个人,就让它写一个日期范围差值算法吧
如果你讨厌一个人,就让它写一个日期范围差值算法吧
引
在我负责的应用中,有个功能点是是通过选择的日期范围计算出这个范围的日期的差值(计算时包含最后一天),比如:日期范围为2020-01-01 ~ 2021-02-28,则该日期范围的日期差值是1年2个月。这个功能点初始是其他同事做的,他的计算逻辑是使用getTime
语法获取到两个日期的时间戳,拿到相差的毫秒数,一天有86400000毫秒,然后除以这个常量,就拿到净天数了,到这一步都没有问题,然而在获取年,月这两个单位的值是,因为每月天数有[28, 29, 30, 31]天,每年天数有[365, 366]天,因为这个问题,他取了一个平均数,定义每月有30天,每年有365.5天。这样的做法,必然会导致计算后的结果是一个约数,并没有精确计算。
果然,在项目进入测试阶段,测试人员在验证这个功能时,不能接受这个算法得出的结果。然后在npm仓库中找可以使用的功能模块,在测试了好几个package之后,选定了包datetime-difference,经验证,大多数能得到正确的结果。就这样项目上线了。然而在最近,测试又打开了这个功能的bug,datetime-difference在计算2019-12-01 ~ 2020-05-31时,按人类的思维,正确的结果应该是6个月整,然而得出的结果是6个月1天。天呢,又出问题了。这个差值计算也太麻烦了吧!没办法,又去npm仓库看能否找到一个完美计算日期范围差值的功能模块。结果是消极的,尝试了搜索结果首页的大部分模块,要么是计算得到的数据结构不能用,要么就是某些日期区间差值计算错误。遂下定决心自己写一个这样的功能模块。
正
算法思路如下:
- 对传入的两个日期对象进行排序。如果传入的日期前大后小,则交换其值,保证第一个参数是开始日期,第二个参数是结束日期,也就是第一个参数的日期一定不大于第一个参数的日期
- 以年月日的顺序开始对开始日期逐个进行累加操作,并将相应的结果进行累加操作
- 步骤2增加后的开始日期和第二个参数的结束日期相比较,如果比较结果还是不大于,则继续进行步骤2,否则将结果进行减一操作,并移动下一个年月日的顺序进行步骤2的操作。
- 在进行日单位的累加操作后,必然会使得开始日期和结束日期相同,这个时候拿到的结果就是最终的结果。
思路没有问题,开始码代码吧。开发好代码后,修改了一些小bug,然后用确定的日期进行验证,似乎没有什么问题,然后涉及到闰年的2月29号,以及平年的2月28号,以及包含这两个特殊日期的日期范围时,都会出现问题。举个例子:2020-01-31 ~ 2020-02-28应该是一个月,而2020-01-31 ~ 2021-02-28却是1年1月1天,我只增加了一年,为什么得到的结果会多出一天啊。
我分析了当前的代码实现,发现在对开始日期的年的2月最后一天进行操作时,没有处理人类预期的结果,说到这个,有个问题需要说一下,执行如下的代码:
const dt = new Date('2020-02-29')
dt.setFullYear(dt.getFullYear() + 1)
dt.toLocaleString() // 2021-03-01
const dt2 = new Date('2020-01-31')
dt2.setMonth(dt2.getMonth() + 1)
dt2.toLocaleString() // 2020/3/2
这个输出结果很多人没有想到吧。是的,我还要hack语言本身的某些异常结果,比较高兴的是setDate
的语法是没有任何问题的。
最重要的一个点是在操作月单位时,某些情况,人类的思维也无法得出确切的结果,比如:在2020年中,1月1号的下一个月是2月1号,1月31号的下一个月是2月29号,1月30号的下一个月是2月28号,然而1月有31天,2月有29天,这样必然会导致1月的天数不能和2月的天数进行一一对应。
那么,请问1月15号的下一个月从前向后计算应该是2月15号,然后从后往前计算则是2月13号。
这就导致在操作月单位出现不确定的结果,这样的情况不止出现在1月的下一月,还有2月的下一月。编程到这一步已经进行不下去了,因为人类自己也没有厘定这种情况下的确切结果,编程更不能了。
我突然想到一个网站:wolfram alpha,看下这种情况它是怎么处理的。有如下结果:
编号 | 输入表达式 | 计算结果 |
---|---|---|
1 | 2019/2/28 + 1month | 2020-03-28 |
2 | 2020/1/27 + 1month | 2020-02-27 |
3 | 2020/1/28 + 1month | 2020-02-28 |
4 | 2020/1/29 + 1month | 2020-02-29 |
5 | 2020/1/30 + 1month | 2020-02-29 |
6 | 2020/1/31 + 1month | 2020-02-29 |
7 | 2020/2/29 + 1month | 2020-03-29 |
8 | 2020/3/31 + 1month | 2020-04-30 |
9 | 2020/4/1 + 1month | 2020-05-01 |
从上面的表格中,我们不难发现wolfram网站的计算月份加法的逻辑是:从前往后计算,不管从后往前计算的可能性,然后确定下一个月的最大天数,如果计算得到的结果超过了下一个月的最大天数进入了下下月,则直接取下一个月的最后一天。 所以编号4,5,6的测试的结果都是同一个值。
这个计算逻辑对我们很有启发性。最后我们的月份累加,累减运算的逻辑如下:如果是月份的最后一天,则结果是下一个月的最后一天,其他情况都是直接让月份累加或者累减。
为了处理年,月单位的累加,累减操作,我创建了一个类HumanDate
,封装了方法addOneYear
, addOneMonth
, minusOneYear
, minusOneMonth
来处理相应的运算细节。
最后,项目githu地址为:daterange-difference,大家如果对具体的实现细节感兴趣,可以看相应的源码。
欢迎大家star。该项目也发布到npm仓库中,大家可以很方便引入到开发的项目中。
P.S. 在发布项目到npm仓库时,没有采用wolfram网站的月份累加的计算逻辑。后续看是否要采用它的计算逻辑吧,现在就先这样了。