Avoiding bad local dates

Rowan 🤖
Technical Credit 💸
6 min readDec 16, 2017

This post is illustrated in .NET (C#), but the problems and solutions are applicable to many other languages.

Most applications will require the use of a data type that represents a moment in time. On the surface of it, dates and times are a basic concept that we all grasp at a young age, so plugging them into our application shouldn’t be that difficult. Using the DateTime object in C# is straight forward and it is common to see DateTime.Now, DateTime.Today, or new DateTime(2018, 1, 1) sprinkled throughout the code base. Unfortunately those innocent looking sprinkles can often be the cause of bugs, confusion and limitations.

You may never encounter any issues with the aforementioned code if your app is hosted and only used by people in the same time zone, if it never does anything during a daylight savings crossover or if it has limited test coverage. But for me it presents the following challenges.

The future is now the past

DateTime.Now is always going to return the current time. If a method uses this internally for a calculation, then it cannot be a pure function as subsequent calls will have different values for Now and it will therefore produce different results. This also makes the methods impossible (or at least very hard) to test.

One solution to limit the usage of DateTime.Now would be to have your methods take the “now” date as a parameter and push DateTime.Now as high up in the call chain as possible, this would allow you to write pure functions that can easily be tested. You could then take it a step further and create an IDateTimeNow interface that gets injected into your objects. You can create an implementation that just wraps DateTime.Now and another one for testing that lets you provide what the value should be.

But even with this solution we still need to think about what “now” actually means and other pitfalls when using it.

Everyone thinks they’re a local

private DateTime _saleDate = new DateTime(2018, 1, 1);
public DateTime SaleDate => _saleDate;

What time zone is _saleDate intended to be in? It will be DateTimeKind.Unspecified in this example. We can make it a local date by changing it to DateTime(2017, 12, 25, 0,0,0,0, DateTimeKind.Local). But unfortunately the date time kind is not used in comparison or arithmetic operations , so we cannot ensure that other code doesn’t get unexpected results by comparing the public property SalesDate against UTC date times. A simple way to state our intentions would be to add a Local or Utc suffix to our property name. This makes SaleDateLocal less ambiguous, but it still doesn’t prevent it from being used incorrectly.

Taking a step back, what does local even mean? Using DateTimeKind.Local means local in the time zone of the system that the code is running on. It doesn’t contain any information about what the actual time zone is and you can’t tell your server in Sydney that local actually means New Zealand daylight time. So you need to think about what will happen if a user takes the app on a plane and starts using it in another country? Will we have problems if we decide to migrate our services to another cloud host in another time zone (from experience, yes, yes you will have problems)?

Thoughts: Avoid using DateTime unless you have a specific need for it e.g. I sometimes find it useful to use DateTimeKind.Unspecified for more abstract dates where the UTC or local date doesn’t really matter e.g. birthdays.

DateTimeOffset to the rescue?

These uses for DateTimeOffset values are much more common than those for DateTime values. As a result, DateTimeOffset should be considered the default date and time type for application development.
-Microsoft

DateTimeOffset has been around forever (.NET 2) and was introduced to fix the problems with DateTime, so this should be a solved problem and we can stop talking about DateTime. The problem however is that it only stores the date’s offset from UTC and unfortunately nothing about the time zone:

Although a DateTimeOffset value includes an offset, it is not a fully time zone-aware data structure. While an offset from UTC is one characteristic of a time zone, it does not unambiguously identify a time zone. Not only do multiple time zones share the same offset from UTC, but the offset of a single time zone changes if it observes daylight saving time. This means that, as soon as a DateTimeOffset value is disassociated from its time zone, it can no longer be unambiguously linked back to its original time zone.

I had fun with this once while working in Singapore with dates coming from both Singapore and Perth. Both places are UTC+08:00 throughout the entire year, so DateTimeOffset seemed like a good candidate. But a fun fact for when dealing with historical dates is that prior to 29 March 2009 Perth did observe daylight savings time, so some dates form Perth could actually be UTC+09:00. Obviously this presents some interesting challenges.

You are also likely to run into similar problems with DateTimeOffset if you are working with local dates and your servers are located in different time zones. The remote servers won’t know about your local time zone rules as the UTC offset does not provide enough information.

Thoughts: DateTimeOffset eliminates the problems with DateTimeKind as it uses UTC values for comparisons and arithmetic, so it has no problem working with values with different offsets. In most cases it should be used instead of DateTime. However, it’s lack of time zone information may make it unsuitable in your application.

Getting into the zone

Hopefully it’s clear now that knowing the time zone is a critical part of writing an application that is accurate and independent of its hosting environment. A simple solution could be to make your own data type which stores the date information as well as the time zone:

Source: Jon Skeet

This solution works reasonably well, but it still has the potential for mix-ups as it has DateTime as inputs and outputs. You could extend it to throw exceptions if the constructor gets passed in a date that isn’t of kind Unspecified and also add some methods for arithmetic and comparisons directly against DateTimeWithZone. But a more robust solution would be to just bite the bullet and use Noda Time.

Noda Time is an alternative date and time API for .NET. It helps you to think about your data more clearly, and express operations on that data more precisely.

Using Noda Time has many benefits, but for me the greatest is that it forces you to think about what dates in your application actually mean e.g. should I use ZonedDateTime or LocalDateTime? Once you make that decision, then it is also clear to consumers of your APIs and the ambiguity around dates disappears. It also supports testing in a similar way to IDateTimeNow from earlier.

Conclusion

Use Noda Time or a similar implementation that gives you the ability to specify what time zone (if any) you are operating in. It might be a bit of extra work and it requires self-control to completely abandon the built-in .NET date types, but it will help to reduce the chance of making errors when dealing with dates. Even if there is only a small chance that your application will be one day run in a different time zone, is it it’s better to future proof it now that have to do a painful migration it in the future.

Some tips

  • To reiterate, don’t directly use DateTime.Now, DateTimeOffset.UtcNow, or any other test blocking equivalents.
  • Consider having some linting rules to ensure that DateTime and DateTime.Now are not used.
  • Develop locally against services that are hosted on systems with a UTC system time (or with another wildly different time zone). This will help catch any errors with code that makes assumptions around what the local time zone is.
  • Dealing with daylight saving time is the worst. Write tests around DST cross overs to ensure that your application can handle this transition.
  • Make sure that any dates you store in a database have enough information about them to be useful. Also think about how the data might get used. For example, if the dates are just stored as a UTC value, can people writing reports still easily write their yearly sales or performance reports that get aggregated by month?

--

--