報表查詢、統計分析常需「本週」、「上月」等動態日期範圍。本文提供可重用的 PHP 函式庫,支援週/月範圍計算、任意偏移、時區處理與資料庫查詢整合。
<?php
class DateRange {
private DateTimeZone $timezone;
public function __construct(string $timezone = 'Asia/Taipei') {
$this->timezone = new DateTimeZone($timezone);
}
/**
* 取得指定日期所在週的範圍(週一至週日)
* @param DateTime|string $date 基準日期
* @return array ['start' => DateTime, 'end' => DateTime]
*/
public function getWeekRange(DateTime|string $date): array {
$dt = $this->normalizeDate($date);
// ISO-8601: 週一=1, 週日=7
$dayOfWeek = (int)$dt->format('N');
$start = clone $dt;
$start->modify("-" . ($dayOfWeek - 1) . " days");
$start->setTime(0, 0, 0);
$end = clone $start;
$end->modify('+6 days');
$end->setTime(23, 59, 59);
return ['start' => $start, 'end' => $end];
}
/**
* 取得指定日期所在月的範圍
*/
public function getMonthRange(DateTime|string $date): array {
$dt = $this->normalizeDate($date);
$start = clone $dt;
$start->modify('first day of this month');
$start->setTime(0, 0, 0);
$end = clone $dt;
$end->modify('last day of this month');
$end->setTime(23, 59, 59);
return ['start' => $start, 'end' => $end];
}
/**
* 取得常用預設範圍
* @param int $type 1=昨天 2=本週 3=上週 4=上上週 5=本月 6=上月 7=本年 8=上年
*/
function getPresetRange(int $type): array {
$now = new DateTime('now', $this->timezone);
return match ($type) {
1 => $this->getDayRange($now->modify('-1 day')),
2 => $this->getWeekRange($now),
3 => $this->getWeekRange($now->modify('-1 week')),
4 => $this->getWeekRange($now->modify('-2 weeks')),
5 => $this->getMonthRange($now),
6 => $this->getMonthRange($now->modify('-1 month')),
7 => $this->getYearRange($now),
8 => $this->getYearRange($now->modify('-1 year')),
default => throw new InvalidArgumentException("Invalid preset type: $type")
};
}
private function getDayRange(DateTime $date): array {
$start = clone $date;
$start->setTime(0, 0, 0);
$end = clone $date;
$end->setTime(23, 59, 59);
return ['start' => $start, 'end' => $end];
}
private function getYearRange(DateTime $date): array {
$start = clone $date;
$start->modify('first day of January');
$start->setTime(0, 0, 0);
$end = clone $date;
$end->modify('last day of December');
$end->setTime(23, 59, 59);
return ['start' => $start, 'end' => $end];
}
private function normalizeDate(DateTime|string $date): DateTime {
if ($date instanceof DateTime) {
return clone $date;
}
return new DateTime($date, $this->timezone);
}
}
?>
<?php
$range = new DateRange('Asia/Taipei');
// 本週訂單統計
$week = $range->getPresetRange(2); // 本週
$stmt = $pdo->prepare("
SELECT COUNT(*) as cnt, SUM(amount) as total
FROM orders
WHERE created_at BETWEEN :start AND :end
");
$stmt->execute([
'start' => $week['start']->format('Y-m-d H:i:s'),
'end' => $week['end']->format('Y-m-d H:i:s')
]);
$stats = $stmt->fetch();
// 上月會員註冊趨勢(每日)
$month = $range->getPresetRange(6); // 上月
$stmt = $pdo->prepare("
SELECT DATE(created_at) as day, COUNT(*) as cnt
FROM users
WHERE created_at BETWEEN :start AND :end
GROUP BY DATE(created_at)
ORDER BY day
");
$stmt->execute([
'start' => $month['start']->format('Y-m-d H:i:s'),
'end' => $month['end']->format('Y-m-d H:i:s')
]);
$daily = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
?>
<?php
function buildDateRangeWhere(string $column, array $range, string $alias = ''): array {
$prefix = $alias ? "$alias." : "";
$where = "{$prefix}$column BETWEEN :{$alias}start AND :{$alias}end";
$params = [
"{$alias}start" => $range['start']->format('Y-m-d H:i:s'),
"{$alias}end" => $range['end']->format('Y-m-d H:i:s')
];
return [$where, $params];
}
// 使用
[$where, $params] = buildDateRangeWhere('created_at', $weekRange, 'o');
// $where: "o.created_at BETWEEN :ostart AND :oend"
// $params: ['ostart' => '2023-12-18 00:00:00', 'oend' => '2023-12-24 23:59:59']
?>
<input type="text" id="date-range" placeholder="選擇日期範圍">
<script>
flatpickr('#date-range', {
mode: 'range',
dateFormat: 'Y-m-d',
locale: 'zh_tw',
onChange: function(selectedDates) {
if (selectedDates.length === 2) {
const [start, end] = selectedDates;
// 送往後端 API
fetch('/api/report', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
start: start.toISOString().split('T')[0],
end: end.toISOString().split('T')[0]
})
});
}
}
});
</script>
<?php
// API 端點
$data = json_decode(file_get_contents('php://input'), true);
$range = new DateRange();
$start = $data['start'] ?? null;
$end = $data['end'] ?? null;
if ($start && $end) {
$startDt = (new DateTime($start, $range->timezone))->setTime(0, 0, 0);
$endDt = (new DateTime($end, $range->timezone))->setTime(23, 59, 59);
$range = ['start' => $startDt, 'end' => $endDt];
} else {
// 預設本月
$range = $range->getPresetRange(5);
}
// 執行查詢...
?>
<?php
class FiscalDateRange extends DateRange {
private int $fiscalYearStartMonth; // 1-12
public function __construct(string $timezone, int $fiscalYearStartMonth = 4) {
parent::__construct($timezone);
$this->fiscalYearStartMonth = $fiscalYearStartMonth;
}
public function getFiscalYearRange(DateTime|string $date): array {
$dt = $this->normalizeDate($date);
$year = (int)$dt->format('Y');
$month = (int)$dt->format('n');
$fiscalYear = $month >= $this->fiscalYearStartMonth ? $year : $year - 1;
$start = new DateTime("{$fiscalYear}-{$this->fiscalYearStartMonth}-01", $this->timezone);
$start->setTime(0, 0, 0);
$end = clone $start;
$end->modify('+1 year -1 second'); // 下一年第一天前一秒
return ['start' => $start, 'end' => $end];
}
public function getFiscalQuarterRange(DateTime|string $date): array {
$dt = $this->normalizeDate($date);
$month = (int)$dt->format('n');
$year = (int)$dt->format('Y');
// 計算財務季度 (Q1 從 fiscalYearStartMonth 開始)
$offset = ($month - $this->fiscalYearStartMonth + 12) % 12;
$quarter = (int)($offset / 3) + 1;
$quarterStartMonth = (($quarter - 1) * 3 + $this->fiscalYearStartMonth - 1) % 12 + 1;
$quarterYear = $quarterStartMonth > $month ? $year - 1 : $year;
$start = new DateTime("{$quarterYear}-{$quarterStartMonth}-01", $this->timezone);
$start->setTime(0, 0, 0);
$end = clone $start;
$end->modify('+3 months -1 second');
return ['start' => $start, 'end' => $end];
}
}
?>
<?php
// tests/DateRangeTest.php
use PHPUnit\Framework\TestCase;
class DateRangeTest extends TestCase {
private DateRange $range;
protected function setUp(): void {
$this->range = new DateRange('UTC');
}
public function testWeekRange(): void {
// 2023-12-25 是週一
$result = $this->range->getWeekRange('2023-12-25');
$this->assertEquals('2023-12-25 00:00:00', $result['start']->format('Y-m-d H:i:s'));
$this->assertEquals('2023-12-31 23:59:59', $result['end']->format('Y-m-d H:i:s'));
}
public function testMonthRange(): void {
$result = $this->range->getMonthRange('2023-02-15');
$this->assertEquals('2023-02-01 00:00:00', $result['start']->format('Y-m-d H:i:s'));
$this->assertEquals('2023-02-28 23:59:59', $result['end']->format('Y-m-d H:i:s'));
}
public function testLeapYearFebruary(): void {
$result = $this->range->getMonthRange('2024-02-15');
$this->assertEquals('2024-02-29 23:59:59', $result['end']->format('Y-m-d H:i:s'));
}
}
?>
這組工具函式解決了日期範圍計算的常見痛點:時區一致性、週起始日定義、月份長度差異、閏年處理。建議專案統一引用此類別,避免分散在各處的 `strtotime`、`date` 組合邏輯導致不一致錯誤。