如何把周数转换为日期范围——完整指南

有人给你发来排期:“第 23 周交付(W23)。”你打开日历盯着看:这到底对应哪些日期?

周数在项目计划、制造业、零售和物流中无处不在——但大多数人只把它当作标签。把周数反推回具体的日历日期并没有那么直观,而且跨年边界的情况会让连经验丰富的开发者都踩坑。

本指南涵盖完整换算:从周数得到周一,从周数得到完整日期范围(周一–周日),以及反向(日期 → 周数)。并提供 Python、JavaScript、Excel 和 SQL 的示例代码。

周数到底告诉了你什么

在开始计算之前,你必须先知道这个周数来自哪一种“周编号系统”。

ISO 8601(国际标准): 第 1 周是包含当年“第一个星期四”的那一周。周从周一到周日。一年有 52 或 53 个 ISO 周。关键点:ISO 的“周年份(week year)”可能与公历年份不同。12 月最后几天有时属于下一年的第 1 周。

美式(US-style): 第 1 周包含 1 月 1 日。周可能是周日到周六,或周一到周日(取决于工具/地区设置)。1 月 1 日永远在第 1 周,没有例外。

本指南后续提到的“周数”,除非特别说明,默认指 ISO 8601。如果别人给你的周数来自美式工具(例如 Excel WEEKNUM 的默认模式),换算公式会不同——见下文相关章节。

你还必须知道年份。 只有“第 23 周”是没有意义的。务必把周数当成一对 (year, week)。2025 年第 23 周从 6 月 2 日开始;2026 年第 23 周从 6 月 1 日开始。2026 年第 1 周甚至从 2025 年 12 月 29 日开始。

核心公式:ISO 周数 → 周一

每个 ISO 周都从周一开始。要找到年份 Y 的第 W 周的周一:

Monday = Jan 4 of Y + (W - 1) × 7 days - weekday(Jan 4 of Y) + 1

思路是:1 月 4 日一定在 ISO 第 1 周(按定义,它落在一年最初 4 天内,保证那一周包含星期四)。先找到包含 1 月 4 日那一周的周一,然后向前推进 (W - 1) 周。

一个更清晰的等价写法:

Monday of W1 = Jan 4 of Y - weekday(Jan 4 of Y) + 1
Monday of W  = Monday of W1 + (W - 1) × 7

其中 weekday 返回周一=1 到周日=7(ISO 的 weekday 编号)。

示例:2026 年第 23 周

  • 2026 年 1 月 4 日是周日。ISO weekday = 7。
  • 第 1 周的周一:1 月 4 日 − 7 + 1 = 2025 年 12 月 29 日。
  • 第 23 周的周一:2025-12-29 + 22 × 7 = 2025-12-29 + 154 天 = 2026 年 6 月 1 日。
  • 第 23 周的周日:6 月 1 日 + 6 = 2026 年 6 月 7 日。

2026 年第 23 周范围是 2026-06-01(周一)到 2026-06-07(周日)。

跨年边界陷阱

最常见的错误:以为年份 Y 的第 1 周一定从年份 Y 的某一天开始。

并不是。

第 1 周是包含当年第一个星期四的那一周。如果那个星期四在 1 月初,那么第 1 周的周一可能落在上一年的 12 月。

示例:

ISO 周周一周日
W120252024 年 12 月 30 日2025 年 1 月 5 日
W120262025 年 12 月 29 日2026 年 1 月 4 日
W120272027 年 1 月 4 日2027 年 1 月 10 日
W120282028 年 1 月 3 日2028 年 1 月 9 日

2025 年第 1 周从 2024 年 12 月开始。2026 年第 1 周从 2025 年 12 月开始。

这意味着:

  • 2025 年 12 月 29–31 日属于 2026 年的 ISO 第 1 周,而不是 2025 年。
  • 如果有人说“在 2026-W1 之前交付”,截止周其实是 2025-12-29 开始的那一周。

反向陷阱也存在:某些年份的最后几天会落入下一年的第 1 周,而不是本年的最后一周。给某一周打标签时,务必使用 ISO 周年份(Python 中的 %G,PostgreSQL 中的 isoyear),而不是公历年份。

任意周数的完整日期范围

一旦得到周一,本周其余日期就很简单:

星期相对周一的偏移
周一+0
周二+1
周三+2
周四+3
周五+4
周六+5
周日+6

第 W 周的日期范围是:[周一, 周一 + 6 天]

如果只关心工作日(周一–周五):[周一, 周一 + 4 天]

Python

from datetime import date, timedelta

def iso_week_to_monday(year: int, week: int) -> date:
    # Jan 4 is always in ISO week 1
    jan4 = date(year, 1, 4)
    # Move back to Monday of that week
    week1_monday = jan4 - timedelta(days=jan4.weekday())
    # Step forward to the target week
    return week1_monday + timedelta(weeks=week - 1)

def iso_week_to_range(year: int, week: int) -> tuple[date, date]:
    monday = iso_week_to_monday(year, week)
    sunday = monday + timedelta(days=6)
    return monday, sunday

# Examples
monday, sunday = iso_week_to_range(2026, 23)
print(monday)   # 2026-06-01
print(sunday)   # 2026-06-07

monday, sunday = iso_week_to_range(2026, 1)
print(monday)   # 2025-12-29  ← note: starts in 2025
print(sunday)   # 2026-01-04

Python 也内置了反向转换(日期 → ISO 周):

d = date(2026, 6, 4)
iso_year, iso_week, iso_weekday = d.isocalendar()
print(iso_year, iso_week)  # 2026 23

当你需要 ISO 年份时,使用 d.isocalendar().year(不要用 d.year)——在跨年边界它们会不同。

Python 3.8+ 还可以用 fromisocalendar 反向获取:

# Get Monday of ISO week 23, 2026 directly
monday = date.fromisocalendar(2026, 23, 1)  # weekday 1 = Monday
print(monday)  # 2026-06-01

如果你使用 Python 3.8 或更高版本,date.fromisocalendar(year, week, weekday) 是最干净的方案。

JavaScript

JavaScript 没有原生 ISO 周支持。可以手写实现:

function isoWeekToMonday(year, week) {
  // Jan 4 is always in ISO week 1
  const jan4 = new Date(year, 0, 4)
  const dayOfWeek = jan4.getDay() || 7  // convert Sun=0 to 7
  const week1Monday = new Date(jan4)
  week1Monday.setDate(jan4.getDate() - dayOfWeek + 1)
  
  const monday = new Date(week1Monday)
  monday.setDate(week1Monday.getDate() + (week - 1) * 7)
  return monday
}

function isoWeekToRange(year, week) {
  const monday = isoWeekToMonday(year, week)
  const sunday = new Date(monday)
  sunday.setDate(monday.getDate() + 6)
  return { monday, sunday }
}

// Examples
const { monday, sunday } = isoWeekToRange(2026, 23)
console.log(monday.toISOString().slice(0, 10))  // 2026-06-01
console.log(sunday.toISOString().slice(0, 10))  // 2026-06-07

const w1 = isoWeekToRange(2026, 1)
console.log(w1.monday.toISOString().slice(0, 10))  // 2025-12-29

使用 date-fns

import { setISOWeek, setISOWeekYear, startOfISOWeek, endOfISOWeek } from 'date-fns'

function isoWeekToRange(year, week) {
  let d = new Date(year, 0, 4)  // any date in the target year
  d = setISOWeekYear(d, year)
  d = setISOWeek(d, week)
  return {
    monday: startOfISOWeek(d),
    sunday: endOfISOWeek(d)
  }
}

date-fns 能正确处理所有边界情况,在生产环境更推荐。

Excel 和 Google Sheets

Excel 没有直接的“周数 → 日期”函数,但可以用公式拼出来。

年份 Y 的 ISO 第 W 周的周一:

=DATE(Y,1,4) - WEEKDAY(DATE(Y,1,4),2) + 1 + (W-1)*7

拆解一下:

  • DATE(Y,1,4) — 当年 1 月 4 日
  • WEEKDAY(DATE(Y,1,4),2) — 星期几,其中周一=1,周日=7
  • 相减得到第 1 周的周一
  • 加上 (W-1)*7 移动到目标周

要得到完整范围,周日就是:

=Monday_formula + 6

表格示例(A1=年份,B1=周数):

周一: =DATE(A1,1,4) - WEEKDAY(DATE(A1,1,4),2) + 1 + (B1-1)*7
周日: =Monday_cell + 6

把单元格格式设置为日期。对于 2026 年第 1 周,这个公式会返回 2025 年 12 月 29 日作为周一——这是正确的。

反向(日期 → ISO 周数): 使用 ISOWEEKNUM(date)。ISO 周年份在 Excel 里没有原生函数,需要用 ISOWEEKNUM 相关的 workaround 来计算。

SQL

PostgreSQL 的 ISO 支持最完整:

-- Monday of ISO week W in year Y
SELECT
  make_date(2026, 1, 4)
    + (23 - 1) * 7  -- step to week 23
    - EXTRACT(isodow FROM make_date(2026, 1, 4))::int + 1
    AS week_monday;
-- 2026-06-01

-- Or more cleanly using generate_series for a range lookup
SELECT
  d::date AS week_monday,
  (d + 6)::date AS week_sunday,
  EXTRACT(isoyear FROM d) AS iso_year,
  EXTRACT(week FROM d) AS iso_week
FROM generate_series(
  DATE '2026-01-01',
  DATE '2026-12-31',
  INTERVAL '7 days'
) d
WHERE EXTRACT(week FROM d) = 23
  AND EXTRACT(isoyear FROM d) = 2026;

更简洁的方法是使用 PostgreSQL 的 ISO 格式解析:

-- Convert ISO year + week to Monday
SELECT to_date('2026' || '23' || '1', 'IYYYIWid') AS monday;
-- 2026-06-01

-- Format: IYYY=ISO year, IW=ISO week, id=ISO day (1=Monday)

这是最简单的 PostgreSQL 写法:构造 YYYYWWD 字符串并用 ISO 格式码解析。

MySQL:

-- Monday of ISO week 23, 2026
SELECT STR_TO_DATE('202623 Monday', '%X%V %W');
-- 2026-06-01

-- Or step-based:
SELECT DATE_ADD(
  DATE_ADD(STR_TO_DATE('2026-01-04', '%Y-%m-%d'),
    INTERVAL (-(WEEKDAY(STR_TO_DATE('2026-01-04', '%Y-%m-%d')))) DAY),
  INTERVAL (23 - 1) * 7 DAY
) AS week_monday;

SQL Server:

-- Monday of ISO week 23, 2026
DECLARE @year INT = 2026, @week INT = 23

SELECT DATEADD(
  DAY,
  (@week - 1) * 7
    - (DATEPART(WEEKDAY, DATEFROMPARTS(@year, 1, 4)) + 5) % 7,
  DATEFROMPARTS(@year, 1, 4)
) AS week_monday;
-- 2026-06-01

美式(US-style)周数 → 日期

如果你的周数来自美式系统(第 1 周包含 1 月 1 日,周从周日或周一开始),公式更简单,因为没有跨年边界问题:

周起始日(以周日为一周开始):

Week_start = DATE(Y, 1, 1) + (W - 1) × 7 - WEEKDAY(DATE(Y, 1, 1), 1)

其中 WEEKDAY 返回周日=1 到周六=7。

不过在排期和物流里,美式周数远不如 ISO 常见。如果有人只给你周数却不说明系统,尤其在国际/欧洲业务语境中,默认按 ISO 理解通常更安全。

2026 快速参考:ISO 第 1–10 周与第 48–53 周

ISO 周周一周日
W120262025-12-292026-01-04
W220262026-01-052026-01-11
W320262026-01-122026-01-18
W420262026-01-192026-01-25
W520262026-01-262026-02-01
W620262026-02-022026-02-08
W720262026-02-092026-02-15
W820262026-02-162026-02-22
W920262026-02-232026-03-01
W1020262026-03-022026-03-08
...
W4820262026-11-232026-11-29
W4920262026-11-302026-12-06
W5020262026-12-072026-12-13
W5120262026-12-142026-12-20
W5220262026-12-212026-12-27
W5320262026-12-282027-01-03

注意:2026 年第 1 周从 2025-12-29 开始。注意:2026 年第 53 周到 2027-01-03 结束——2026 年是一个 53 周的年份。

常见错误汇总

不带年份。 “第 8 周”是含糊的。总是用 2026-W08 或等效格式。

用公历年份而不是 ISO 周年份。 2025-12-29 属于 2026 年 ISO 第 1 周,而不是 2025 年。把它存到 2025 的桶里会错分。

以为第 1 周从 1 月 1 日开始。 很多年份第 1 周从 12 月 29/30/31 开始。如果你需要 1 月 1 日永远属于第 1 周,那你用的是美式系统,不是 ISO。

周一公式 off-by-one。 Jan 4 这个锚点很关键——不是 1 月 1 日(它可能属于上一年的最后一周)。用 1 月 1 日做锚点在某些年份会错。

不测试跨年日期。 任何日期处理代码都应使用 12 月 28–31 和 1 月 1–4 进行测试。这几天集中了几乎所有边界情况。

使用 ISO Week Number Calculator 查询任意周数对应的日期范围,或查看 current week number 与本周的 ISO 日期范围。