ASP.NET Core – DateTime, DateTimeOffset, DateOnly, TimeOnly, TimeSpan, TimeZone, NodaTime 使用基础
前言
心血来潮,这篇讲点基础的东西。
对日期和时区 timezone 不熟悉的读者,请先看这篇 Time Zone, Leap Year, Date Format, Epoch Time 时区, 闰年, 日期格式。
.NET 中的日期与时间类型
DateTime
DateTime 是用来管理 date 和 time 的对象。它是一个比较早期的机制,目前已经逐渐被淘汰了。
取而代之的是 DateTimeOffset、DateOnly 和 TimeOnly。但我们考古一下也是不错的。
DateTime 最大的缺陷是它对 offset 的表达力很差。offset 就是 +08:00 这些(注:offset != timezone)
var datetime = new DateTime(1975, 1, 1, 8, 0, 0, DateTimeKind.Utc);
这是 1975 年 1 月 1 号 上午 8 点。UTC 时区,也就是英国 +00:00。由于是 +00:00 所以表达起来是 ok 的。
但是如果要表达不是 UTC 的话就...它就不太行了。
var datetime = new DateTime(2023, 1, 1, 8, 0, 0, DateTimeKind.Local);
这句表达的是 2023 年 1 月 1 号 上午 8 点。Local 的意思是依据当前 Server OS 选择的 timezone 来设置 offset。我的 Server 在 Malaysia offset 是 +08.00。
我们把它转成 UTC 显示看看
var datetime = new DateTime(2023, 1, 1, 8, 0, 0, DateTimeKind.Local); var utcDatetime = datetime.ToUniversalTime(); Console.WriteLine(datetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 2023-01-01 08:00:00 AM +08:00 Console.WriteLine(utcDatetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 2023-01-01 12:00:00 AM Z
2023 年 1 月 1 号 凌晨 12 点钟,正确
虽然看上去还不错,但是它已经有 2 个大问题了。
1. 它无法任意的选择 timezone,因为它只让你选 Local,除非你去修改 Local timezone。
2. offset != timezone,在不同的时间上,它会有意想不到的结果。
我们把时间换成 1975 年,然后转成 UTC 来显示
var datetime = new DateTime(1975, 1, 1, 8, 0, 0, DateTimeKind.Local); var utcDatetime = datetime.ToUniversalTime(); Console.WriteLine(datetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 08:00:00 AM +08:00 Console.WriteLine(utcDatetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 12:00:00 AM Z
结果是 1975 年 1 月 1 号 凌晨 12 点钟。如果不是马来西亚人,会以为这个答案是正确的,但其实它是错误的。
我们用 JavaScript 来验证。
console.log(new Date(2023, 0, 1, 8)); // 2023-01-01T00:00:00.000Z console.log(new Date(1975, 0, 1, 8)); // 1975-01-01T00:30:00.000Z
注意看,正确答案是 1975 年 1 月 1 号 凌晨 12 点 30 分。这个 30 分钟是因为马来西亚曾经换过 timezone offset。以前是 +07:30 后来才改成 +08:00。
而 kind = Local 只是单纯的使用了当前 Server OS 此时此刻的 offset 而非 date time value 那个时候的 timezone offset,所以表达的时间就出错了。
所以结论是,如果你要表达精准时间,就用 UTC,如果不想用 UTC,那就不要用 DateTime,改用 DateTimeOffset。
DateTimeOffset
顾名思义,它就是多了一个 offset 设置的 DateTime。所以我们不在设置 kind = UTC 或 Local,我们直接告诉它具体的 offset 是多少。
var datetime = new DateTimeOffset(2023, 1, 1, 8, 0, 0, offset: TimeSpan.FromHours(8)); var utcDatetime = datetime.ToUniversalTime(); Console.WriteLine(datetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 2023-01-01 08:00:00 AM +08:00 Console.WriteLine(utcDatetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 2023-01-01 12:00:00 AM +00:00
我们可以放任何的 offset,而不像 DateTime kind = Local 那样,只能设置 Server OS 的 timezone offset。
虽然它已经比 DateTime 好多了,但是,它依据没有解决 offset != timezone 的问题哦。
1975 年依然是错误
var datetime = new DateTimeOffset(1975, 1, 1, 8, 0, 0, offset: TimeSpan.FromHours(8)); var utcDatetime = datetime.ToUniversalTime(); Console.WriteLine(datetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 08:00:00 AM +08:00 Console.WriteLine(utcDatetime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 12:00:00 AM +00:00
但这个很好理解,因为我们设置的是 offset,而不是 timezone,依据 datetime 和 timezone 找出 offset 是我们的责任。
所以 1975 年,马来西亚的 timezone 我应该要设置的 offset 是 +07:30 才对。(注:ASP.NET Core 没有 build-in 的方法提我们做这个 timezone to offset 的转换,但是我们可以用 library NodaTime,下面我会提到)
DateOnly
DateTime 的问题是 offset,UTC 不需要 offset 所以可以用 DateTime,还有一种情况也是不需要 offset,那就是表达日期。
比如 1987 年 12 月 15 日是我的生辰,我就只表达日期,不表达具体时间和时区,这种情况 DateTime 也够用。
虽然如此,DateTime 还是不够好,因为我只是要表达日期,没有要表达时间丫。0 时 0 分 0 秒 表达的是 0 而不是 null。
于是为了更精确的表达,.NET 6.0 推出了 DateOnly 类型。它就是没有 Time 的 DateTime,仅此而已。
TimeOnly
有 DateOnly 自然就有 TimeOnly 咯。不管是 DateOnly 还是 TimeOnly,都只是缩小了原本 DateTime 的管理范围而已。
所以掌握了 DateTimeOffset 就掌握了 DateTime 就掌握了 DateOnly 和 TimeOnly。
NodaTime
NodaTime 不是 build-in 的类型,它是一个 library。上面提到最厉害的 DateTimeOffset 都无法处理 timezone,它只是处理 offset 而已。
当需要处理 timezone 时,需要引入这个 library。下面会给出例子。
常用方法
此时此刻
var utcNow = DateTimeOffset.UtcNow; // offset +00:00 var localNow = DateTimeOffset.Now; // offset +08:00 依据 Server OS 的 timezone offset
都是输出此时此刻,只是一个是用 UTC +00:00 表达,一个是依据 Server OS timezone offset 来表达。
今天
var today1 = DateTime.Today; var today2 = DateTimeOffset.Now.Date;
2 个方法都可以,时间为 0 时 0 分 0 秒
Since 1970-01-01 (unix/epoch time)
相等于 JavaScript 的 .getTime()
var unitTimeMilliseconds = DateTimeOffset.Now.ToUnixTimeMilliseconds; var unitTimeSeconds = DateTimeOffset.Now.ToUnixTimeSeconds;
秒或毫秒都可以。
DateTimeOffset to String
DateTimeOffset.Now.ToString("yyyy-MM-dd hh:mm:ss tt K"); // 2023-10-29 05:29:07 PM +08:00 DateTimeOffset.Now.ToString("yyyy-MM-dd"); // 2023-10-29 DateTimeOffset.Now.ToString("hh:mm:ss tt"); // 05:29:07 PM
String to DateTimeOffset
var datetimeOffset = DateTimeOffset.ParseExact( "2023-10-29 05:29:07 PM +08:00", "yyyy-MM-dd hh:mm:ss tt K", CultureInfo.InvariantCulture ); if ( DateTimeOffset.TryParseExact( "2023-10-29 05:29:07 PM +08:00", "yyyy-MM-dd hh:mm:ss tt K", CultureInfo.InvariantCulture, DateTimeStyles.None, out var datetime ) ) { // do something with datetime }
DateTimeOffset to DateOnly and TimeOnly
var dateTimeOffset = new DateTimeOffset(1975, 1, 1, 8, 0, 0, TimeSpan.FromHours(8)); var date = DateOnly.FromDateTime(dateTimeOffset.Date); // 1975-01-01 var time = TimeOnly.FromDateTime(dateTimeOffset.DateTime); // 08:00
注意:它是不看 offset 的哦,直接 ignore。
DateOnly to DateTime
var date = new DateOnly(2025, 01, 01); var datetime = date.ToDateTime(TimeOnly.MinValue); // 补上 time 00:00:00 就可以了
Server OS Timezone Informations
foreach (TimeZoneInfo timeZoneInfo in TimeZoneInfo.GetSystemTimeZones()) { Console.WriteLine($"{timeZoneInfo.Id} : {timeZoneInfo.DisplayName} : {timeZoneInfo.BaseUtcOffset.Hours}"); // Hawaiian Standard Time : (UTC-10:00) Hawaii : -10 // GMT Standard Time : (UTC+00:00) Dublin, Edinburgh, Lisbon, London : 0 // Singapore Standard Time : (UTC+08:00) Kuala Lumpur, Singapore : 8 }
通过 OS timezone 可以找到不同 timezone 的 offset,这个 offset 是最新的,像马来西亚曾经换过 timezone offset 从 +07:30 换成 +08:00,这里只会显示 +08:00。
NodaTime 常用方法
创建指定 timezone 的 DatetimeOffset
1. 创建 timezone
var timezone = DateTimeZoneProviders.Tzdb["Asia/Kuala_Lumpur"];
注: Noda timezone 和 .NET timezone 的 ID 虽然 ISO 一样但写法却不一样,参考 Docs – IANA (TZDB) time zone information
2. 定义日期时间
var datetime = new LocalDateTime(1975, 1, 1, 8, 0, 0); // 或者 from .NET DateTime // var datetime = LocalDateTime.FromDateTime(new DateTime(1975, 1, 1, 8, 0, 0));
我们用回上面 1975 年的例子。
3. set timezone to datetime
var datetimeWithZone = datetime.InZoneStrictly(timezone);
5. convert Noda DateTime to .NET DateTimeOffset
var datetimeoffset = datetimeWithZone.ToDateTimeOffset(); Console.WriteLine(datetimeoffset.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 08:00:00 AM +07:30 Console.WriteLine(datetimeoffset.UtcDateTime.ToString("yyyy-MM-dd hh:mm:ss tt K")); // 1975-01-01 12:30:00 AM Z
Noda 成功依据 datetime 和 timezone 返回了正确的 offset,1975 年 Malaysia 的 timezone 是 +07:30,现在才是 +08:00。
Change TimeZone
把马来西亚 1975 年 1 月 1 日 上午 8 点 convert to 夏威夷时区的时间。
var malaysiaTimeZone = DateTimeZoneProviders.Tzdb["Asia/Kuala_Lumpur"]; var malaysiaDateTime = new LocalDateTime(1975, 1, 1, 8, 0, 0); var malaysiaDateTimeOffset = malaysiaDateTime.InZoneStrictly(malaysiaTimeZone).ToDateTimeOffset(); var hawaiiTimeZone = DateTimeZoneProviders.Tzdb["Pacific/Honolulu"]; Console.WriteLine( Instant.FromDateTimeOffset(malaysiaDateTimeOffset).InZone(hawaiiTimeZone).ToDateTimeOffset().ToString("yyyy-MM-dd hh:mm:ss tt K") ); // 1974-12-31 02:30:00 PM -10:00
关键是 NodaTime 有所以 timezone 在不同时期的 offset。
所以只要可以拿到指定 datetime 的 timezone offset,之后就只是 convert 来 convert 的问题而已。
List out all timezone ID
参考 Docs – IANA (TZDB) time zone information
foreach (var timezone in TzdbDateTimeZoneSource.Default.ZoneLocations!) { Console.WriteLine(timezone.ZoneId); // Pacific/Honolulu Console.WriteLine(timezone.CountryName); // United States Console.WriteLine(timezone.Comment); // Hawaii }

浙公网安备 33010602011771号