日期邊界計算是報表、統計、排程系統的基礎需求。以下提供生產級擴充方法,涵蓋週、月、季、年等所有常見邊界,並處理跨年、閏年等邊界條件。
public static class DateTimeExtensions
{
#region 本週邊界
/// 本週一 00:00:00 (以週一為週首)
public static DateTime StartOfWeek(this DateTime dt, DayOfWeek startOfWeek = DayOfWeek.Monday)
{
int diff = (7 + (dt.DayOfWeek - startOfWeek)) % 7;
return dt.Date.AddDays(-diff);
}
/// 本週日 23:59:59.999
public static DateTime EndOfWeek(this DateTime dt, DayOfWeek startOfWeek = DayOfWeek.Monday)
{
return dt.StartOfWeek(startOfWeek).AddDays(7).AddTicks(-1);
}
#endregion
#region 本月邊界
/// 本月 1 號 00:00:00
public static DateTime StartOfMonth(this DateTime dt)
{
return new DateTime(dt.Year, dt.Month, 1);
}
/// 本月最後一天 23:59:59.999
public static DateTime EndOfMonth(this DateTime dt)
{
return dt.StartOfMonth().AddMonths(1).AddTicks(-1);
}
#endregion
#region 本季邊界
/// 本季第一天 00:00:00
public static DateTime StartOfQuarter(this DateTime dt)
{
int quarter = (dt.Month - 1) / 3 + 1;
return new DateTime(dt.Year, (quarter - 1) * 3 + 1, 1);
}
/// 本季最後一天 23:59:59.999
public static DateTime EndOfQuarter(this DateTime dt)
{
return dt.StartOfQuarter().AddMonths(3).AddTicks(-1);
}
#endregion
#region 本年邊界
public static DateTime StartOfYear(this DateTime dt) => new DateTime(dt.Year, 1, 1);
public static DateTime EndOfYear(this DateTime dt) => new DateTime(dt.Year, 12, 31, 23, 59, 59, 999);
#endregion
#region 相對日期
/// 取得 N 個月前/後的同一天 (自動處理月底)
public static DateTime AddMonthsKeepDay(this DateTime dt, int months)
{
DateTime result = dt.AddMonths(months);
// 若原日期為月底,結果也應為目標月月底
if (dt.Day == DateTime.DaysInMonth(dt.Year, dt.Month))
result = new DateTime(result.Year, result.Month, DateTime.DaysInMonth(result.Year, result.Month));
return result;
}
/// 取得指定週的第 N 天 (0=週一...6=週日)
public static DateTime GetWeekDay(this DateTime dt, int dayOfWeek, DayOfWeek startOfWeek = DayOfWeek.Monday)
{
return dt.StartOfWeek(startOfWeek).AddDays(dayOfWeek);
}
#endregion
}
| 需求 | 原始寫法 | 擴充方法 | 優勢 |
|---|---|---|---|
| 本月第一天 | dt.AddDays(-dt.Day + 1) | dt.StartOfMonth() | 語意清晰、不可變、可測試 |
| 本月最後一天 | dt.AddMonths(1).AddDays(-dt.Day) | dt.EndOfMonth() | 避免跨月邏輯錯誤 |
| 本週第一天 | dt.AddDays(-(int)dt.DayOfWeek + 1) | dt.StartOfWeek() | 支援自訂週首 (週日/週一) |
| 本週最後一天 | dt.AddDays(7 - (int)dt.DayOfWeek) | dt.EndOfWeek() | 含時分秒精確到毫秒 |
public class ReportQueryBuilder
{
public (DateTime Start, DateTime End) GetDateRange(ReportPeriod period, DateTime? baseDate = null)
{
var dt = baseDate ?? DateTime.Today;
return period switch
{
ReportPeriod.Today => (dt.Date, dt.Date.AddDays(1).AddTicks(-1)),
ReportPeriod.ThisWeek => (dt.StartOfWeek(), dt.EndOfWeek()),
ReportPeriod.LastWeek => (dt.AddDays(-7).StartOfWeek(), dt.AddDays(-7).EndOfWeek()),
ReportPeriod.ThisMonth => (dt.StartOfMonth(), dt.EndOfMonth()),
ReportPeriod.LastMonth => (dt.AddMonths(-1).StartOfMonth(), dt.AddMonths(-1).EndOfMonth()),
ReportPeriod.ThisQuarter => (dt.StartOfQuarter(), dt.EndOfQuarter()),
ReportPeriod.ThisYear => (dt.StartOfYear(), dt.EndOfYear()),
_ => throw new ArgumentOutOfRangeException(nameof(period))
};
}
}
var (start, end) = new ReportQueryBuilder().GetDateRange(ReportPeriod.ThisMonth);
string sql = @"SELECT * FROM Orders
WHERE CreateTime >= @Start AND CreateTime < @End";
var params = new { Start = start, End = end.AddTicks(1) }; // 使用半開區間 [start, end)
// 產生某月所有週的起迄日 (用於月曆網格)
public IEnumerable<(DateTime WeekStart, DateTime WeekEnd)> GetMonthWeeks(int year, int month)
{
var firstDay = new DateTime(year, month, 1);
var lastDay = firstDay.AddMonths(1).AddDays(-1);
var current = firstDay.StartOfWeek();
while (current <= lastDay)
{
yield return (current, current.EndOfWeek());
current = current.AddDays(7);
}
}
| 陷阱 | 錯誤示範 | 正確做法 |
|---|---|---|
| 跨年週計算 | 12/31 算作下一年的第 1 週 | 使用 ISO 8601 週編號:ISOWeek.GetYear(dt) |
| 時區問題 | 伺服器 UTC vs 使用者本地時間 | 統一儲存 UTC,顯示時轉換:TimeZoneInfo.ConvertTime |
| 夏令時間 | AddDays(1) 在 DST 切換日可能非 24 小時 | 使用 DateTimeOffset 或僅操作 .Date 屬性 |
| 閏年 2/29 | AddYears(1) 導致 2/29 變 3/1 | 使用 AddMonthsKeepDay 保持月底邏輯 |
HttpContext.Items 快取將日期邊界計算封裝為擴充方法,不僅提升程式碼可讀性,更將易錯的邊界邏輯集中管理、測試。建議建立專案共用的 DateTimeExtensions.cs,配合 ReportPeriod 列舉與 ReportQueryBuilder 類別,形成標準化的日期區間處理體系,消除專案中散落的重複日期計算邏輯。