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

C# DateTime 時間加減運算與比較實用指南

     編輯:關於C#

DateTime 提供豐富的時間運算方法,支援加減年/月/日/時/分/秒/毫秒/微秒,並能直接比較大小。本文系統整理常用操作、投票系統間隔檢查實例、時區陷阱與最佳實踐。

核心加減方法

DateTime now = DateTime.Now;

// 加減年/月/日 (會處理月份天數差異、閏年)
DateTime nextYear = now.AddYears(1);
DateTime nextMonth = now.AddMonths(3);
DateTime nextWeek = now.AddDays(7);
DateTime tomorrow = now.AddDays(1);
DateTime yesterday = now.AddDays(-1);

// 加減時/分/秒/毫秒/微秒
DateTime oneHourLater = now.AddHours(1);
DateTime thirtyMinLater = now.AddMinutes(30);
DateTime oneMinAgo = now.AddMinutes(-1);
DateTime oneSecondLater = now.AddSeconds(1);
DateTime oneMsLater = now.AddMilliseconds(1);
DateTime oneTickLater = now.AddTicks(1); // 100 奈秒

// 通用 TimeSpan 加減
TimeSpan duration = TimeSpan.FromHours(2.5); // 2 小時 30 分
DateTime later = now + duration;
DateTime earlier = now - duration;

// 靜態工廠方法
DateTime specific = new DateTime(2024, 12, 25, 10, 30, 0);
DateTime fromParts = DateTime.Parse("2024-12-25 10:30:00");

比較運算

DateTime dt1 = new DateTime(2024, 1, 1);
DateTime dt2 = new DateTime(2024, 12, 31);

// 直接比較 (運算子多載)
bool isBefore = dt1 < dt2;    // true
bool isAfter = dt1 > dt2;     // false
bool isEqual = dt1 == dt2;    // false
bool isSameOrBefore = dt1 <= dt2; // true

// CompareTo 方法
int cmp = dt1.CompareTo(dt2); // -1 (dt1 < dt2), 0 (相等), 1 (dt1 > dt2)

// 靜態比較
int cmpStatic = DateTime.Compare(dt1, dt2); // 同上

// 日期部分比較 (忽略時間)
bool sameDay = dt1.Date == dt2.Date;
bool sameDay2 = dt1.Year == dt2.Year && dt1.Month == dt2.Month && dt1.Day == dt2.Day;

// TimeSpan 差值
TimeSpan diff = dt2 - dt1; // 相減得 TimeSpan
double totalDays = diff.TotalDays;
double totalHours = diff.TotalHours;
int days = diff.Days; // 整數天數

實用範例:投票系統間隔檢查

public class VoteThrottle
{
    private readonly TimeSpan _minInterval;

    public VoteThrottle(TimeSpan minInterval)
    {
        _minInterval = minInterval;
    }

    public bool CanVote(DateTime lastVoteTime, out TimeSpan remaining)
    {
        DateTime now = DateTime.UtcNow; // 建議使用 UTC 避免時區問題
        DateTime nextAllowed = lastVoteTime + _minInterval;
        
        if (now >= nextAllowed)
        {
            remaining = TimeSpan.Zero;
            return true;
        }
        
        remaining = nextAllowed - now;
        return false;
    }

    // 簡化版:僅回傳布林
    public bool CanVoteSimple(DateTime lastVoteTime)
    {
        return DateTime.UtcNow >= lastVoteTime + _minInterval;
    }
}

// 使用
var throttle = new VoteThrottle(TimeSpan.FromHours(24)); // 24 小時投一次
DateTime lastVote = DateTime.UtcNow.AddHours(-12); // 12 小時前投過

if (throttle.CanVote(lastVote, out var remaining))
{
    Console.WriteLine("可以投票");
    // 執行投票邏輯
}
else
{
    Console.WriteLine($"尚需等待 {remaining.Hours} 小時 {remaining.Minutes} 分鐘");
}

進階:只運算日期部分

// 僅加減日期,時間歸零
DateTime dateOnly = DateTime.Today; // 今天 00:00:00
DateTime nextWeekDate = dateOnly.AddDays(7);

// 取得本週一
DateTime today = DateTime.Today;
int daysUntilMonday = ((int)DayOfWeek.Monday - (int)today.DayOfWeek + 7) % 7;
DateTime thisMonday = today.AddDays(daysUntilMonday);

// 取得本月第一天/最後一天
DateTime firstDayOfMonth = new DateTime(today.Year, today.Month, 1);
DateTime lastDayOfMonth = firstDayOfMonth.AddMonths(1).AddDays(-1);

// 取得下個月同一天 (自動處理 31/30/28 天)
DateTime nextMonthSameDay = today.AddMonths(1);

時區與 Kind 關鍵處理

// 危險:混合 Kind 比較
DateTime utc = DateTime.UtcNow;           // Kind = Utc
DateTime local = DateTime.Now;            // Kind = Local
// utc < local 結果不可預期!

// 安全:統一轉換後比較
DateTime utcNow = DateTime.UtcNow;
DateTime localNow = DateTime.Now;
bool safeCompare = utcNow >= localNow.ToUniversalTime(); // 正確

// 儲存建議:統一儲存 UTC
public class VoteRecord
{
    public long VotedAtUtcTicks { get; set; } // 儲存 UTC Ticks
    
    public DateTime VotedAtUtc => new DateTime(VotedAtUtcTicks, DateTimeKind.Utc);
    public DateTime VotedAtLocal => VotedAtUtc.ToLocalTime();
}

常見陷阱與解決方案

| 陷阱 | 說明 | 解決方案 | |------|------|----------| | `AddMonths` 跨月邏輯 | 1/31 + 1 月 = 2/28 (非閏年) | 預期行為,若需「月底」用 `DateTime.DaysInMonth` 計算 | | `DateTime.Now` 多次呼叫不一致 | 毫秒級差異 | 變數儲存 `var now = DateTime.Now` 統一使用 | | 忽略 `Kind` 導致比較錯誤 | Utc vs Local 直接比較 | 統一轉 UTC 或同一時區後比較 | | `TimeSpan` 精度誤解 | `TotalDays` 含小數,`Days` 為整數 | 依需求選擇 `TotalXxx` 或 `Xxx` | | 夏令時間轉換錯誤 | `ToLocalTime`/`ToUniversalTime` 依系統規則 | 使用 `TimeZoneInfo` 指定時區、或儲存 UTC 避開問題 | | `DateTime.MinValue` 參與運算 | 可能溢位拋例外 | 運算前檢查 `== DateTime.MinValue` |

現代替代:DateTimeOffset / NodaTime

// DateTimeOffset:內建 Offset,避免 Kind 混淆
var dto = DateTimeOffset.UtcNow;
var later = dto.AddHours(3); // 保留 Offset

// NodaTime:專業日期時間庫 (強烈推薦複雜業務)
using NodaTime;
var clock = SystemClock.Instance;
Instant now = clock.GetCurrentInstant();
ZonedDateTime taipei = now.InZone(DateTimeZoneProviders.Tzdb["Asia/Taipei"]);
var nextWeek = taipei.LocalDateTime.PlusWeeks(1).InZoneLeniently(taipei.Zone);

效能提示

  • 熱路徑:優先使用 `DateTime` 結構體 (堆疊配置、無 GC)
  • 避免:頻繁 `DateTime.Parse`、字串格式化、`ToLocalTime`/`ToUniversalTime`
  • 快取:`TimeSpan.FromHours(24)` 等常用間隔
  • 比較:直接用 `<` `>` `==`,避免 `CompareTo` 方法呼叫開銷

掌握 `DateTime` 運算細節,能正確實作排程、限流、統計、帳單等時間敏感業務邏輯,避開時區與邊界條件陷阱。

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