偶尔会碰到生成一组日期序列的需求。比如,要按天展示用户最近5天的数据,如果某天没数据,则这一天数据显示零。
如果此时,你从数据库中查出的数据是这样的:
2021-03-18 102021-03-17 152021-03-15 202021-03-14 10
复制
发现少了 2021-03-16 这天的数据。所以,你可能会首先生成从 2021-03-14 到 2021-03-18 这个日期的日期数组,然后遍历该数组对上面数据进行补漏。
本文要谈的,是怎么生成这个日期数组,以及从中能发现什么技巧。
Java 版本的日期序列生成
通常的经验中,同样的功能用 Java 写明显要繁琐很多,但这次却不一样,一行代码就能完成:
List<LocalDate> dates = Stream // 从当前时间开始,不停地生成前一天的数据 .iterate(LocalDate.now(), b -> b.minusDays(1)) // 只取最近5天的 .limit(5) // 从远到近排序 .sorted() .collect(Collectors.toList());
复制
得易于 Java 8 引入的重新设计的日期类,Java 日期处理从来没像今天这么简单。

但本文的重点是怎么用 JavaScript 来写同样的功能。
JavaScript 的版本
早期的 JavaScript ,很多 API 设计都是参考自 Java 语言,这就导致 JavaScript 中的 Date 和 Java 中的一样难用,以致于 JS 生态中出现了层出不穷的第三方日期库。
不过这里,我们要尝试使用原生 JavaScript 来生成日期序列,而这主要用到的就是 Date 对象的 setDate() 方法,它用来设置 年月日 中的 日 (即,我们常说的几号)。
假设今天是 2021-03-18,如果要生成最近 5 天的日期数据,你可能会尝试这样写:
// 给定日期,因为月份是从 0 开始,所以要减 1const now = new Date(2021, 3 - 1, 18);// 日期序列const dates = [];// 当前日期,即 18const date = now.getDate();// 生成几天的数据let n = 5;while (n--) { now.setDate(date - n); dates.push(format(now));}console.log(dates);
复制
运行上面代码输出如下:

好像没有错 😆 !
但如果把当前日期从 2021-03-18 换为 2021-03-02 就会发现有蹊跷:

生成的数据全乱了!
原因是 setDate() 这个方法,它和你初看产生的理解略有差别。
setDate() 用法
假设当前日期是 2021-03-02,对于 setDate(n):
如果 n > 0,则有2种情况 如果 n 小于当月最大天数,则日期设置到当月的第 n 天。即 setDate(4) 是当月 4 号,setDate(31) 是当月 31 号。 如果 n 大于当月最大天数,则月份和日期都往前推。即 setDate(32) 是下月 1 号(4月1号)。 如果 n = 0,则月份变为上一月,日期变为上一月的最后一天。即 setDate(0) 是上月 28 号 (2月28号)。 如果 n < 0,则月份先变为上一月,日期从上月最一天往后推 n 天(如果 n+1 超过了上一月的最大天数,则月份也会往前推)。即setDate(-1) 是上月 27 号 (2月27号,上月最后一天往前推一天),而 setDate(-28) 则是1月31号(因为2月就28天)。
上述种种,如果月份推到尽头(1月或12月),则年份也会相应转变。
有了以上基础,就能理解为什么上面的日期会错乱了,因为我们没有考虑到 n <= 0 的情况。
调整后的代码如下:
/** * 生成给定日期之前的 n 个日期序列 * * @param d 给定的日期 * @param n 生成几个之前的日期 */function* dates(d, n = 1) { // 生成日期数量小于1时,直接返回 if (n < 1) { return; } // 创建一个日期参数的拷贝,因为我们不想直接修改入参 const cp = new Date(d); const day = d.getDate(); // 第一个日期的前一天 cp.setDate(day - n); while (n--) { // 每次循环,都将日期设为下一天 cp.setDate(cp.getDate() + 1); yield format(cp); }}
复制
重要的部分都给了注释,这次是把它封装成了一个函数,因为我们要复用它。
如果调用这个函数:
console.log([...dates(new Date(), 5)]);
复制
会得到如下结果:

没有问题,尝试把当前日期设为 2021-03-02 :
console.log([...dates(new Date(2021,3 - 1,2), 5)]);
复制

也没有问题!
解疑:为什么上面的函数要用 generator 来写?
如果你总是要生成 给定数量 的日期序列,直接将函数中的日期入在数组中返回也可。但试想这样一种场景:你想生成一组日期序列,在生成过程中如果达到某种条件时,就提前终止。这时使用 generator 可以达到更好的效果(相到于懒生成,而不是一次性生成所有日期):
var myDates = [];for (const date of dates(new Date(2021,3 - 1,2), 5)) { myDates.push(date); // 模拟某种提前结束的条件 if (Math.random() < 0.5) { break; }}console.log(myDates);
复制
某次运行的结果如下:

可以想见,当 n 的值非常大时,这种懒生成的方式能达到更好的效果。
附:上面的代码中的 format 函数如下:
/** * 将日期对象转换为 yyyy-MM-dd 格式的字符串 */function format(d) {
return [
d.getFullYear(),
String(d.getMonth() + 1).padStart(2, 0),
d.getDate()
].join('-');
}复制
谢谢阅读!
- END -