如何使用 DateOnly 和 TimeOnly 結構

DateOnlyTimeOnly 結構隨 .NET 6 引進,分別代表特定的日期或當天時間。 在 .NET 6 之前,且一律在 .NET Framework 中,開發人員使用 DateTime 類型 (或某些其他替代) 來代表下列其中一項:

  • 整個日期和時間。
  • 日期,忽略時間。
  • 時間,忽略日期。

DateOnlyTimeOnly 是代表 DateTime 類型的特定部分類型。

重要

DateOnlyTimeOnly 類型無法在 .NET Framework 中使用。

DateOnly 結構

DateOnly 結構代表不含時間的特定日期。 因為沒有時間元件,所以該結構代表從一天開始到一天結束的日期。 此結構非常適合用來儲存特定日期,例如生日、週年日或商務相關日期。

雖然您可以在忽略時間元件時使用 DateTime,但對 DateTime 使用 DateOnly 有幾個優點:

  • 如果結構是依時區時差,則 DateTime 結構可能會向前一天或下一天變換。 DateOnly 無法依時區時差,而且一律代表設定的日期。

  • 序列化 DateTime 結構包含時間元件,這可能會遮蔽資料的意圖。 此外,DateOnly 序列化的資料較少。

  • 當程式碼與資料庫 (例如 SQL Server) 互動時,整個日期通常會儲存為 date 資料類型,不包含時間。 DateOnly 更適合比對資料庫類型。

DateOnly 的範圍從 0001-01-01 到 9999-12-31,就像 DateTime 一樣。 您可以在 DateOnly 建構函式中指定特定的行事曆。 不過,不論使用哪一個行事歷來建構日期,DateOnly 物件一律代表西曆中的日期。 例如,您可以從希伯來曆建立日期,但日期會轉換成西曆:

var hebrewCalendar = new System.Globalization.HebrewCalendar();
var theDate = new DateOnly(5776, 2, 8, hebrewCalendar); // 8 Cheshvan 5776

Console.WriteLine(theDate);

/* This example produces the following output:
 *
 * 10/21/2015
*/
Dim hebrewCalendar = New System.Globalization.HebrewCalendar()
Dim theDate = New DateOnly(5776, 2, 8, hebrewCalendar) ' 8 Cheshvan 5776

Console.WriteLine(theDate)

' This example produces the following output
'
' 10/21/2015

DateOnly 範例

使用下列範例以了解 DateOnly

將 DateTime 轉換為 DateOnly

使用 DateOnly.FromDateTime 靜態方法從 DateTime 類型建立 DateOnly 類型,如下列程式碼所示:

var today = DateOnly.FromDateTime(DateTime.Now);
Console.WriteLine($"Today is {today}");

/* This example produces output similar to the following:
 * 
 * Today is 12/28/2022
*/
Dim today = DateOnly.FromDateTime(DateTime.Now)
Console.WriteLine($"Today is {today}")

' This example produces output similar to the following
' 
' Today is 12/28/2022

新增或減去天數、月數、年份

有三種方法可用來調整 DateOnly 結構:AddDaysAddMonthsAddYears。 每個方法都會採用整數參數,並依該度量增加日期。 如果提供了負數,日期就會減去該度量。 方法會傳回 DateOnly 的新執行個體,因為結構是不可變的。

var theDate = new DateOnly(2015, 10, 21);

var nextDay = theDate.AddDays(1);
var previousDay = theDate.AddDays(-1);
var decadeLater = theDate.AddYears(10);
var lastMonth = theDate.AddMonths(-1);

Console.WriteLine($"Date: {theDate}");
Console.WriteLine($" Next day: {nextDay}");
Console.WriteLine($" Previous day: {previousDay}");
Console.WriteLine($" Decade later: {decadeLater}");
Console.WriteLine($" Last month: {lastMonth}");

/* This example produces the following output:
 * 
 * Date: 10/21/2015
 *  Next day: 10/22/2015
 *  Previous day: 10/20/2015
 *  Decade later: 10/21/2025
 *  Last month: 9/21/2015
*/
Dim theDate = New DateOnly(2015, 10, 21)

Dim nextDay = theDate.AddDays(1)
Dim previousDay = theDate.AddDays(-1)
Dim decadeLater = theDate.AddYears(10)
Dim lastMonth = theDate.AddMonths(-1)

Console.WriteLine($"Date: {theDate}")
Console.WriteLine($" Next day: {nextDay}")
Console.WriteLine($" Previous day: {previousDay}")
Console.WriteLine($" Decade later: {decadeLater}")
Console.WriteLine($" Last month: {lastMonth}")

' This example produces the following output
' 
' Date: 10/21/2015
'  Next day: 10/22/2015
'  Previous day: 10/20/2015
'  Decade later: 10/21/2025
'  Last month: 9/21/2015

剖析和格式化 DateOnly

DateOnly 可以從字串剖析,就像 DateTime 結構一樣。 所有標準 .NET 以日期為基礎的剖析權杖都可以搭配使用 DateOnly。 將 DateOnly 類型轉換成字串時,您也可以使用標準 .NET 以日期為基礎的格式設定模式。 如需格式字串的詳細資訊,請參閱標準日期和時間格式字串

var theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
var theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture);

Console.WriteLine(theDate.ToString("m", CultureInfo.InvariantCulture));     // Month day pattern
Console.WriteLine(theDate2.ToString("o", CultureInfo.InvariantCulture));    // ISO 8601 format
Console.WriteLine(theDate2.ToLongDateString());

/* This example produces the following output:
 * 
 * October 21
 * 2015-10-21
 * Wednesday, October 21, 2015
*/
Dim theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture) ' Custom format
Dim theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture)

Console.WriteLine(theDate.ToString("m", CultureInfo.InvariantCulture))     ' Month day pattern
Console.WriteLine(theDate2.ToString("o", CultureInfo.InvariantCulture))    ' ISO 8601 format
Console.WriteLine(theDate2.ToLongDateString())

' This example produces the following output
' 
' October 21
' 2015-10-21
' Wednesday, October 21, 2015

比較 DateOnly

DateOnly 可以與其他執行個體進行比較。 例如,您可以檢查日期是否在另一個日期之前或之後,或某個日期是否符合特定日期。

var theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture);  // Custom format
var theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture);
var dateLater = theDate.AddMonths(6);
var dateBefore = theDate.AddDays(-10);

Console.WriteLine($"Consider {theDate}...");
Console.WriteLine($" Is '{nameof(theDate2)}' equal? {theDate == theDate2}");
Console.WriteLine($" Is {dateLater} after? {dateLater > theDate} ");
Console.WriteLine($" Is {dateLater} before? {dateLater < theDate} ");
Console.WriteLine($" Is {dateBefore} after? {dateBefore > theDate} ");
Console.WriteLine($" Is {dateBefore} before? {dateBefore < theDate} ");

/* This example produces the following output:
 * 
 * Consider 10/21/2015
 *  Is 'theDate2' equal? True
 *  Is 4/21/2016 after? True
 *  Is 4/21/2016 before? False
 *  Is 10/11/2015 after? False
 *  Is 10/11/2015 before? True
*/
Dim theDate = DateOnly.ParseExact("21 Oct 2015", "dd MMM yyyy", CultureInfo.InvariantCulture) ' Custom format
Dim theDate2 = DateOnly.Parse("October 21, 2015", CultureInfo.InvariantCulture)
Dim dateLater = theDate.AddMonths(6)
Dim dateBefore = theDate.AddDays(-10)

Console.WriteLine($"Consider {theDate}...")
Console.WriteLine($" Is '{NameOf(theDate2)}' equal? {theDate = theDate2}")
Console.WriteLine($" Is {dateLater} after? {dateLater > theDate} ")
Console.WriteLine($" Is {dateLater} before? {dateLater < theDate} ")
Console.WriteLine($" Is {dateBefore} after? {dateBefore > theDate} ")
Console.WriteLine($" Is {dateBefore} before? {dateBefore < theDate} ")

' This example produces the following output
' 
' Consider 10/21/2015
'  Is 'theDate2' equal? True
'  Is 4/21/2016 after? True
'  Is 4/21/2016 before? False
'  Is 10/11/2015 after? False
'  Is 10/11/2015 before? True

TimeOnly 結構

TimeOnly 結構代表當天時間值,例如每日鬧鐘,或您每天午餐的時間。 TimeOnly 限制為 00:00:00.0000000 - 23:59:59.9999999,即一天的特定時間。

在引進 TimeOnly 類型之前,程式設計師通常會使用 DateTime 類型或 TimeSpan 類型來代表特定時間。 不過,使用這些結構來模擬沒有日期的時間可能會造成一些問題,這些問題可使用 TimeOnly 解決:

  • TimeSpan 代表已耗用時間,例如使用停止監看式測量的時間。 上限範圍超過 29,000 年,而且其值可以是負值,表示時間往後移動。 負數 TimeSpan 不表示一天的特定時間。

  • 如果使用 TimeSpan 作為一天的時間,系統有可能會將其操作到 24 小時以外的值。 TimeOnly 就沒有此風險。 例如,如果員工的工作班次從 18:00 開始,且持續 8 小時,將 8 小時新增至 TimeOnly 結構會變換為 2:00

  • 若針對一天中的時間使用 DateTime,需要有任意日期與時間相關聯,之後再忽略。 選擇 DateTime.MinValue (0001-01-01-01) 作為日期是常見的作法,不過,如果從 DateTime 值減去小時,可能會發生例外狀況 OutOfRangeTimeOnly 因為時間在 24 小時的時間範圍內向前和向後移動,因此沒有這個問題。

  • 序列化 DateTime 結構包含日期元件,這可能會遮蔽資料的意圖。 此外,TimeOnly 序列化的資料較少。

TimeOnly 範例

使用下列範例以了解 TimeOnly

將 DateTime 轉換為 TimeOnly

使用 TimeOnly.FromDateTime 靜態方法從 DateTime 類型建立 TimeOnly 類型,如下列程式碼所示:

var now = TimeOnly.FromDateTime(DateTime.Now);
Console.WriteLine($"It is {now} right now");

/* This example produces output similar to the following:
 * 
 * It is 2:01 PM right now
*/
Dim now = TimeOnly.FromDateTime(DateTime.Now)
Console.WriteLine($"It is {now} right now")

' This example produces output similar to the following
' 
' It is 2:01 PM right now

加法或減法

有三種方法可用來調整 TimeOnly 結構:AddHoursAddMinutesAddAddHoursAddMinutes 都會採用整數參數,並據以調整值。 您可以使用負值來減去以及使用正值來加入。 方法會傳回 TimeOnly 的新執行個體,因為結構是不可變的。 Add 方法會採用 TimeSpan 參數,並從 TimeOnly 值中加入或減去值。

因為 TimeOnly 只代表 24 小時期間,所以在新增提供給這三種方法的值時,會適當地向前或向後輪替。 例如,如果您使用 01:30:00 的值來代表上午 1:30,則從該期間新增 -4 小時,時間會復原到 21:30:00,也就是下午 9:30。 AddHoursAddMinutesAdd 的方法多載會擷取輪替的天數。

var theTime = new TimeOnly(7, 23, 11);

var hourLater = theTime.AddHours(1);
var minutesBefore = theTime.AddMinutes(-12);
var secondsAfter = theTime.Add(TimeSpan.FromSeconds(10));
var daysLater = theTime.Add(new TimeSpan(hours: 21, minutes: 200, seconds: 83), out int wrappedDays);
var daysBehind = theTime.AddHours(-222, out int wrappedDaysFromHours);

Console.WriteLine($"Time: {theTime}");
Console.WriteLine($" Hours later: {hourLater}");
Console.WriteLine($" Minutes before: {minutesBefore}");
Console.WriteLine($" Seconds after: {secondsAfter}");
Console.WriteLine($" {daysLater} is the time, which is {wrappedDays} days later");
Console.WriteLine($" {daysBehind} is the time, which is {wrappedDaysFromHours} days prior");

/* This example produces the following output:
 * 
 * Time: 7:23 AM
 *  Hours later: 8:23 AM
 *  Minutes before: 7:11 AM
 *  Seconds after: 7:23 AM
 *  7:44 AM is the time, which is 1 days later 
 *  1:23 AM is the time, which is -9 days prior
*/
Dim wrappedDays As Integer
Dim wrappedDaysFromHours As Integer

Dim theTime = New TimeOnly(7, 23, 11)

Dim hourLater = theTime.AddHours(1)
Dim minutesBefore = theTime.AddMinutes(-12)
Dim secondsAfter = theTime.Add(TimeSpan.FromSeconds(10))
Dim daysLater = theTime.Add(New TimeSpan(hours:=21, minutes:=200, seconds:=83), wrappedDays)
Dim daysBehind = theTime.AddHours(-222, wrappedDaysFromHours)

Console.WriteLine($"Time: {theTime}")
Console.WriteLine($" Hours later: {hourLater}")
Console.WriteLine($" Minutes before: {minutesBefore}")
Console.WriteLine($" Seconds after: {secondsAfter}")
Console.WriteLine($" {daysLater} is the time, which is {wrappedDays} days later")
Console.WriteLine($" {daysBehind} is the time, which is {wrappedDaysFromHours} days prior")

' This example produces the following output
' 
' Time: 7:23 AM
'  Hours later: 8:23 AM
'  Minutes before: 7:11 AM
'  Seconds after: 7:23 AM
'  7:44 AM is the time, which is 1 days later 
'  1:23 AM is the time, which is -9 days prior

剖析及格式化 TimeOnly

TimeOnly 可以從字串剖析,就像 DateTime 結構一樣。 所有標準 .NET 以時間為基礎的剖析權杖都可以搭配使用 TimeOnly。 將 TimeOnly 類型轉換成字串時,您也可以使用標準 .NET 以日期為基礎的格式設定模式。 如需格式字串的詳細資訊,請參閱標準日期和時間格式字串

var theTime = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture);  // Custom format
var theTime2 = TimeOnly.Parse("17:30:25", CultureInfo.InvariantCulture);

Console.WriteLine(theTime.ToString("o", CultureInfo.InvariantCulture));     // Round-trip pattern.
Console.WriteLine(theTime2.ToString("t", CultureInfo.InvariantCulture));    // Long time format
Console.WriteLine(theTime2.ToLongTimeString());

/* This example produces the following output:
 * 
 * 17:00:00.0000000
 * 17:30
 * 5:30:25 PM
*/
Dim theTime = TimeOnly.ParseExact("5:00 pm", "h:mm tt", CultureInfo.InvariantCulture) ' Custom format
Dim theTime2 = TimeOnly.Parse("17:30:25", CultureInfo.InvariantCulture)

Console.WriteLine(theTime.ToString("o", CultureInfo.InvariantCulture))     ' Round-trip pattern.
Console.WriteLine(theTime2.ToString("t", CultureInfo.InvariantCulture))    ' Long time format
Console.WriteLine(theTime2.ToLongTimeString())

' This example produces the following output
' 
' 17:00:00.0000000
' 17:30
' 5:30:25 PM

序列化 DateOnly 和 TimeOnly 類型

使用 .NET 7+,System.Text.Json 支援序列化和還原序列化 DateOnlyTimeOnly 類型。 請考慮下列物件:

sealed file record Appointment(
    Guid Id,
    string Description,
    DateOnly Date,
    TimeOnly StartTime,
    TimeOnly EndTime);
Public NotInheritable Class Appointment
    Public Property Id As Guid
    Public Property Description As String
    Public Property DateValue As DateOnly?
    Public Property StartTime As TimeOnly?
    Public Property EndTime As TimeOnly?
End Class

下列範例會序列化 Appointment 物件、顯示產生的 JSON,然後將其還原序列化回 Appointment 類型的新執行個體。 最後,系統會比較原始和新還原序列化的執行個體是否相等,並將結果寫入主控台:

Appointment originalAppointment = new(
    Id: Guid.NewGuid(),
    Description: "Take dog to veterinarian.",
    Date: new DateOnly(2002, 1, 13),
    StartTime: new TimeOnly(5,15),
    EndTime: new TimeOnly(5, 45));
string serialized = JsonSerializer.Serialize(originalAppointment);

Console.WriteLine($"Resulting JSON: {serialized}");

Appointment deserializedAppointment =
    JsonSerializer.Deserialize<Appointment>(serialized)!;

bool valuesAreTheSame = originalAppointment == deserializedAppointment;
Console.WriteLine($"""
    Original record has the same values as the deserialized record: {valuesAreTheSame}
    """);
        Dim originalAppointment As New Appointment With {
            .Id = Guid.NewGuid(),
            .Description = "Take dog to veterinarian.",
            .DateValue = New DateOnly(2002, 1, 13),
            .StartTime = New TimeOnly(5, 3, 1),
            .EndTime = New TimeOnly(5, 3, 1)
}
        Dim serialized As String = JsonSerializer.Serialize(originalAppointment)

        Console.WriteLine($"Resulting JSON: {serialized}")

        Dim deserializedAppointment As Appointment =
            JsonSerializer.Deserialize(Of Appointment)(serialized)

        Dim valuesAreTheSame As Boolean =
            (originalAppointment.DateValue = deserializedAppointment.DateValue AndAlso
            originalAppointment.StartTime = deserializedAppointment.StartTime AndAlso
            originalAppointment.EndTime = deserializedAppointment.EndTime AndAlso
            originalAppointment.Id = deserializedAppointment.Id AndAlso
            originalAppointment.Description = deserializedAppointment.Description)

        Console.WriteLine(
            $"Original object has the same values as the deserialized object: {valuesAreTheSame}")

在上述程式碼中:

  • Appointment 物件會具現化並指派給 appointment 變數。
  • appointment 執行個體會使用 JsonSerializer.Serialize 序列化為 JSON。
  • 產生的 JSON 會寫入主控台。
  • JSON 會使用 JsonSerializer.Deserialize 還原序列化回 Appointment 類型的新執行個體。
  • 系統會比較原始和新還原序列化的執行個體是否相等。
  • 比較的結果會寫入主控台。

如需詳細資訊,請參閱如何在 .NET 中序列化和還原序列化 JSON

搭配使用 TimeSpan 和 DateTime

TimeOnly 可以從其中建立,並轉換成 TimeSpan。 此外,TimeOnly 也可以搭配 DateTime 使用來建立 TimeOnly 執行個體,或在提供日期時可建立 DateTime 執行個體。

下列範例會從 TimeSpan 建立 TimeOnly 物件,然後將其轉換回:

// TimeSpan must in the range of 00:00:00.0000000 to 23:59:59.9999999
var theTime = TimeOnly.FromTimeSpan(new TimeSpan(23, 59, 59));
var theTimeSpan = theTime.ToTimeSpan();

Console.WriteLine($"Variable '{nameof(theTime)}' is {theTime}");
Console.WriteLine($"Variable '{nameof(theTimeSpan)}' is {theTimeSpan}");

/* This example produces the following output:
 * 
 * Variable 'theTime' is 11:59 PM
 * Variable 'theTimeSpan' is 23:59:59
*/
' TimeSpan must in the range of 00:00:00.0000000 to 23:59:59.9999999
Dim theTime = TimeOnly.FromTimeSpan(New TimeSpan(23, 59, 59))
Dim theTimeSpan = theTime.ToTimeSpan()

Console.WriteLine($"Variable '{NameOf(theTime)}' is {theTime}")
Console.WriteLine($"Variable '{NameOf(theTimeSpan)}' is {theTimeSpan}")

' This example produces the following output
' 
' Variable 'theTime' is 11:59 PM
' Variable 'theTimeSpan' is 23:59:59

下列範例會從 TimeOnly 物件建立 DateTime,並選取任意日期:

var theTime = new TimeOnly(11, 25, 46);   // 11:25 AM and 46 seconds
var theDate = new DateOnly(2015, 10, 21); // October 21, 2015
var theDateTime = theDate.ToDateTime(theTime);
var reverseTime = TimeOnly.FromDateTime(theDateTime);

Console.WriteLine($"Date only is {theDate}");
Console.WriteLine($"Time only is {theTime}");
Console.WriteLine();
Console.WriteLine($"Combined to a DateTime type, the value is {theDateTime}");
Console.WriteLine($"Converted back from DateTime, the time is {reverseTime}");

/* This example produces the following output:
 * 
 * Date only is 10/21/2015
 * Time only is 11:25 AM
 * 
 * Combined to a DateTime type, the value is 10/21/2015 11:25:46 AM
 * Converted back from DateTime, the time is 11:25 AM
*/
Dim theTime = New TimeOnly(11, 25, 46) ' 11:   25 PM And 46 seconds
Dim theDate = New DateOnly(2015, 10, 21) ' October 21, 2015
Dim theDateTime = theDate.ToDateTime(theTime)
Dim reverseTime = TimeOnly.FromDateTime(theDateTime)

Console.WriteLine($"Date only is {theDate}")
Console.WriteLine($"Time only is {theTime}")
Console.WriteLine()
Console.WriteLine($"Combined to a DateTime type, the value is {theDateTime}")
Console.WriteLine($"Converted back from DateTime, the time is {reverseTime}")

' This example produces the following output
' 
' Date only is 10/21/2015
' Time only is 11:25 AM
' 
' Combined to a DateTime type, the value is 10/21/2015 11:25:46 AM
' Converted back from DateTime, the time is 11:25 AM

算術運算子和比較 TimeOnly

兩個 TimeOnly 執行個體可以彼此比較,且您可以使用 IsBetween 方法來檢查某個時間是否在其他兩次之間。 當在 TimeOnly 上使用加法或減法運算子時,系統會傳回 TimeSpan,表示時間的持續時間。

var start = new TimeOnly(10, 12, 01); // 10:12:01 AM
var end = new TimeOnly(14, 00, 53); // 02:00:53 PM

var outside = start.AddMinutes(-3);
var inside = start.AddMinutes(120);

Console.WriteLine($"Time starts at {start} and ends at {end}");
Console.WriteLine($" Is {outside} between the start and end? {outside.IsBetween(start, end)}");
Console.WriteLine($" Is {inside} between the start and end? {inside.IsBetween(start, end)}");
Console.WriteLine($" Is {start} less than {end}? {start < end}");
Console.WriteLine($" Is {start} greater than {end}? {start > end}");
Console.WriteLine($" Does {start} equal {end}? {start == end}");
Console.WriteLine($" The time between {start} and {end} is {end - start}");

/* This example produces the following output:
 * 
 * Time starts at 10:12 AM and ends at 2:00 PM
 *  Is 10:09 AM between the start and end? False
 *  Is 12:12 PM between the start and end? True
 *  Is 10:12 AM less than 2:00 PM? True
 *  Is 10:12 AM greater than 2:00 PM? False
 *  Does 10:12 AM equal 2:00 PM? False
 *  The time between 10:12 AM and 2:00 PM is 03:48:52
*/
Dim startDate = New TimeOnly(10, 12, 1) ' 10:12:01 AM
Dim endDate = New TimeOnly(14, 0, 53) ' 02:00:53 PM

Dim outside = startDate.AddMinutes(-3)
Dim inside = startDate.AddMinutes(120)

Console.WriteLine($"Time starts at {startDate} and ends at {endDate}")
Console.WriteLine($" Is {outside} between the start and end? {outside.IsBetween(startDate, endDate)}")
Console.WriteLine($" Is {inside} between the start and end? {inside.IsBetween(startDate, endDate)}")
Console.WriteLine($" Is {startDate} less than {endDate}? {startDate < endDate}")
Console.WriteLine($" Is {startDate} greater than {endDate}? {startDate > endDate}")
Console.WriteLine($" Does {startDate} equal {endDate}? {startDate = endDate}")
Console.WriteLine($" The time between {startDate} and {endDate} is {endDate - startDate}")

' This example produces the following output
' 
' Time starts at 10:12 AM And ends at 2:00 PM
'  Is 10:09 AM between the start And end? False
'  Is 12:12 PM between the start And end? True
'  Is 10:12 AM less than 2:00 PM? True
'  Is 10:12 AM greater than 2:00 PM? False
'  Does 10:12 AM equal 2:00 PM? False
'  The time between 10:12 AM and 2:00 PM is 03:48:52