Using DateOnly and TimeOnly in .NET 6

Using DateOnly and TimeOnly in .NET 6

In .NET 6 (preview 4), two long-awaited types have been introduced as part of the core library. DateOnly and TimeOnly allow developers to represent either the date or time portion of a DateTime. These two new types are structs (value types) and may be used when your code deals with date or time concepts independently. Both types can be found in the System namespace. Using these new types may align well with how databases allow similar data to be represented. Specifically, these types align well with the SQL Server date and time data types.

NOTE: To access these types, you’ll need to download and install .NET 6 preview 4 (or newer) and Visual Studio 16.11 (currently in preview).

Using DateOnly in .NET 6

The types are pretty much self-explanatory with regards to what they represent. We can use DateOnly when we need to represent a date without a time component. For example, perhaps we represent someone’s date of birth in our application. In such cases, we rarely need to utilise the time portion of a DateTime, and a standard solution would be to set the time to 00:00:00.000. With DateOnly, we can be more explicit about our intent.

We can construct an instance on DateOnly, passing the year, month and day as arguments:

var date = new DateOnly(2020, 04, 20);

This creates a DateOnly representing the 20th April 2020. Internally, the DateOnly struct uses an integer to track a day number with a valid range of 0, mapped to 1st January 0001, to 3,652,058, mapped to 31st December 9999.

Often, you will start with an existing DateTime and want to create a DateOnly instance from it. To achieve this, we can call the FromDateTime method:

var currentDate = DateOnly.FromDateTime(DateTime.Now);

As with the existing DateTime type, we may also parse a string representing a date into its DateOnly representation using either Parse, which may throw an exception or TryParse, which returns a bool indicating success or failure.

if (DateOnly.TryParse("28/09/1984", new CultureInfo("en-GB"), DateTimeStyles.None, out var result))
{
    Console.WriteLine(result);
}

The above code attempts to parse a date from the first argument, the string representation of the date. Parsing of dates can be affected by culture since different countries and regions interpret dates differently. In this example, I’ve been explicit in providing the en-GB culture to ensure that it correctly parsed from a string using a day/month/year format.

In cases where the DateOnly has been successfully parsed, it is written to the console. Again; culture plays an important role here. In this example, the current culture of the running thread is used to determine the format used. My application thread happens to be running under en-GB, based on my system configuration, so the formatted string appears as:

28/09/1984

To learn more about parsing and formatting, you can view my latest Pluralsight course, String Manipulation in C#: Best Practices.

We may also add days, months or years to a DateOnly instance, resulting in a new instance with the adjusted date.

var newDate = date.AddDays(1).AddMonths(1).AddYears(1);

Using TimeOnly in .NET 6

The TimeOnly struct is used to represent a time that is independent of the date. For example, imagine creating an alarm clock app that lets the user create a reoccurring alarm. In this situation, we want to store the time of day when the alarm should sound, but the date is irrelevant.

The TimeOnly type has several constructor overloads. The more common ones that I expect most developers will use allow us to create a date accepting either the hour and minute for the time, the hour, minute and second, or the hour, minute, second and millisecond.

public TimeOnly(int hour, int minute)
public TimeOnly(int hour, int minute, int second)
public TimeOnly(int hour, int minute, int second, int millisecond)

For example, to represent 10:30am in the morning, we can create the following TimeOnly instance.

var startTime = new TimeOnly(10, 30);

The hour portion is expected to be provided using the 24-hour clock format, where 1pm is 13 hours.

Internally, TimeOnly stores a long which represents the number of ticks (100 nanosecond intervals) that have elapsed since midnight by the defined time. For example, 1am is 1 hour into the day and therefore 36,000,000,000 ticks since midnight (00:00:00.0000000). This implementation detail is not essential for general use, although we can also construct a TimeOnly by providing the ticks as an argument.

public TimeOnly(long ticks);

Having defined a start time above, let’s define an end time of 5pm using another TimeOnly instance.

var endTime = new TimeOnly(17, 00, 00);

We can now perform mathematic operations on these TimeOnly instances, such as calculating the difference.

var diff = endTime - startTime;

The return type of this operation is a TimeSpan which we can then use to write the number of hours between the two times.

Console.WriteLine($"Hours: {diff.TotalHours}");
// Output = Hours: 6.5

Another check we can perform is to identify whether a particular TimeOnly falls within a time window. For example, let’s say we want to check if the current time is between the start and end times we have already defined. Just as with DateOnly, we can convert from an existing DateTime into a TimeOnly using the FromDateTime static method.

var currentTime = TimeOnly.FromDateTime(DateTime.Now);
var isBetween = currentTime.IsBetween(startTime, endTime);
Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end");

The above code will now write to console whether the current time falls between 10:30 (10:30am) and 17:00 (5pm).

The IsBetween method accepts normal ranges such as the one we used in the prior example as well as ranges that span midnight such as 22:00-02:00.

var startTime = new TimeOnly(22, 00);
var endTime = new TimeOnly(02, 00);
var now = new TimeOnly(23, 25);
 
var isBetween = now.IsBetween(startTime, endTime);
Console.WriteLine($"Current time {(isBetween ? "is" : "is not")} between start and end"); 
// Output = Current time is between start and end

TimeOnly also includes operators to compare times using a circular clock.

var startTime = new TimeOnly(08, 00);
var endTime = new TimeOnly(09, 00);
 
Console.WriteLine($"{startTime < endTime}");
// Output = True

This code checks if 8am is earlier than 9am, which clearly it is!

Summary

That concludes our early look at these two new types, expected to arrive in .NET 6, preview 4. I felt it worth highlighting that these types exist because such changes are easy to miss when we’ve been used to defaulting to DateTime/DateTimeOffset in the past. If you have data that requires either a date or time to be represented independently, then DateOnly and TimeOnly are worth considering for new applications targetting .NET 6.


Have you enjoyed this post and found it useful? If so, please consider supporting me:

Buy me a coffeeBuy me a coffee Donate with PayPal

Steve Gordon

Steve Gordon is a Pluralsight author, 6x Microsoft MVP, and a .NET engineer at Elastic where he maintains the .NET APM agent and related libraries. Steve is passionate about community and all things .NET related, having worked with ASP.NET for over 21 years. Steve enjoys sharing his knowledge through his blog, in videos and by presenting talks at user groups and conferences. Steve is excited to participate in the active .NET community and founded .NET South East, a .NET Meetup group based in Brighton. He enjoys contributing to and maintaining OSS projects. You can find Steve on most social media platforms as @stevejgordon