Handling dates can be confusing, especially when developing an application with time-sensitive data and users across different time zones. That’s where Noda Time comes in. In this article, we will learn about handling dates using Noda Time in .NET and streamline our experience working with dates.

To download the source code for this article, you can visit our GitHub repository.

Let’s dive in.

What Is Noda Time?

Noda Time is an open-source library that simplifies working with dates in .NET. Using the library, we don’t need to worry about the inner workings of dates and time. The library takes care of that for us. We can use the library to track events across various time zones, calculate durations, or manage dates.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

Why Does Noda Time Exist?

Noda Time extends the .NET built-in date and time API and also addresses its inadequacies. Let’s look into some of the reasons why Noda Time was created and why it’s relevant to us today.

Clear Separation of Concerns

It supports multiple types for handling date and time. Noda Time has Instant, LocalDateTime and LocalDate types, just to mention a few, each of which gives us a clear separation of concerns. 

The Instant type represents a point in time in the UTC time zone, LocalDate represents just a date without a time or time zone, and ZonedDateTime represents a date and time in a specific time zone. With these types, we ensure type safety in our applications in turn reducing errors.

This clear separation helps avoid common pitfalls and mistakes that can arise when using .NET’s DateTime type, which can ambiguously represent both local and UTC times.

Immutable Node Time Types

All Noda Time types are immutable, which means their values cannot be changed after they are created. This makes them safer to use in multithreaded applications and reduces potential bugs related to unexpected state changes.

Fluent API for Arithmetic Operations

Noda Time provides a fluent API for date and time arithmetic, making operations like adding or subtracting periods to dates more intuitive.

Support for Interval and Period

Noda Time offers Interval and Period types that represent time spans and can be used to express differences between dates or times in a human-readable format.

Noda Time Formatting and Parsing

The library offers improved formatting and parsing options, allowing for more control over how dates and times are represented as strings.

Avoid Ambiguities

With Noda Time, we can avoid ambiguities related to daylight-saving time transitions. For instance, when the clocks go back, a local time can occur twice. Noda Time allows us to handle such scenarios explicitly.

Noda Time has an active community, and its development is led by Jon Skeet, a well-known figure in the .NET community. This ensures that the library is regularly updated and improved.

With that out of the way, let’s dig deeper into the library.

Project Setup

For this article, let’s create a console application using Visual Studio or by running the command:

dotnet new console

After that, let’s install the Noda Time Nuget package using the .NET CLI command:

dotnet add package NodaTime

Alternatively, we could also use Package Manager to install the package.

Having set up the project, let’s dive deeper into the library’s main features.

Instants

An instant is a Noda Time type that represents time not tied to a specific timezone or calendar system. It represents the number of nanoseconds since the Unix epoch, that is January 1, 1970. 

The most appropriate use case for the Instant type is when we need to know the exact time when an event happened, for instance, writing logs and audit trails.

We can use the SystemClock.Instance.GetCurrentInstant() method to create an instant: 

var currentInstant = SystemClock.Instance.GetCurrentInstant();

The GetCurrentInstant() method is a method of Noda Time’s IClock interface which returns the current Instant.

It’s worth noting that we’re only using the SystemClock.Instance.GetCurrentInstant() method directly for demonstration purposes. In production-ready code, we should depend on abstractions rather than implementations, SystemClock is the implementation and IClock is the abstraction. This way, we respect the dependency inversion principle.

Alternatively, we could also use the Instant.FromUtc method to create an instant:

var instant = Instant.FromUtc(2023, 8, 31, 20, 00);

This method returns an Instant corresponding to 31st August 2023, 20:00 UTC time. 

Other than that, we could also use the Instant.FromUnixTimeTicks() method to create a Noda Time Instant:

var instant = Instant.FromUnixTimeTicks(1693153840);

This method creates an instance of Instant from ticks since the Unix epoch time.

These are but a few methods we could use to create a Noda Time Instant.

Also, we can convert the Instant type to other time representations using inbuilt methods. One such method is the ToUnixTimeTicks() method:

var epochTime = 1693153840;

var instant = Instant.FromUnixTimeTicks(epochTime);
var unixTimeTicks = instant.ToUnixTimeTicks();

First, we initialize the variable epochTime. Then, using the FromUnixTimeTicks(epochTime) method, we create a variable instant of type Instant.

Finally, we convert the Instant back to the Unix epoch. At this point, comparing the two values, we find that they are equal.

The Instant type has additional methods that we could work with to manipulate dates in C#. 

Duration in Noda Time

We use the Duration type to represent a fixed length of time that’s not tied to a specific calendar system. With this type, we measure time in hours, minutes, seconds, and milliseconds.

The appropriate use case for this would be when we want to know how long an activity took, for instance, application usage time.

To instantiate a type Duration, Noda Time has factory methods to help us achieve that. Let’s look at a few of them.

Using FromTicks() method

We use this method to create durations equivalent to a specific number of ticks. In computing, a tick is the smallest unit of measuring time.

To create a duration, we call this method by passing the number of ticks as a parameter: 

var ticks = 600000000;

var duration = Duration.FromTicks(ticks);

First, we declare the ticks variable. Then, we pass the variable to the FromTicks() method, which returns the Duration value representing 1 minute.

Using FromHours() method

Given a number of hours, we could also instantiate a duration:

var hours = 12;

var duration = Duration.FromHours(hours);

This method returns a duration that’s equal to the number of hours specified.

These two are some of the main methods of instantiating a duration. 

It’s also worth noting that we can also perform mathematical computations on the duration type to manipulate durations.

We can add durations:

var durationInDays = Duration.FromDays(1);
var durationInHours = Duration.FromHours(3);

var sumOfDurations = durationInDays + durationInHours;

First, we get the duration representing one day. Then, we get the duration representing 3 hours. Adding the two durations, we get 1 day and 3 hours.

Also, we can subtract durations:

var firstDuration = Duration.FromHours(3);
var secondDuration = Duration.FromHours(2);

var difference = firstDuration - secondDuration;

First, we get a duration representing 3 hours. Then we get a duration representing 2 hours. Subtracting the two durations, the difference is 1 hour.

These are just two of the arithmetic operations. However, they are not the only ones. We could also multiply, divide, or compare duration.

LocalTime in Noda Time

LocalTime represents a specific time of day, like 3:30 PM or 9:15 AM. It’s not tied to any specific time zone or calendar system. If we need finer details, the LocalTime type gives us precise details down to milliseconds and even nanoseconds.

We can easily create a LocalTime instance for a specific time

var localTime = new LocalTime(10, 30);

Here, we create a local time of 10:30 AM. This could come in handy when we want to schedule a task at a specific time.

Also, we can take LocalTime and convert it into a string:

var time = new LocalTime(10, 30);
var pattern = LocalTimePattern.CreateWithInvariantCulture("HH:mm");

var formattedTime = pattern.Format(time);

We first instantiate 10:30 AM local time, then we instantiate a formatting pattern in which we’d like to format the local time value. In our case, we’re using InvariantCulture. Finally, we call the Format() method which returns a formatted local time string.

Parsing String Values to Time

We can also read and parse a string to time. This comes in handy when reading user input values where time is passed as a string:

var input = "11:00:46";
var pattern = LocalTimePattern.ExtendedIso;

var parsedTime = pattern.Parse(input);
var time = parsedTime.Value;

We are reading the string input and using the ExtendedIso formatting pattern. The pattern formats and parses dates and times using the ISO-8601 format, but with extended precision. It supports up to 9 decimal places, meaning it has very high accuracy. 

When we use the Parse() method to parse an input string, we get a ParseResult instance. To get the actual value, we have to get the Value property.

Comparing Time in Noda Time

When scheduling or sorting events, we would want to know the order in which they will occur. If each event has a fixed time, we can use LocalTime factory methods to compare the times. The CompareTo() method comes in handy for such comparisons:

var eventOneTime = new LocalTime(7, 30);
var eventTwoTime = new LocalTime(12, 0);
var eventThreeTime = new LocalTime(12, 0);

We declare different local times for three events that are supposed to happen. Now, using the CompareTo() method to compare the times of the three events:

var eventOneResult = eventOneTime.CompareTo(eventTwoTime);
var eventTwoResult = eventTwoTime.CompareTo(eventOneTime);
var eventThreeResult = eventTwoTime.CompareTo(eventThreeTime);

Assert.Equal(-1, eventOneResult);
Assert.Equal(1, eventTwoResult);
Assert.Equal(0, eventThreeResult);

The CompareTo() method call is sensitive to the initial time and the parameter passed to it. If we compare a later time to a more recent time, we get 1. Otherwise, we get -1. If the two values we’re comparing are equal, we get 0.

In this case, eventTwoTime > eventOneTime. So eventOneTime.CompareTo(eventTwoTime) returns -1. Similarly, the  eventTwoTime.CompareTo(eventOneTime) method call returns 1 because eventOneTime < eventTwoTime. Finally, eventTwoTime == eventThreeTime. So the eventTwoTime.CompareTo(eventThreeTime) method call returns 0.

DateTimeZone

DateTimeZone is a fundamental building block in Noda Time for handling time zones. It helps to accurately map UTC instants to local time. For this reason, it’s the perfect type when working with time across different time zones.

To create an instance of DateTimeZone, we use DateTimeZoneProvider specifying the time zone:

var timeZoneId = "America/New_York";

var newYorkTimeZone = DateTimeZoneProviders.Tzdb[timeZoneId];

We instantiate the DateTimeZone using a specific time zone ID from the time zone database (Tzdb). The timezone ID uniquely identifies the different time zones.

ZoneDateTime

We use ZoneDateTime to represent a precise moment in time by combining date and time details as well as time zone details. By using ZoneDateTime, we avoid the ambiguity that might arise when working with dates.

To create ZoneDateTime, we combine LocalDateTime and a DateTimeZone:

var localDateTime = new LocalDateTime(2023, 8, 31, 15, 30);
var timezone = DateTimeZoneProviders.Tzdb["Africa/Cairo"];

var zoneDateTime = localDateTime.InZoneLeniently(timezone);

First, we create a LocalDateTime instance. Then, we get a timezone, in this case the Africa/Cairo timezone. Finally, we combine both the date and timezone to get the ZoneDateTime value.

Having the correct time zones ensures our applications have the correct current time.

Noda Time Extension Methods

Noda Time has extension methods that ship with the library that enable us to work with the core types. One such method is the ToInstant() method which converts DateTime to Instant type:

var dateTime = new DateTime(2023, 9, 15, 9, 30, 50, DateTimeKind.Utc);
var expectedInstant = Instant.FromUtc(2023, 9, 15, 9, 30, 50);

var instant = dateTime.ToInstant();

This method takes a DateTime as a parameter. Then using the ToInstant() method, it converts the DateTime passed as a parameter to Instant.

Also, we can convert the values of TimeSpan type to Duration type using the ToDuration() extension method:

var timeSpan = TimeSpan.FromHours(3);
var expectedDuration = Duration.FromHours(3);

var duration = timeSpan.ToDuration();

Assert.Equal(expectedDuration, duration);

Comparing the two Duration values, we find that they are equal.

We can also do mathematical computation on types, for instance adding days to LocalDate using the PlusDays() method:

var currentDate = new LocalDate(2023, 9, 15);
var expectedDate = new LocalDate(2023, 9, 16);

var futureDate = currentDate.PlusDays(1);

Assert.Equal(expectedDate, futureDate);

This method takes the number of days passed as a parameter and adds it to LocalDate. Then, we check whether the two dates are equal. Executing this test, we get a pass result.

Also, we can use the Period.Between() extension method to find the difference between two dates:

var startDate = new LocalDate(1985, 12, 12);

var endDate = new DateTime(2023, 9, 15, 9, 30, 50);

var finalDate = LocalDate.FromDateTime(endDate);
var difference = Period.Between(startDate, finalDate, PeriodUnits.Years);

This method takes three parameters: two DateTime values and the PeriodUnits parameter, that specifies the unit of time, for instance, years or months. Then, it computes and returns the Period between the two dates.

NodaTime.Text

The NodaTime.Text namespace contains types that enable us to format and parse date and time values to and from text. The namespace contains pattern classes that define the formatting and parsing patterns for dates. 

We can specify the different patterns for parsing the inputs:

var dateTimeText = "2023-09-17 14:30:00";
var localDateTimePattern = LocalDateTimePattern
                           .CreateWithInvariantCulture("yyyy-MM-dd HH:mm:ss");

var parsedDateTime = localDateTimePattern.Parse(dateTimeText).Value;

Here, we first create the LocalDateTimePattern variable which we use to parse the DateTime string. Then, we proceed to parse the dateTimeText string using the pattern. The return type of this operation is LocalDateTime.

Similarly, using NodaTime.Text patterns to convert Noda Time types to string representations:

var dateToFormat = new LocalDate(2023, 9, 17);
var localDatePattern = LocalDatePattern
                       .CreateWithInvariantCulture("yyyy-MM-dd");

var formattedDate = localDatePattern.Format(dateToFormat);

First, we initialize the dateToFormat variable which is of type LocalDate. Then, we create a formatting pattern which we use to format the LocalDate type. Finally, we call the Format() method that returns the string representation of the LocalDate type. This can be applied to other types as well.

Conclusion

In this article, we’ve learned about handling dates with Noda Time in .NET. We’ve discussed the core types of the library and their use cases. We’ve also learned about Noda Time extension methods, the NodaTime.Text namespace, and the various methods available in the namespace. A good understanding of how to use the different types goes a long way in improving the accuracy with which we handle dates in C#.

While Noda Time has many advantages, it’s essential to evaluate whether you need its features based on your project’s requirements. For simple date and time manipulations, the built-in .NET types might be sufficient. However, if you’re dealing with complex date-time scenarios, especially those involving multiple time zones, NodaTime can be a valuable tool to have in your toolkit.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!