在周報系統、考勤統計、財務結算等業務場景中,常需根據年份和週數快速獲取該週的起始日期(週一)和結束日期(週日)。本文提供高性能、無依賴的 PHP 實現,並解決跨年週邊界問題。
<?php
/**
* 獲取指定年份所有週的起止日期
* @param int $year 年份 (如 2024)
* @return array [週號 => [開始日期(Y-m-d), 結束日期(Y-m-d)], ...]
*/
function getYearWeeks(int $year): array {
// 1. 確定該年第一週的週一日期
// ISO-8601: 第一週是包含該年第一個週四的週
// 等價:1月4日所在的週
$jan4 = new DateTime("$year-01-04");
$jan4->modify('monday this week'); // 回溯到本週週一
$firstMonday = clone $jan4;
// 2. 確定該年最後一週的週日日期
// 12月28日必在最後一週
$dec28 = new DateTime("$year-12-28");
$dec28->modify('sunday this week'); // 前進到本週週日
$lastSunday = clone $dec28;
// 3. 計算總週數
$totalWeeks = (int)$firstMonday->format('W'); // 第一週週號
$lastWeekNum = (int)$lastSunday->format('W'); // 最後一週週號
// 處理跨年:如果最後一週屬於下一年(週號 01),實際為 52 或 53 週
if ($lastWeekNum == 1) {
$totalWeeks = 52 + (int)$firstMonday->format('L'); // 閏年可能 53 週
} else {
$totalWeeks = $lastWeekNum;
}
// 4. 批量生成所有週
$weeks = [];
$currentMonday = clone $firstMonday;
for ($week = 1; $week <= $totalWeeks; $week++) {
$start = $currentMonday->format('Y-m-d');
$end = (clone $currentMonday)->modify('+6 days')->format('Y-m-d');
$weeks[$week] = [$start, $end];
// 下週週一
$currentMonday->modify('+7 days');
}
return $weeks;
}
/**
* 獲取指定年份、指定週的起止日期(單週查詢優化版)
* @param int $year
* @param int $week (1-53)
* @return array|false [開始日期, 結束日期] 或 false
*/
function getWeekRange(int $year, int $week): array|false {
if ($week < 1 || $week > 53) return false;
// 利用 ISO 週日期格式直接構造
// "2024-W18-1" 表示 2024 年第 18 週的週一
$monday = DateTime::createFromFormat('Y-\WW-N', sprintf('%04d-W%02d-1', $year, $week));
if (!$monday) return false;
$sunday = clone $monday;
$sunday->modify('+6 days');
// 驗證週號是否匹配(處理跨年邊界)
if ((int)$monday->format('W') !== $week) {
return false;
}
return [$monday->format('Y-m-d'), $sunday->format('Y-m-d')];
}
<?php
// 獲取 2024 年所有週
$weeks2024 = getYearWeeks(2024);
echo "2024 年共 " . count($weeks2024) . " 週\n";
// 查看第 1 週、第 18 週、最後一週
print_r($weeks2024[1]); // 第 1 週
print_r($weeks2024[18]); // 第 18 週
print_r($weeks2024[count($weeks2024)]); // 最後一週
// 輸出示例:
// 2024 年共 52 週
// Array ( [0] => 2024-01-01 [1] => 2024-01-07 )
// Array ( [0] => 2024-04-29 [1] => 2024-05-05 )
// Array ( [0] => 2024-12-23 [1] => 2024-12-29 )
// 單週查詢
var_dump(getWeekRange(2024, 18));
// array(2) { [0]=> string(10) "2024-04-29" [1]=> string(10) "2024-05-05" }
function formatWeekRange(array $range, string $format = 'Y.m.d'): string {
$start = DateTime::createFromFormat('Y-m-d', $range[0]);
$end = DateTime::createFromFormat('Y-m-d', $range[1]);
return $start->format($format) . ' - ' . $end->format($format);
}
// 使用
echo formatWeekRange($weeks2024[18]); // 2024.04.29 - 2024.05.05
<?php
$iterations = 100000;
// DateTime 面向對象版
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
getWeekRange(2024, 18);
}
echo "DateTime 版: " . (microtime(true) - $start) * 1000 . " ms\n";
// strtotime 過程式版(參考原文邏輯優化)
function getWeekRangeStrtotime(int $year, int $week): array|false {
$firstDay = strtotime("$year-01-01");
$firstMonday = strtotime('monday this week', $firstDay);
// 修正:如果 1月1日 是週五六日,firstMonday 會落在下一年第一週
if ((int)date('W', $firstMonday) != 1) {
$firstMonday = strtotime('next monday', $firstDay);
}
$targetMonday = strtotime("+".($week-1)." weeks", $firstMonday);
if ((int)date('W', $targetMonday) !== $week) return false;
$targetSunday = strtotime("+6 days", $targetMonday);
return [date('Y-m-d', $targetMonday), date('Y-m-d', $targetSunday)];
}
$start = microtime(true);
for ($i = 0; $i < $iterations; $i++) {
getWeekRangeStrtotime(2024, 18);
}
echo "strtotime 版: " . (microtime(true) - $start) * 1000 . " ms\n";
// 典型輸出(PHP 8.2):
// DateTime 版: 185.3 ms
// strtotime 版: 92.7 ms
| 年份 | 總週數 | 第 1 週範圍 | 最後一週範圍 | 說明 |
|---|---|---|---|---|
| 2023 | 52 | 2023-01-02 ~ 2023-01-08 | 2023-12-25 ~ 2023-12-31 | 常規年 |
| 2024 | 52 | 2024-01-01 ~ 2024-01-07 | 2024-12-23 ~ 2024-12-29 | 閏年,1/1 是週一 |
| 2025 | 52 | 2024-12-30 ~ 2025-01-05 | 2025-12-29 ~ 2026-01-04 | 跨年週明顯 |
| 2026 | 53 | 2026-01-05 ~ 2026-01-11 | 2026-12-28 ~ 2027-01-03 | 53 週年 |
year_week CHAR(7) 字段(如 2024-W18),便於分組統計/api/weeks?year=2024 接口,前端下拉選擇週數function getMonthWeeks(int $year, int $month): array {
$firstDay = new DateTime("$year-$month-01");
$lastDay = clone $firstDay;
$lastDay->modify('last day of this month');
$weeks = [];
$current = clone $firstDay;
$current->modify('monday this week');
while ($current <= $lastDay) {
$weekStart = clone $current;
$weekEnd = clone $current;
$weekEnd->modify('+6 days');
// 僅保留與目標月份有交集的部分
$overlapStart = max($weekStart, $firstDay);
$overlapEnd = min($weekEnd, $lastDay);
if ($overlapStart <= $overlapEnd) {
$weeks[] = [
'week_num' => (int)$current->format('W'),
'start' => $overlapStart->format('Y-m-d'),
'end' => $overlapEnd->format('Y-m-d'),
'is_cross_month' => $weekStart < $firstDay || $weekEnd > $lastDay,
];
}
$current->modify('+7 days');
}
return $weeks;
}
掌握週日期計算的核心在於理解 ISO-8601 週定義規則,善用 PHP DateTime 類的 modify() 相對時間功能,並妥善處理跨年邊界條件。建議將核心邏輯封裝為工具類,配合單元測試覆蓋邊界年份(2000、2026、2100 等)。