首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP 日期運算:取得本月、上月、下月首尾日期的最佳實踐

     編輯:PHP 相關知識

前言

報表統計、帳單週期、排程任務常需「本月第一天/最後一天」、「上月範圍」、「下月範圍」。PHP DateTime 類別配合 modify() 相對時間格式,能以零數學運算、自動處理閏年/月份天數差異的方式優雅解決。本文提供可直接使用的工具函數與最佳實踐。

核心工具函數

<?php
/**
 * 取得指定月份的首尾日期 (DateTimeImmutable 物件)
 *
 * @param string|DateTimeInterface $base 基準日期,預設 today
 * @param int $month_offset 月份偏移:0=本月, -1=上月, +1=下月, ...
 * @return array ['start' => DateTimeImmutable, 'end' => DateTimeImmutable]
 */
function getMonthRange($base = 'today', int $month_offset = 0): array {
    $dt = $base instanceof DateTimeInterface
        ? DateTimeImmutable::createFromInterface($base)
        : new DateTimeImmutable($base);

    // 1. 移動到目標月份的第一天 00:00:00
    $start = $dt->modify("first day of {$month_offset} month")
                ->setTime(0, 0, 0);

    // 2. 最後一天 23:59:59
    $end = $start->modify('last day of this month')
                 ->setTime(23, 59, 59);

    return ['start' => $start, 'end' => $end];
}

/**
 * 格式化輸出常用格式
 */
function formatRange(array $range, string $format = 'Y-m-d'): array {
    return [
        'start' => $range['start']->format($format),
        'end'   => $range['end']->format($format),
    ];
}

// === 使用範例 ===
// 本月
$now = new DateTimeImmutable();
echo "本月: " . formatRange(getMonthRange($now, 0))['start'] . " ~ " . formatRange(getMonthRange($now, 0))['end'] . "\n";
// 2024-01-01 ~ 2024-01-31

// 上月
echo "上月: " . formatRange(getMonthRange($now, -1))['start'] . " ~ " . formatRange(getMonthRange($now, -1))['end'] . "\n";
// 2023-12-01 ~ 2023-12-31

// 下月
echo "下月: " . formatRange(getMonthRange($now, +1))['start'] . " ~ " . formatRange(getMonthRange($now, +1))['end'] . "\n";
// 2024-02-01 ~ 2024-02-29 (閏年自動處理)

// 指定日期為基準 (如帳單日)
$billing = new DateTimeImmutable('2024-03-15');
echo "帳單月 (3/15 基準): " . formatRange(getMonthRange($billing, 0))['start'] . " ~ " . formatRange(getMonthRange($billing, 0))['end'] . "\n";
// 2024-03-01 ~ 2024-03-31
?>

相對時間格式關鍵字速查

進階:取得月份天數、季度、年度範圍

<?php
function getMonthDays($base = 'today', int $offset = 0): int {
    $range = getMonthRange($base, $offset);
    return (int)$range['start']->diff($range['end'])->format('%a') + 1;
}

function getQuarterRange($base = 'today', int $quarter_offset = 0): array {
    $dt = $base instanceof DateTimeInterface
        ? DateTimeImmutable::createFromInterface($base)
        : new DateTimeImmutable($base);

    $month = (int)$dt->format('n');
    $quarter = (int)ceil($month / 3) + $quarter_offset;
    $year = (int)$dt->format('Y');

    // 處理季度跨年
    while ($quarter < 1) { $quarter += 4; $year--; }
    while ($quarter > 4) { $quarter -= 4; $year++; }

    $start_month = ($quarter - 1) * 3 + 1;
    $start = new DateTimeImmutable("{$year}-{$start_month}-01 00:00:00");
    $end = $start->modify('last day of +2 months')->setTime(23, 59, 59);

    return ['start' => $start, 'end' => $end];
}

function getYearRange($base = 'today', int $year_offset = 0): array {
    $dt = $base instanceof DateTimeInterface
        ? DateTimeImmutable::createFromInterface($base)
        : new DateTimeImmutable($base);
    $year = (int)$dt->format('Y') + $year_offset;
    $start = new DateTimeImmutable("{$year}-01-01 00:00:00");
    $end = new DateTimeImmutable("{$year}-12-31 23:59:59");
    return ['start' => $start, 'end' => $end];
}
?>

DateTime vs strtotime() 為何選 DateTimeImmutable

格式字串含義範例
first day of this month本月 1 號 00:002024-01-01 00:00:00
last day of this month本月最後一天 23:59:592024-01-31 23:59:59
first day of -1 month上月 1 號2023-12-01 00:00:00
first day of +1 month下月 1 號2024-02-01 00:00:00
first day of +3 months三個月後的月份第一天
last day of -1 month上月最後一天
midnight / today當天 00:00:00
yesterday / tomorrow前一天/後一天 00:00
特性strtotime() + date()DateTimeImmutable
型別安全字串/整數,易出錯強型別物件,IDE 提示
不可變性無修改返回新物件,避免副作用
時區處理依賴 date_default_timezone_set物件內建時區,可單獨設定
閏年/月份邊界手動計算易錯內建邏輯自動正確
格式化date('Y-m-d', $ts)$dt->format('Y-m-d')
運算strtotime('+1 month', $ts)$dt->modify('+1 month')

常見陷阱與避坑

  1. strtotime('+1 month', '2024-01-31') → 2024-03-03:因 2 月無 31 號向後溢位。解決:用 first day of +1 month。
  2. 修改原物件:DateTime::modify() 會改變原物件。解決:用 DateTimeImmutable 或 clone。
  3. 時區不一致:伺服器時區 UTC,業務時區 Asia/Taipei。解決:建立物件時指定時區 new DateTimeImmutable('now', new DateTimeZone('Asia/Taipei'))。
  4. 格式化輸出給 SQL:MySQL DATETIME 需 Y-m-d H:i:s,DATE 需 Y-m-d。

資料庫查詢範例 (PDO)

<?php
$range = getMonthRange('today', -1); // 上月
$sql = "SELECT * FROM orders
        WHERE created_at >= :start AND created_at <= :end";
$stmt = $pdo->prepare($sql);
$stmt->execute([
    'start' => $range['start']->format('Y-m-d H:i:s'),
    'end'   => $range['end']->format('Y-m-d H:i:s'),
]);
$orders = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>

總結

使用 DateTimeImmutable 搭配 first day of ... month / last day of ... month 相對格式,即可零 Bug、零數學、自動處理閏年地取得任意月份首尾日期。封裝為 getMonthRange() 等工具函數,專案中統一呼叫,徹底消除日期邊界 Bug。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved