文章目录
今天UI 组件组内小伙伴需要在日期组件中实现显示周数的需求,周的开始起点可以是周一,或者周日。这里自然而然会碰到一个问题,就是上一年度的12 月月底与下一年度的 1 月份会出现在同一周里。那是算上一年的周数还是下一年的第一周呢?于是比对了业内UI 组件库设计的比较好的 ant-design
ant-design中英文环境下不同点
在对比了业内使用比较广的 ant-design UI。 发现如下所示,他们在中文的语言环境下。2009年 1 月 1 日 属于 2008 年的 53 周。
中文环境
中文环境下每周的起点是周一,2009 年 12 月 28 日-2010 年 1 月 3 日 是 2009 年的 第53 周。
英文环境
从图中可以看到英文环境下是 2008 年 12 月 28 日到 2009 年 1 月 3 日 属于2009的第一周,此外一周的起点是周日。
chagpt o1 的回答
这里面有个问题,就是时间的上关于第几周的计算,究竟有没有标准。是否有国际标准,是否每个国家的情况并不一样。通过 chatgpt o1 模型查阅情况如下。
不同的标准
ISO 8601
下面是 ios8601 对每年第一个星期的定义:
使用年份和星期表示某一日期的格式形如YYYYWwwD或YYYY-Www-D,YYYY表示年份,其值与年月日格式中的年份略有差别;Www表示该日期所属星期是今年的第几个星期,范围在W01到W53之间;D表示该日是本星期的第几天,范围在1到7之间,每个星期以周一作为第一天。例如1926年8月17日可写成1926-W33-2或1926W332。
每年的第一个星期可以用如下方法决定: 1,本年度第一个星期四所在的星期; 2,1月4日所在的星期; 3,本年度第一个至少有4天在同一星期内的星期; 4,星期一在去年12月29日至今年1月4日以内的星期;
推理可得,如果1月1日是星期一、星期二、星期三或者星期四,它所在星期就是本年第一个星期;如果1月1日是星期五、星期六或星期日,则它所在星期就是上一年第52或者53个日历星期;12月28日总是在一年最后一个星期。
GB/T 7408.1-2023
我国最新的标准是 是GB/T 7408.1-2023 。相关信息如下
总结
ISO 8601 与 GB/T 7408.1-2023 一致,以周一为起点。如果1月1日是星期一、星期二、星期三或者星期四,它所在星期就是本年第一个星期;如果1月1日是星期五、星期六或星期日,则它所在星期就是上一年第52或者53个日历星期
ANSI INCITS 30-1997 (R2008) 和 NIST FIPS PUB 4-2 标准
仍以 周日 作为一周的开端。
• 只要 1 月 1 日在某周内,即视该周为新的一年第一周。
• 由于这符合当时美国绝大部分政府单位及民用日历习惯,所以在联邦机构数据处理上使用时不会造成太大混淆。
所以 ant-design 在切换为英语的时候,采用的是美国传统用法。美国在很多度量单位保留非国际单位制(非 SI 制)的使用习惯。如长度与距离,重量,体积,温度等度量单位方面。
这个里面有个细节,当我们编写 与 date 相关UI 组件时,需要考虑不同语种与习惯,并不是所有国家都是按照国际标准进行定义的。
相关代码实现
以 ISO 8601
/**
* 根据 GB/T 7408.1-2023 / ISO 8601 计算指定日期是该年的第几周
* (每年的第一周可通过以下方法等效决定:
* 1) 本年度第一个星期四所在的星期;
* 2) 1月4日所在的星期;
* 3) 本年度第一个至少有4天在同一星期内的星期;
* 4) 星期一在去年12月29日至今年1月4日以内的星期。)
*
* 支持以下三种输入格式:
* 1. Date 对象
* 2. "YYYYMMDD"(纯数字 8 位)
* 3. "YYYY-MM-DD"(带短横线分隔)
*
* 若传入的参数不符合上述任何一种格式,则会抛出异常。
*
* @param {Date | string} input - 要计算的日期对象或可解析的字符串
* @returns {number} - 该日期是当年的第几周(ISO 8601 / GB/T 7408.1-2023)
*/
function getISOWeekNumber(input) {
// 1. 解析输入,得到有效的 Date 对象
const dateObj = parseToDate(input);
// 2. 转换为基于 UTC 的日期,以免时区差异影响计算
const tempDate = new Date(Date.UTC(
dateObj.getFullYear(),
dateObj.getMonth(),
dateObj.getDate()
));
// 3. ISO 8601 规定的技巧:将当前日期平移到同一周的“星期四”
// getUTCDay() 返回 0(周日)~6(周六),若为0改为7(周日视为第7天)
const dayNum = tempDate.getUTCDay() || 7;
tempDate.setUTCDate(tempDate.getUTCDate() + 4 - dayNum);
// 4. 计算该周所属年份的年初(UTC)
const yearStart = new Date(Date.UTC(tempDate.getUTCFullYear(), 0, 1));
// 5. 计算从年初到平移后日期的天数
const days = Math.floor((tempDate - yearStart) / 86400000) + 1;
// 6. 周次 = ceil(天数 / 7)
return Math.ceil(days / 7);
}
/**
* 辅助函数:根据输入格式将其解析为 Date 对象
* 支持以下三种情况:
* 1. 原生 Date 对象:确保它是有效日期
* 2. "YYYYMMDD":纯数字 8 位
* 3. "YYYY-MM-DD":中间带短横线分隔
*
* 其他格式或无效日期会抛出异常
*
* @param {Date | string} input
* @returns {Date}
*/
function parseToDate(input) {
// 情况 1:本身是 Date 对象
if (input instanceof Date) {
if (isNaN(input.getTime())) {
throw new Error("Invalid Date object");
}
return input;
}
// 情况 2 & 3:字符串,需要匹配正则
if (typeof input === 'string') {
// 匹配纯 8 位数字格式 "YYYYMMDD"
const matchPure = input.match(/^(\d{4})(\d{2})(\d{2})$/);
// 匹配带短横线的 "YYYY-MM-DD"
const matchHyphen = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
if (matchPure) {
const [ , y, m, d ] = matchPure;
const year = parseInt(y, 10);
const month = parseInt(m, 10) - 1; // JS 中月份从 0 开始
const day = parseInt(d, 10);
const date = new Date(year, month, day);
if (isNaN(date.getTime())) {
throw new Error("Invalid date from 'YYYYMMDD'");
}
return date;
} else if (matchHyphen) {
const [ , y, m, d ] = matchHyphen;
const year = parseInt(y, 10);
const month = parseInt(m, 10) - 1;
const day = parseInt(d, 10);
const date = new Date(year, month, day);
if (isNaN(date.getTime())) {
throw new Error("Invalid date from 'YYYY-MM-DD'");
}
return date;
} else {
throw new Error("Invalid date string format");
}
}
// 其他情况一律抛出异常
throw new Error("Unsupported date type or format");
}
// -------------------- 测试示例 --------------------
try {
// 示例 1:传入 Date 对象
const dateObj = new Date(2010, 0, 1); // 2010-01-01
console.log("Test1 (Date obj):", getISOWeekNumber(dateObj)); // 期望 53
// 示例 2:传入 "YYYYMMDD"
console.log("Test2 (YYYYMMDD):", getISOWeekNumber("20100101")); // 期望 53
// 示例 3:传入 "YYYY-MM-DD"
console.log("Test3 (YYYY-MM-DD):", getISOWeekNumber("2010-01-01")); // 期望 53
// 示例 4:不合法输入(应抛出异常)
console.log("Test4:", getISOWeekNumber("2010/01/01"));
} catch (err) {
console.error("Error:", err.message);
}
以周日进行计算
/**
* 以周日为起始日,计算指定日期是该年的第几周
* (只要在包含 1 月 1 日的周里,就算作第 1 周)
* @param {Date} date - 要计算的日期对象
* @returns {number} - 该日期是当年的第几周
*/
function getSundayStartWeekNumber(date) {
// 当年的第一天
const startOfYear = new Date(date.getFullYear(), 0, 1);
// 获取这一天是周几(0=周日,1=周一,...,6=周六)
const dayOfWeek = startOfYear.getDay();
// 若第一天不是周日,则需要找出当年的第一个“周日”
const offset = dayOfWeek > 0 ? dayOfWeek : 0;
const firstSunday = new Date(startOfYear.getFullYear(), 0, 1 - offset);
// 计算差值(毫秒),再转为天数
const diff = date - firstSunday;
const dayCount = Math.floor(diff / (1000 * 60 * 60 * 24));
// 每 7 天为一周
return Math.floor(dayCount / 7) + 1;
}
// 测试:2010 年 1 月 1 日
const testDate2 = new Date(2010, 0, 1);
console.log("周日开头 周次:", getSundayStartWeekNumber(testDate2));
发表评论