.NET 6 is currently scheduled to be released-to-production on 9th Nov 2021. So, to get ready for that, we're kicking off a short new series that shows some of the smaller, but cool, things we can do in .NET 6 with some new framework features.

Let's kick off this new series by discussing a long-awaited feature: DateOnly and TimeOnly!

A large amount of analog clocks, set to various times, some using numbers and some using Roman numerals.
We're gonna need a bigger truck. Photo by Jon Tyson / Unsplash

Current Implementation

To represent a date without a time, we are forced to use the DateTime class, which has several overloads that help us get the date.

var myDate = new DateTime(2021, 9, 23);
var datePart = DateTime.Now.Date;

However, these overloads and properties will always return type DateTime, which will include a time, regardless of whether or not we need it.

Similarly, before .NET 6 there was no real way to represent a time of day without a date. We could use the TimeSpan class to represent time elapsed (e.g. 7 hours, 21 minutes and 9 seconds), but there was no way to represent a specific time of day (e.g. 7:21:09 AM) without using DateTime.

New Implementation

.NET 6 introduces the new DateOnly and TimeOnly structs, which allow us to represent dates without time of day, or time of day without a date.

DateOnly sep10th = new DateOnly(2021, 9, 10);
var dec31st = new DateOnly(1999, 12, 31);
DateOnly aug3rd = new(1988, 8, 3);

TimeOnly nineThirtyPM = new TimeOnly(21, 30); //21:30, or 9:30 PM
TimeOnly fourTwentyThreeAM = new(4, 23, 19); //04:23:19, or 4:23:19 AM

We can create instances of DateOnly or TimeOnly from corresponding DateTime instances using a special new overload, FromDateTime().

DateTime dateOnlyExample = new DateTime(2004, 5, 19, 4, 45, 30);
DateOnly date3 = DateOnly.FromDateTime(dateOnlyExample); //May 19th, 2004

DateTime sevenFortyFiveDT = new DateTime(2011, 11, 11, 7, 45, 00);
TimeOnly sevenFortyFive = TimeOnly.FromDateTime(sevenFortyFiveDT); //07:45 AM

DateOnly Details

Automatically Determines Local Culture

The DateOnly struct automatically determines the local culture, and uses that to output the date.

DateOnly cultureExample = new DateOnly(2004, 5, 19); //May 19 2004
Console.WriteLine(cultureExample);
//American: 5/19/2004, European: 19/5/2004, Universal: 2004-05-19

AddDays(), AddMonths(), AddYears()

Similarly to DateTime, we can use overload methods like AddDays() to modify the value of a DateOnly instance.

DateOnly addTimeExample = new DateOnly(2004, 5, 19); //May 19 2004
addTimeExample = addTimeExample.AddYears(2).AddMonths(2).AddDays(5);
//Jul 24th, 2006

Parsing from Strings

We can also parse DateOnly instances from strings using TryParse(), just like we did with DateTime.

if(DateOnly.TryParse("09/21/2013", out DateOnly result))
    Console.WriteLine(result); 
    //American: 9/21/2013, European: 21/9/2013, Universal: 2013-09-21

Stores Value as Integer

Internally, DateOnly stores its value as an integer, where 0 is January 1st, 0001. We can get that integer value from a DateOnly instance, and use the method DateOnly.FromDayNumber to convert that integer to a DateOnly instance.

DateOnly integerTest = new(2019, 7, 1); //July 1st 2019
int dayNumber = integerTest.DayNumber;
DateOnly integerResult = DateOnly.FromDayNumber(dayNumber); //July 1st 2019

TimeOnly Details

Remember that TimeOnly represents a time of day, not time elapsed (the latter is what TimeSpan is used for).

Stores Value as Ticks from Midnight

Similarly to DateOnly, TimeOnly internally stores its value as a long, which are the ticks (100 nanoseconds) since 00:00:00 (midnight). We can use the ticks to create a new TimeOnly value.

TimeOnly sixTen = new TimeOnly(6, 10);
long ticks = sixTen.Ticks;
TimeOnly sixTenAgain = new TimeOnly(ticks);

Math Operations result in TimeSpans

We can perform math operations on instances of TimeOnly, which give us TimeSpan results.

var afternoon = new TimeOnly(15, 15); //3:15 PM
var morning = new TimeOnly(9, 10); //9:10 AN
TimeSpan difference = afternoon - morning; //6 hours 5 minutes

Checking for Value in a Range

We can check if a TimeOnly instance falls in a given range using the IsBetween() method.

var now = TimeOnly.FromDateTime(DateTime.Now);
var nineAM = new TimeOnly(9, 0);
var fivePM = new TimeOnly(17, 0);

if(now.IsBetween(nineAM, fivePM))
    Console.WriteLine("Work time!");
else
    Console.WriteLine("Anything other than work time!");

We can even check for a value in range that goes across midnight.

var tenPM = new TimeOnly(22, 0);
var twoAM = new TimeOnly(2, 0);

var midnight = new TimeOnly(0); //0 ticks == midnight

if(midnight.IsBetween(tenPM, twoAM))
    Console.WriteLine("It's getting late...");

Comparison Operations

We can use comparison operators (such as < and >) to compare two TimeOnly instances.

TimeOnly noon = new(12, 0);
if (now < noon)
    Console.WriteLine("Good Morning!");

Other Considerations

If you're wondering why these classes were not simply called Date and Time, apparently that has to do with several considerations. Partly, it is because VB has a Date object and .NET needs to be compatible with VB. You can read more in the following blog post:

Date, Time, and Time Zone Enhancements in .NET 6
.NET 6 Preview 4 introduces DateOnly and TimeOnly structs and improves time zone support.

Demo Project

As with all posts in this series, there is a demo project you can check out over on GitHub that shows examples of DateOnly, TimeOnly, and the various things we can do with them. Check it out!

GitHub - exceptionnotfound/NET6BiteSizeDemos
Contribute to exceptionnotfound/NET6BiteSizeDemos development by creating an account on GitHub.

Happy Coding!