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

.NET 2.0 環境下讀取系統時區資訊與夏令時間計算完整實作

     編輯:關於.NET

前言

.NET 3.5+ 內建 TimeZoneInfo 類別,提供豐富的時區轉換、夏令時間判斷功能。但在 .NET 2.0 環境 (或維護舊專案) 時,僅能存取 TimeZone.CurrentTimeZone (僅限伺服器本地時區)。本篇示範如何透過 登錄機碼 讀取 Windows 完整時區資料庫,自行實作 TimeZoneInfo 核心功能:列舉所有时区、計算 UTC 偏移、判斷夏令時間、轉換時間。

Windows 時區資料存放結構

所有時區資訊儲存在登錄機碼:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones

每個子機碼 (如 "Taipei Standard Time") 包含:

  • Display:顯示名稱 (如 "(UTC+08:00) 台北")
  • Std:標準時間名稱 (如 "台北標準時間")
  • Dlt:夏令時間名稱 (如 "台北夏令時間")
  • TZI:REG_BINARY 結構體,含基準偏移、夏令規則 (起始/結束月份、週、日、時)
  • Dynamic DST:歷年夏令規則變更記錄 (如 2010 年前後規則不同)

核心結構體定義 (對應 TZI 二進位結構)

using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct SYSTEMTIME
{
    public ushort wYear, wMonth, wDayOfWeek, wDay;
    public ushort wHour, wMinute, wSecond, wMilliseconds;

    public void SetFromBytes(byte[] data)
    {
        if (data.Length != 16) throw new ArgumentException("SYSTEMTIME 需 16 bytes");
        wYear = BitConverter.ToUInt16(data, 0);
        wMonth = BitConverter.ToUInt16(data, 2);
        wDayOfWeek = BitConverter.ToUInt16(data, 4);
        wDay = BitConverter.ToUInt16(data, 6);
        wHour = BitConverter.ToUInt16(data, 8);
        wMinute = BitConverter.ToUInt16(data, 10);
        wSecond = BitConverter.ToUInt16(data, 12);
        wMilliseconds = BitConverter.ToUInt16(data, 14);
    }
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct TIME_ZONE_INFORMATION
{
    public int Bias;                    // 基準偏移 (分鐘),UTC = Local + Bias
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string StandardName;         // 標準時間名稱
    public SYSTEMTIME StandardDate;     // 標準時間生效規則
    public int StandardBias;            // 標準時間額外偏移
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
    public string DaylightName;         // 夏令時間名稱
    public SYSTEMTIME DaylightDate;     // 夏令時間生效規則
    public int DaylightBias;            // 夏令時間額外偏移 (通常 -60 分鐘)

    public void SetFromBytes(byte[] data)
    {
        if (data.Length != 44) throw new ArgumentException("TZI 需 44 bytes");
        Bias = BitConverter.ToInt32(data, 0);
        StandardBias = BitConverter.ToInt32(data, 4);
        DaylightBias = BitConverter.ToInt32(data, 8);
        var buf = new byte[16];
        Buffer.BlockCopy(data, 12, buf, 0, 16); StandardDate.SetFromBytes(buf);
        Buffer.BlockCopy(data, 28, buf, 0, 16); DaylightDate.SetFromBytes(buf);
    }
}

TimeZoneInfo 完整實作類別

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using Microsoft.Win32;

public class TimeZoneInfo
{
    // --- 私有欄位 ---
    private string _id;                 // 登錄機碼名稱 (如 "Taipei Standard Time")
    private string _displayName;        // 顯示名稱
    private TIME_ZONE_INFORMATION _tzi; // 解析後的 TZI 結構
    private ReadOnlyCollection _adjustmentRules;

    // --- 公開屬性 ---
    public string Id => _id;
    public string DisplayName => _displayName;
    public string StandardName => _tzi.StandardName;
    public string DaylightName => _tzi.DaylightName;
    public TimeSpan BaseUtcOffset => TimeSpan.FromMinutes(-_tzi.Bias); // 注號轉換

    // --- 靜態工廠方法:列舉所有系統時區 ---
    public static TimeZoneInfo[] GetSystemTimeZones()
    {
        var list = new List();
        using (var rootKey = Registry.LocalMachine.OpenSubKey(
            @"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones", false))
        {
            if (rootKey == null) throw new InvalidOperationException("找不到時區登錄機碼");
            
            foreach (string zoneId in rootKey.GetSubKeyNames())
            {
                try
                {
                    var tzi = new TimeZoneInfo { _id = zoneId };
                    tzi.LoadFromRegistry();
                    list.Add(tzi);
                }
                catch { /* 忽略損毀的時區項目 */ }
            }
        }
        // 依 BaseUtcOffset 排序
        list.Sort((a, b) => a.BaseUtcOffset.CompareTo(b.BaseUtcOffset));
        return list.ToArray();
    }

    // --- 從登錄機碼載入資料 ---
    private void LoadFromRegistry()
    {
        using (var key = Registry.LocalMachine.OpenSubKey(
            $@"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones\{_id}", false))
        {
            if (key == null) throw new ArgumentException($"未知時區: {_id}");
            
            _displayName = key.GetValue("Display")?.ToString() ?? _id;
            _tzi.StandardName = key.GetValue("Std")?.ToString() ?? "";
            _tzi.DaylightName = key.GetValue("Dlt")?.ToString() ?? "";
            _tzi.SetFromBytes((byte[])key.GetValue("TZI"));
            
            // 解析 Dynamic DST 歷年規則 (可選,建構 AdjustmentRule)
            BuildAdjustmentRules(key);
        }
    }

    // --- 建構 AdjustmentRule 集合 (支援歷年規則變更) ---
    private void BuildAdjustmentRules(RegistryKey zoneKey)
    {
        var rules = new List();
        using (var dstKey = zoneKey.OpenSubKey("Dynamic DST", false))
        {
            if (dstKey == null)
            {
                // 無歷年變更,使用單一規則
                rules.Add(CreateAdjustmentRule(_tzi.DaylightDate, _tzi.StandardDate, _tzi.DaylightBias));
            }
            else
            {
                // 依年份建立多個規則
                foreach (string yearStr in dstKey.GetValueNames())
                {
                    if (int.TryParse(yearStr, out int year))
                    {
                        var tziBytes = (byte[])dstKey.GetValue(yearStr);
                        var tzi = new TIME_ZONE_INFORMATION();
                        tzi.SetFromBytes(tziBytes);
                        rules.Add(CreateAdjustmentRule(tzi.DaylightDate, tzi.StandardDate, tzi.DaylightBias, year));
                    }
                }
            }
        }
        _adjustmentRules = new ReadOnlyCollection(rules);
    }

    private AdjustmentRule CreateAdjustmentRule(SYSTEMTIME daylightStart, SYSTEMTIME standardStart, int daylightBias, int? startYear = null)
    {
        // 將 SYSTEMTIME 規則轉換為 DateTime 範圍
        // 此處簡化:實際需依規則計算具體生效日期
        var delta = TimeSpan.FromMinutes(-daylightBias); // 夏令偏移量 (通常 +1 小時)
        return AdjustmentRule.CreateAdjustmentRule(
            DateTime.MinValue.Date, DateTime.MaxValue.Date, delta,
            CreateTransitionTime(daylightStart), CreateTransitionTime(standardStart));
    }

    private TransitionTime CreateTransitionTime(SYSTEMTIME st)
    {
        // SYSTEMTIME: wMonth=月, wDay=第幾週(1-5), wDayOfWeek=星期幾(0=日)
        if (st.wMonth == 0) return TransitionTime.CreateFixedDateRule(new DateTime(1,1,1, st.wHour, st.wMinute, 0), 1, 1);
        return TransitionTime.CreateFloatingDateRule(
            new DateTime(1, 1, 1, st.wHour, st.wMinute, 0), st.wMonth, (int)st.wDay, (DayOfWeek)st.wDayOfWeek);
    }

    // --- 核心功能方法 ---

    /// 判斷指定時間是否在夏令時間
    public bool IsDaylightSavingTime(DateTime dateTime)
    {
        // 簡化:依 AdjustmentRule 判斷
        foreach (var rule in _adjustmentRules)
        {
            if (rule.DateStart <= dateTime && dateTime < rule.DateEnd)
            {
                var dstStart = rule.DaylightTransitionStart;
                var dstEnd = rule.DaylightTransitionEnd;
                // 實際需計算該年具體轉換日期時間比較
                return true; // 此處略過詳細計算
            }
        }
        return false;
    }

    /// 取得指定時間的 UTC 偏移 (含夏令調整)
    public TimeSpan GetUtcOffset(DateTime dateTime)
    {
        var offset = BaseUtcOffset;
        if (IsDaylightSavingTime(dateTime))
            offset += TimeSpan.FromHours(1); // 簡化:假設夏令 +1hr
        return offset;
    }

    /// 將本地時間轉換為 UTC
    public DateTime ConvertToUtc(DateTime dateTime) => dateTime - GetUtcOffset(dateTime);

    /// 將 UTC 時間轉換為本地時間
    public DateTime ConvertFromUtc(DateTime dateTime) => dateTime + GetUtcOffset(dateTime);

    /// 取得指定年份的夏令時間變更資訊
    public DaylightTime GetDaylightChanges(int year)
    {
        // 實際需依規則計算該年具體開始/結束日期
        return new DaylightTime(
            new DateTime(year, 3, 1), // 範例
            new DateTime(year, 11, 1),
            TimeSpan.FromHours(1));
    }

    // 靜態排序輔助
    public static void Sort(List list) => 
        list.Sort((a, b) => a.BaseUtcOffset.CompareTo(b.BaseUtcOffset));
}

WinForm 介面展示與測試

public partial class Form1 : Form
{
    private TimeZoneInfo[] _zones;

    private void Form1_Load(object sender, EventArgs e)
    {
        _zones = TimeZoneInfo.GetSystemTimeZones();
        cmbTimeZone.DataSource = _zones;
        cmbTimeZone.DisplayMember = "DisplayName";
        cmbTimeZone.ValueMember = "Id";
    }

    private void btnQuery_Click(object sender, EventArgs e)
    {
        if (cmbTimeZone.SelectedItem is TimeZoneInfo tzi)
        {
            DateTime input = DateTime.Parse(txtDate.Text);
            lblInfo.Text = $"時區 ID: {tzi.Id}\n" +
                           $"顯示名稱: {tzi.DisplayName}\n" +
                           $"標準名稱: {tzi.StandardName}\n" +
                           $"夏令名稱: {tzi.DaylightName}\n" +
                           $"基準偏移: {tzi.BaseUtcOffset}\n" +
                           $"當前偏移: {tzi.GetUtcOffset(input)}\n" +
                           $"是否夏令: {tzi.IsDaylightSavingTime(input)}";
        }
    }
}

關鍵技術細節補充

1. TZI 結構體位元組對應 (44 bytes)

OffsetSize欄位說明
04Bias基準偏移 (分鐘),UTC = Local + Bias (如台北 -480)
44StandardBias標準時間額外偏移 (通常 0)
84DaylightBias夏令時間額外偏移 (通常 -60 = 夏令比標準早 1hr)
1216StandardDateSYSTEMTIME: 標準時間生效規則 (月、第幾週、星期幾、時)
2816DaylightDateSYSTEMTIME: 夏令時間生效規則

2. SYSTEMTIME 規則解讀

  • wMonth:1-12 (0 表示無規則)
  • wDay:1-5 (第幾週),5 表示最後一週
  • wDayOfWeek:0=週日 ... 6=週六
  • wHour:0-23 (UTC 時間)
  • 範例:3月 第2週 週日 02:00 → 夏令開始

3. Dynamic DST 歷年規則處理

某些時區 (如美國、俄羅斯) 歷年調整夏令規則。登錄機碼 Dynamic DST 子機碼下以年份為值名儲存對應年份的 TZI。建構 AdjustmentRule 時需逐年建立,TimeZoneInfo 內部會依輸入日期自動選擇正確規則。

部署與相容性注意

  1. 需登錄讀取權限:ASP.NET 執行身分需具備 `HKLM\...\Time Zones` 讀取權限 (通常預設可讀)。
  2. 跨平台限制:此方案僅適用 Windows;Linux/macOS 需改用 TimeZoneInfo (CoreFX) 或 IANA tz database (NodaTime)。
  3. 快取機制:時區資料極少變更,建議靜態快取 GetSystemTimeZones() 結果,避免重複讀取登錄。
  4. .NET 3.5+ 專案:請直接使用內建 System.TimeZoneInfo,功能更完整且經過測試。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved