If you work in the .NET ecosystem, you may well have heard of Noda Time. Noda Time styles itself as "A better date and time API for .NET" and states on its homepage that
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.
That's an intriguing claim 🤔 Let's explore!
Problems with the standard .NET types
Jon Skeet laid out some problems that Noda Time attempts to solve in a 2011 blog post. The crux is the difficulty of representing time that is local to some place and knowing what instant (point on the global timeline) it corresponds to:
DateTime
s, wrapping up a date and a time, come in three kinds: UTC, local and unspecified. UTC is, obviously, for times in UTC, good for instants but not useful for local times; local is for expressing times local to the executing system, not useful for other locations; there's no way to know which instant an unspecifiedDateTime
corresponds to.DateTimeOffset
s—a date, time and offset from UTC—represent instants in time. However, they're still not great for representing a time in a particular location. Because there's no associated time zone, it's difficult to know what the local time will be six months later: maybe the clocks will go forward or back for the start or end of summer time? The answer depends on the time zone, which we don't know.
In some applications these concepts won't matter, but it is easy to see this being a problem even in some very simple applications. For example, you may want to show the amount of time until a certain event whose start time is expressed in a local time. That such a simple scenario poses difficulties with the standard .NET types is pretty startling.
Solutions from Noda Time
The key thing that Noda Time does to solve the problems listed above is to be aware that the problems exist, and provide data types that have precise definitions. This allows you to create unambiguous data and use operations on it that have well-defined results.
Let's take a look at a few of those types and operations. I'll share some code that you can run in F# interactive.
As setup, this code loads the NuGet package into the script, opens the namespace and creates a DateTimeZone
and a LocalDateTime
whose instant is ambiguous in that time zone (on 30 Oct 2022 in London, clocks went back by 1 hour at 2am, so 1.30am happened twice).
#r "nuget: NodaTime"
open NodaTime
let zone = DateTimeZoneProviders.Tzdb["Europe/London"]
let ambiguousLocalTime = LocalDateTime(2022, 10, 30, 01, 30)
NodaTime provides various ways to create a ZonedDateTime
from a LocalDateTime
and DateTimeZone
. You can specify an offset explicitly (which NodaTime will validate), ask NodaTime to be lenient (choose the first of the possible instants) or ask NodaTime to be strict (throw an exception if there's more than one matching instant).
let afterClocksHaveGoneBack = ZonedDateTime(ambiguousLocalTime, zone, Offset.FromHours(0))
// 2022-10-30T01:30:00 Europe/London (+00)
let beforeClocksHaveGoneBack = ambiguousLocalTime |> zone.AtLeniently
// 2022-10-30T01:30:00 Europe/London (+01)
ambiguousLocalTime |> zone.AtStrictly
// NodaTime.AmbiguousTimeException: ... 'Local time 30/10/2022 01:30:00 is ambiguous in time zone Europe/London'
ZonedDateTime(ambiguousLocalTime, zone, Offset.FromHours(2))
// System.ArgumentException: Offset +02 is invalid for local date and time 30/10/2022 01:30:00 in time zone Europe/London
You can create a duration from two ZoneDateTime
s by "subtracting" one from the other. Because ZonedDateTime
s can be unambiguously mapped to an instant, there's no need to worry that the calculation hasn't taken clock changes into account.
let partyStart = LocalDateTime(2022, 12, 31, 18, 00) |> zone.AtStrictly
let longerDuration = ZonedDateTime.Subtract(partyStart, beforeClocksHaveGoneBack)
printfn $"There are %i{longerDuration.Days} days and %i{longerDuration.Hours} hours until the party starts!"
// There are 62 days and 17 hours until the party starts!
let shorterDuration = partyStart - afterClocksHaveGoneBack
printfn $"There are %i{shorterDuration.Days} days and %i{shorterDuration.Hours} hours until the party starts!"
// There are 62 days and 16 hours until the party starts!
Summary
The standard .NET date and time types struggle to represent a local time in a particular time zone that has an unambiguous associated instant (point on the global timeline). NodaTime solves this problem by providing data types that can do this. Because those data types have precise meanings, NodaTime can offer operations on the data to answer questions which have well-defined results. We saw an example of this using a London time that was ambiguous (because of a clock change at the end of summer time). This helps you avoid bugs that you might not even have known existed when using the standard .NET types. If it's important in your application to have times local to a particular time zone and, for example, to also know what time they represent in UTC, you may want to give NodaTime a try.
I hope that you found this post informative and interesting. I certainly enjoyed experimenting with NodaTime to put the post together 🤓 Thanks to the wonderful NodaTime contributors for providing the .NET community with a great library!