在Go语言开发中,处理时间相关的计算是常见需求,其中根据ISO年份和周数计算对应周一零点时间戳的场景十分普遍,比如周维度的数据统计、按周划分的业务周期计算等。ISO周数体系和普通公历周数存在规则差异,需要遵循特定的计算逻辑才能得到正确结果。

ISO年份和周数的核心规则
ISO 8601标准对周数的定义有明确规范,和常规认知的周数有两点核心区别:
- 每年的第一周是包含该年第一个星期四的那一周,也就是说这一周至少有4天属于新的一年
- 每周的起始日是周一,周一为一周的第一天,周日为一周的最后一天
基于这个规则,可能出现某一年的12月最后几天属于下一年的第一周,或者1月前几天属于上一年的最后一周的情况,计算时需要特别注意边界场景。
计算思路拆解
要实现根据ISO年份和周数计算周一零点时间戳,核心思路可以分为三步:
- 先找到指定ISO年份的1月1日对应的时间对象
- 根据ISO第一周的规则,定位到该年第一周的周一日期
- 在第一周周一的基础上,加上(目标周数-1)*7天,得到目标周的周一日期,再取当天的零点时间戳
Go语言实现代码
下面是完整的Go语言实现代码,包含边界场景的处理:
package main
import (
"fmt"
"time"
)
// CalcMondayTimestampByISOWeek 根据ISO年份和周数计算周一零点时间戳(秒级)
// year: ISO年份,week: ISO周数,返回对应周一的0点0分0秒的Unix时间戳,错误返回非nil
func CalcMondayTimestampByISOWeek(year int, week int) (int64, error) {
// 周数校验,ISO周数范围是1-53
if week < 1 || week > 53 {
return 0, fmt.Errorf("invalid ISO week number: %d, should be 1-53", week)
}
// 1. 获取指定年份的1月1日
janFirst := time.Date(year, time.January, 1, 0, 0, 0, 0, time.UTC)
// 2. 计算该年第一周的周一
// 先获取1月1日是周几,time.Weekday中Monday是1,Sunday是0
firstDayWeekday := janFirst.Weekday()
var offset int
if firstDayWeekday <= time.Thursday {
// 如果1月1日在周一到周四之间,第一周的周一就是1月1日往前推(firstDayWeekday - time.Monday)天
offset = int(time.Monday - firstDayWeekday)
} else {
// 如果1月1日在周五到周日之间,第一周的周一是1月1日往后推(7 - (firstDayWeekday - time.Monday))天
offset = int(7 - (firstDayWeekday - time.Monday))
}
firstWeekMonday := janFirst.AddDate(0, 0, offset)
// 3. 计算目标周的周一
targetMonday := firstWeekMonday.AddDate(0, 0, (week-1)*7)
// 4. 取目标周一的零点时间戳,确保是当天0点0分0秒
targetMondayZero := time.Date(targetMonday.Year(), targetMonday.Month(), targetMonday.Day(), 0, 0, 0, 0, targetMonday.Location())
// 校验计算出的周一是否属于指定的ISO年份,避免周数超出范围的情况
if targetMondayZero.ISOWeek() != (year, week) {
return 0, fmt.Errorf("week %d does not exist in ISO year %d", week, year)
}
return targetMondayZero.Unix(), nil
}
func main() {
// 测试场景1:2024年第1周
ts1, err := CalcMondayTimestampByISOWeek(2024, 1)
if err != nil {
fmt.Println("error:", err)
} else {
t := time.Unix(ts1, 0).UTC()
fmt.Printf("2024年第1周周一零点时间:%v,时间戳:%dn", t, ts1)
}
// 测试场景2:2023年第52周
ts2, err := CalcMondayTimestampByISOWeek(2023, 52)
if err != nil {
fmt.Println("error:", err)
} else {
t := time.Unix(ts2, 0).UTC()
fmt.Printf("2023年第52周周一零点时间:%v,时间戳:%dn", t, ts2)
}
// 测试场景3:错误周数
ts3, err := CalcMondayTimestampByISOWeek(2024, 54)
if err != nil {
fmt.Println("error:", err)
} else {
fmt.Println(ts3)
}
}
代码说明
上述代码中,time.ISOWeek()方法可以直接返回时间对应的ISO年份和周数,我们用来做最终的校验,确保计算结果的周数确实属于目标ISO年份。时区方面默认使用UTC,如果需要本地时区的时间戳,可以将time.UTC替换为time.Local,或者根据业务需求指定对应的时区。
需要注意如果传入的周数不存在于目标ISO年份中,比如2024年只有52周,传入53就会返回错误,避免计算出错误的时间戳。