國內大多數開發者使用的電腦,都是使用的北京時間,日常開發的過程中其實並沒有什么不便;不過,等遇到了阿里雲等雲服務器,系統默認使用的時間大多為UTC時間,這個時候,時區和時間的問題,就是不容忽視的大問題。
概念
首先明確一點,對於一個時刻,不管你用UTC時間還是UTC+8的時間來表示,本質上是一個時刻,就是一樣的。我們處理日期和時間的目標,也是為了保證這個時刻不會因為時區的不同出現對不上的情況。
DateTime與DateTimeOffset
.NET中表示時刻的數據類型有這兩個(新出的Date和Time不作討論),關於這兩個數據類型,已經有同學寫的很清楚了,阿里雲很多服務器使用的時間為UTC時間,這個時候,如果使用DateTime,是很難說清楚時區(Kind只有UTC、Local還有未指定,不支持特定的某個時區),因此我們應當優先使用DateTimeOffset。
TimeZoneInfo
用於跨時區的情況下,時區的信息是很重要的,.NET中使用TimeZoneInfo這個類表示時區的信息。該類提供了一些靜態方法,可以用於查找時區和創建時區等等。最早我是傾向於使用這些方法找到東八區的信息的,但是我發現諸如ConvertTimeBySystemTimeZoneId
和FindSystemTimeZoneById
的方法,都依賴於系統中的定義,不同的系統可能還不一樣,自己定義是比較保險的,於是,我使用了CreateCustomTimeZone
來新建一個時區。
Unix時間戳是比較於1970年的UTC標准時間,因此在處理的過程中,DateTime的時間表示應當將它轉換為UTC時間,以下的代碼,是使用TimeZoneInfo實現時間轉換的,使用的是DateTime數據類型。如果改用DateTimeOffset,這個類型對轉換為Unix時間戳更加友好。
internal static class DateTimeExtension
{
private static readonly TimeZoneInfo gmt8 = TimeZoneInfo.CreateCustomTimeZone("GMT+8", TimeSpan.FromHours(8), "China Standard Time", "(UTC+8)China Standard Time");
public static long ToUnixTime(this DateTime datetime)
{
DateTime dateTimeUtc = datetime;
if (datetime.Kind != DateTimeKind.Utc)
{
dateTimeUtc = datetime.ToUniversalTime();
}
if (dateTimeUtc.ToUniversalTime() <= DateTime.UnixEpoch)
{
return 0;
}
return (long)(dateTimeUtc - DateTime.UnixEpoch).TotalMilliseconds;
}
public static DateTime ToDateTime(this long unixTimestamp)
{
DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
return TimeZoneInfo.ConvertTimeFromUtc(time, gmt8);
}
public static DateTime ToDateTime(this long unixTimestamp, int timezone)
{
DateTime time = DateTime.UnixEpoch.AddMilliseconds(unixTimestamp);
return time.AddHours(timezone);
}
}
其實,只要時區是正確的,那么可以也可以使用網友提供的方法進行轉換。
// Code from https://stackoverflow.com/questions/5615538/parse-a-date-string-into-a-certain-timezone-supporting-daylight-saving-time
public DateTimeOffset ParseDateExactForTimeZone(string dateTime, TimeZoneInfo timezone)
{
var parsedDateLocal = DateTimeOffset.ParseExact(dateTime, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);
var tzOffset = timezone.GetUtcOffset(parsedDateLocal.DateTime);
var parsedDateTimeZone = new DateTimeOffset(parsedDateLocal.DateTime, tzOffset);
return parsedDateTimeZone;
}
實踐指南
處理日期與時間的過程中,如果加入了TimeZoneInfo的情況下會使得程序變得非常麻煩,特別是各種TimeZone的Id和名稱,不同系統也不統一的情況下,容易出現各種各樣的問題。我想的就是避免用它,說說我的處理原則吧。
- 日期時間不使用DateTime類,全部使用DateTimeOffset類型
- 系統的內部處理,全部使用UTC標准時間進行數據表示
- 對於字符串的轉換為DataTimeOffset的情況,顯式指定時區的小時偏移量
- 直接使用時間的加減,避免使用時區的信息轉換導致的代碼復雜度增加
- 【可選】如果不用考慮2038年的情況下,可以考慮Unix時間戳簡化時間表示
直接貼上我現在使用的代碼段,思路就是在強制給字符串表示的時間,加上UTC標准時區信息,然后再修正時差。
public static class DateTimeExtension
{
public static long? ParseUnixTimeMillisecondsWithTimeZone(string datetimeString, string format = "yyyyMMddHHmmss", int timezoneOffset = 8)
{
//注意這里非常關鍵的參數DateTimeStyles.AssumeUniversal,就是設定數據都是UTC的,不管是不是,都強行指定為UTC,然后再按照時區的信息調整為正確的時間。
//給定的數據是東八區時間,但是加上這個參數,實際上的時間就會提前了8個小時,因此需要在后面的數據中直接減去8個小時,如果是其他地區的時間,那么也是一樣操作。
if (!DateTimeOffset.TryParseExact(datetimeString, format, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out DateTimeOffset time)) return null;
DateTimeOffset dateTimeUtcOffset = time.AddHours(-timezoneOffset);
return dateTimeUtcOffset.ToUnixTimeMilliseconds();
}
public static DateTimeOffset ToDateTime(this long unixTimestamp) => DateTimeOffset.FromUnixTimeMilliseconds(unixTimestamp);
}
對於ASP.NET CORE,JSON.NET會自動處理符合ISO8601規范的日期格式,只要指定數據類型為DateTimeOffset,就能夠准確轉換了。
參考
- https://stackoverflow.com/questions/5615538/parse-a-date-string-into-a-certain-timezone-supporting-daylight-saving-time
- https://stackoverflow.com/questions/62433342/c-sharp-datetime-converting-a-datetimeoffset-to-another-timezone
- https://stackoverflow.com/questions/63153809/parse-string-into-datetimeoffset-while-assuming-timezone