$ 数据分析中的年中周数界定
数据分析中常常会按照日期维度分析的需求,日期维度包括年(年份)、季(季度)、月(月份)、周(即年中周数)、日(年月日)等等。其中周是从日期转换而来的,通常需求是输入一个日期,输出对应的年份和年中周数。其他维度都比较容易界定,可"周"这个维度比较难以界定,其涉及了多方面的因素,包括:
以星期几作为周的起始?通常国外习惯用星期日,国内习惯用星期一。
第一周如何界定?是将元旦(1月1日)所在周定为第一周,还是将第一个至少四天的周为第一周?
周是否是连续的、完整的?即年与年交界处的周如何划分?
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周),这样基于周的年份和日期的原始年份就不一样了。
Starrocks
和Apache 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月1日所在周,也可以是第一个至少四天的周。
- 周可以是连续的,也可以是不连续的。
这样原生的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