Struggling with Dates in .NET

Struggling with Dates in .NET

Avoid common pitfalls when using Date and Time data types.

 


Introduction

While writing applications I’ve encountered many unforeseen errors and strange behavior when using Date and Time data types. Looking back, it was a lack of understanding of how Date and Time worked in the .NET framework that resulted in these time-consuming missteps.

The goal of this blog is to guide beginning developers and analysts around possible pitfalls and mistakes when working with Date and Time data types. I try to highlight some best practices on how to handle these within applications and how to share them across an organization. By doing so, I use examples to illustrate my points, hoping to improve your understanding of date and time in .NET.

The examples will all be in C#.NET. However, the general concepts should apply to any .NET applications. This article also assumes that the reader is familiar with terms and concepts such as; time zones, daylight saving time, GMT, …

 

Being right on time

What does Date and Time represent? The complexity of time lies in the fact that … well it depends. No really, it depends on your point of view, the context, where you are, etc.

Example 1:

In Belgium we use Romance Standard Time and it is 19/10/2018 13:36:00 as I’m writing this blog. All the way across the world, in New Zealand it is 20/10/2018 00:36:00. We are comparing the “exact same moment in time”, however the hours and minutes are different and, in this case, even the dates! So how are these values even comparable?

The important thing to note here is that a date and time is more than just a year + month + day + hours + minutes + seconds + milliseconds + …. As we can see, in the previous example, context such as time-zone or calendar will influence the outcome a DateTime variable in C#.

Here’s a shocker, every day does not contain 24h!...  In Time Zones that use daylight saving hours, we have days with 23 or 25 hours.

Example 2: 

In Belgium, on 28/10/2018 03:00 we will revert to 02:00 as Daylight Saving hour gets reverted. We therefore have 2 moments in time where it is 28/10/2018 02:00. Depending on which point we convert to New Zealand Standard Time, they will be one hour apart*.

*New Zealand already reverted form Daylight Saving time om September 30th

The Standard

We use UTC (Coordinated Universal Time), as it is the prime standard around the world to regulate clocks and time. UTC does not use daylight Saving time and can be converted through an offset and calendar to the appropriate local time. In example 1 for example both DateTimes would result to 19/10/2018 11:36:00Z when converted to UTC. Reading the .NET documentation about DateTime we can see that, “under the hood” Microsoft models DateTime as a numeric value that represents a number of ticks (100-nanosecond units) ranging from 00:00:00 (midnight), January 1, 0001 through 23:59:59, December 31, 9999. This is reflected in the public DateTime(long ticks) constructor of the DateTime struct.

This means that UTC time reflects the number of ticks represented in the Gregorian Calendar as a DateTime in C#. By converting that same number of ticks using a different calendar and/or time zone we can calculate the appropriate Local Time.

Manipulating time in C#

So, Dates and Time are complex and they depend on the context. However UTC can help us to share datetime in a meaningful manner across different time zones. Sounds easy, let’s try this in code!

I’ve created a simple Console Application in C# and you can copy paste the code if you want to play around with this.

Example 3a:

Console.WriteLine("Example 3a");

// Step 1: We instantiate a Datetime struct and set it to the local system DateTime

DateTime now = DateTime.Now;

// Step 2: We write the time, the date and the time converted to UTC

Console.WriteLine($"Now -> Time:{now} \t Date:{now.Date.ToShortDateString()} \t Converted2UTC:{now.ToUniversalTime()}");  

// Step 3: We convert our dateTime to "New Zealand Standard Time"

DateTime nowInNewZealand = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.FindSystemTimeZoneById("New Zealand Standard Time"));

// Step 4: We write the time, the date and the time converted to UTC from our converted time

Console.WriteLine($"Now In New Zealand -> Time:{nowInNewZealand} \t Date:{nowInNewZealand.Date.ToShortDateString()} \t Converted2UTC:{nowInNewZealand.ToUniversalTime()}");

// Output:

// Example 3a

// Now                -> Time: 19/10/2018 14:18:23 Date: 19/10/2018   Converted2UTC:19/10/2018 12:18:23

// Now In New Zealand -> Time: 20/10/2018 1:18:23  Date: 20/10/2018   Converted2UTC:19/10/2018 23:18:23 

Hey wait…, shouldn’t both times be exactly the same when converted to UTC? Let’s take a closer look at what is happening here.

Example 3b:

Console.WriteLine("Example 3b");

// Step 1: we instantiate a Datetime struct and set it to the local system DateTime

DateTime now = DateTime.Now;

// Step 2: We write the time, the date and the DateTime.Kind.

Console.WriteLine($"Now -> Kind:{now.Kind}");

// Step 3: We convert our dateTime to "New Zealand Standard Time"

DateTime nowInNewZealand = TimeZoneInfo.ConvertTime(now, TimeZoneInfo.FindSystemTimeZoneById("New Zealand Standard Time"));

// Step 4: We write the time, the date and the DateTime.Kind from our converted time.

Console.WriteLine($"Now In New Zealand -> Kind:{nowInNewZealand.Kind}");

// Output:

// Example 3b

// Now                 -> Kind:Local

// Now In New Zealand  -> Kind:Unspecified

As we can see the DateTime variable nowInNewZealand is of Kind “unspecified”, as a result it assumes it’s a local time and uses my local settings (UTC+2) to convert it to UTC… See here for more about DateTimeKind.

Example 4:

Console.WriteLine("Example 4");

// Step 1: we instantiate a Datetime struct that represents Daylight Saving hours

DateTime time1 = DateTime.Parse("28/10/2018 01:30:00", CultureInfo.CurrentCulture,DateTimeStyles.AssumeLocal);

// Step 2: We write the time and the time +2 hours

Console.WriteLine($"time -> {time1} \t time + 2 hours: {time1.AddHours(2)}");

// Step 3: We convert the local time to UTC do the same manipulations then reconvert it to Local time

DateTime time1UTC = time1.ToUniversalTime();

Console.WriteLine($"utc  -> {time1UTC} \t utc + 2 hours: {time1UTC.AddHours(2)} \t back to Local {time1UTC.AddHours(2).ToLocalTime()}");

// Output

// Example 4

// time-> 28 / 10 / 2018 1:30:00       time + 2 hours: 28 / 10 / 2018 3:30:00

// utc-> 27 / 10 / 2018 23:30:00       utc  + 2 hours: 28 / 10 / 2018 1:30:00       back to Local 28 / 10 / 2018 2:30:00

Example 4 illustrates what is recommended in this MSDN article. When manipulating DateTime variables, it is better to use UTC time. In this scenario adding to the local time does not consider the change in Daylight Saving time as explained in Example 2.

 

Saving Time

Lessons learned

But, what does this mean for budding developers creating their own applications? How to we solve or avoid these issues. Personally, I am a big fan of the KISS principle.

  1. Context matters: The context in which the time is recorded matters, when converting time, we must know the time zone of the Date Time we have. When sharing Date Time data;
    • be transparent about the source.
    • make clear rules about the format.

Adding the offset is good, but agreeing that every Date Time needs to be send in UTC format is perfectly fine too.
I’ve seen applications use both local and utc time in their data model. For example, a Sale object with both a UtcSaleDate and a LocalSaleDate property.

  1. Code to your needs: Don’t make it overly complicated. Reading articles about this subject, it might seem that converting everything to UTC is best practice. While it is certainly much safer, when you are manipulating time, adding and removing. It can pose issues when done blindly.

For example, a small local application records all sales from a shop and then reports on the sales at the end of the day. Converting the DateTime of each record to UTC could impact the date of the record and therefore the daily report in which the sale record is counted. A Belgian Night shop recording all it’s sales as UTC, will have the sales occurring first 1 or 2 hours* after midnight recorded on the previous daily report. (depending on the offset)

Consider the impact, which brings us to the next point.

  1. Think about what the DateTime represents: Is the local value more important than the universal value? In previous point we could see that the local DateTime is more meaningful than the UTC DateTime for what we want to achieve. In a Global application that records shipments from one country to another, sender and receiver might only care about when the times of a shipment in their respective local time. UTC DateTime converted to the user’s local time in the front end of the application will be the best solution to accommodate all.

DateTime is a struct not a class: See ByVal vs ByRef this is an important enough distinction to warrant a mention. The discussion of this subject would lead us too far.

C# has a whole framework built around conversions, and for those interested in digging deeper into the subject I recommend reading Jon Skeet's Blog about Noda Time.

Date and time in SQL

What about SQL? In SQL we don’t always have the same functionality as C#, there are however best practices to consider. Starting from SQL 2008, the number of DateTime data types have expanded to accommodate for the needs of global applications. I recommend taking a closer look at datetimeoffset.

 


Links and references

MSDN article on Best Practices

https://msdn.microsoft.com/en-us/library/ms973825.aspx

.NET Datetime

https://docs.microsoft.com/en-us/dotnet/api/system.datetime?view=netframework-4.7.2

https://docs.microsoft.com/en-us/dotnet/api/system.datetimekind?view=netframework-4.7.2

UTC (Coordinated Universal Time)

https://www.timeanddate.com/time/change/belgium

https://www.timeanddate.com/worldclock/timezone/utc

https://en.wikipedia.org/wiki/Coordinated_Universal_Time

KISS principle

https://en.wikipedia.org/wiki/KISS_principle

Noda Time API by Jon Skeet

https://codeblog.jonskeet.uk/category/nodatime/

https://nodatime.org/

SQL
https://www.mssqltips.com/sqlservertip/5206/sql-server-datetime-best-practices/

ByVal vs ByRef
https://www.c-sharpcorner.com/article/difference-between-passing-reference-types-by-ref-and-by-val/