$ 数据分析中的年中周数界定

数据分析中常常会按照日期维度分析的需求,日期维度包括年(年份)、季(季度)、月(月份)、周(即年中周数)、日(年月日)等等。其中周是从日期转换而来的,通常需求是输入一个日期,输出对应的年份和年中周数。其他维度都比较容易界定,可"周"这个维度比较难以界定,其涉及了多方面的因素,包括:

  1. 以星期几作为周的起始?通常国外习惯用星期日,国内习惯用星期一。

  2. 第一周如何界定?是将元旦(1月1日)所在周定为第一周,还是将第一个至少四天的周为第一周?

  3. 周是否是连续的、完整的?即年与年交界处的周如何划分?

ISO 8601是国际标准化组织的日期和时间的表示方法,规定了星期一为周的起始,对第一周的界定采用的是第一个至少四天的周为第一周,并且规定周是连续的、完整的,年与年交界处的周要么属于前一年最后一周,要么属于当年第一周,也就不会出现第0周的情况。

为什么会有第一个至少四天的周这样的规定?因为一周有七天,一周的一多半就是至少四天。还有一个巧合,1970年1月1日通常被视为计算机中的起始日,那一天恰好是星期四,也就是1970年1月1日所在周恰好是1970年第一周。

在Java编程语言中,JDK提供了符合ISO 8601标准的API查询:

LocalDate dt = LocalDate.of(1970, 1, 1);
dt.get(WeekFields.ISO.weekBasedYear()); // 年份
dt.get(WeekFields.ISO.weekOfWeekBasedYear()); // 年中周数

ISO.weekOfWeekBasedYear()已经提供了年中周数的值,为什么还需要一个ISO.weekBasedYear()API来获取年份呢?因为存在当年前几天被划入前一年的最后一周(比如1971年1月1日被归为1970年第53周),或者当年最后几天被划入下一年第一周的情况(比如1969年12月31日被归为1970年第1周),这样基于周的年份和日期的原始年份就不一样了。

StarrocksApache Doris中的date_format()函数可以计算日期对应的年中周数:

select
 date_format(date'2009-01-01','%x-W%v')/* 星期四 */,
 date_format(date'2009-01-04','%x-W%v')/* 星期日 */,
 date_format(date'2009-01-05','%x-W%v')/* 星期一 */, 
 date_format(date'2010-01-01','%x-W%v')/* 星期五 */;

select
 date_format(date'2001-01-01', '%X-%V'),
 date_format(date'2002-01-01', '%X-%V'),
 date_format(date'2003-01-01', '%X-%V'),
 date_format(date'2003-01-04','%X-W%V'),
 date_format(date'2003-01-05','%X-W%V'), 
 date_format(date'2009-01-01', '%X-%V'),
 date_format(date'2010-01-01', '%X-%V'),
 date_format(date'2011-01-01', '%X-%V'),
 date_format(date'2017-01-01', '%X-%V');

从这几个日期的例子中可以看出来'%x-W%v'格式对应的周设置是符合ISO8601的。而'%X-%V'格式对应的周设置是星期日作为周起始,以第一个星期日作为第一周,周是连续的、完整的。

业务上的需求往往并没有这么简单而统一,也就是说业务上需要周相关三个因素都能够自定义,也就是:

  1. 支持自定义周起始为星期一至星期日中的任意一天。
  2. 第一周可以是1月1日所在周,也可以是第一个至少四天的周。
  3. 周可以是连续的,也可以是不连续的。

这样原生的date_format()函数肯定无法满足,需要自己开发UDF来实现。并且业务需求不止是一个函数,是一系列函数,包括:

函数 参数 返回值
日期d转为对应的年份y和周数w Config, d y, w
获取年份y和周数w内第n天(0<=n<=6)对应的日期d Config, y, w, n d
某年y起始周序数w Config,y w
某年y结尾周序数w Config,y w
某年y总周数n Config,y n

UDF开发完成之后,如何验证UDF的算法是否正确呢?日期那么多,肯定不是一天天计算和校验,一年有平年和闰年,一周有星期一到星期日,组合起来一共有2 * 7 种日期,这里给出一些代表日期:

日期 星期 是否闰年
2001-01-01 星期一 平年
2002-01-01 星期二 平年
2003-01-01 星期三 平年
2009-01-01 星期四 平年
2010-01-01 星期五 平年
2011-01-01 星期六 平年
2017-01-01 星期日 平年
1940-01-01 星期一 闰年
1952-01-01 星期二 闰年
1964-01-01 星期三 闰年
1976-01-01 星期四 闰年
1988-01-01 星期五 闰年
2000-01-01 星期六 闰年
2012-01-01 星期日 闰年

再乘上是否连续的两种情况,一共28种组合。这些代表日期可以覆盖所有测试情况。

其中有意思的是最大周数,按照起始日不同所有周分布情况如下:

是否闰年 第一周天数 中间周数 最后一周天数
平年 7天 51周 1天
平年 6天 51周 2天
平年 5天 51周 3天
平年 4天 51周 4天
平年 3天 51周 5天
平年 2天 51周 6天
平年 1天 52周 0天
闰年 7天 51周 2天
闰年 6天 51周 3天
闰年 5天 51周 4天
闰年 4天 51周 5天
闰年 3天 51周 6天
闰年 2天 52周 0天
闰年 1天 52周 1天

可以看出如果不连续,闰年,首周只有1天,最后一周只有1天,那么最多会出现第54周的情况。

$ 参考

https://en.wikipedia.org/wiki/ISO_8601

https://en.wikipedia.org/wiki/ISO_week_date#First_wee

更新时间: 3/23/2025, 3:12:58 PM