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

ASP.NET 日期時間運算實用技巧:取得本週/本月首尾日、季度邊界與相對日期計算

     編輯:ASP.NET 基礎

核心 DateTime 擴充方法庫

日期邊界計算是報表、統計、排程系統的基礎需求。以下提供生產級擴充方法,涵蓋週、月、季、年等所有常見邊界,並處理跨年、閏年等邊界條件。

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()含時分秒精確到毫秒

實際應用範例

1. 報表查詢區間建構

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))
        };
    }
}

2. SQL 參數化查詢

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)

3. 行事曆/排程顯示

// 產生某月所有週的起迄日 (用於月曆網格)
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/29AddYears(1) 導致 2/29 變 3/1使用 AddMonthsKeepDay 保持月底邏輯

效能與測試建議

  • 無配置、無鎖定:純運算方法,極高效能,適合高頻呼叫
  • 單元測試覆蓋邊界:1/1、12/31、2/28、2/29、跨年週、夏令切換日
  • 快取計算結果:同一 Request 多次使用同一基準日期時,可用 HttpContext.Items 快取

總結

將日期邊界計算封裝為擴充方法,不僅提升程式碼可讀性,更將易錯的邊界邏輯集中管理、測試。建議建立專案共用的 DateTimeExtensions.cs,配合 ReportPeriod 列舉與 ReportQueryBuilder 類別,形成標準化的日期區間處理體系,消除專案中散落的重複日期計算邏輯。

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