Updated on

Noda Time is an open-source date and time library for .NET that gives each kind of value its own type: Instant for a moment on the global timeline, LocalDate and LocalDateTime for calendar values, and ZonedDateTime when a time zone matters.

Handling dates can be confusing, especially when we build an application with time-sensitive data and users across different time zones. That’s where Noda Time comes in.

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 date and time library for .NET. Using it, we don’t need to worry about the inner workings of dates and time, because every concept has its own type.

An Instant is a point on the global timeline, with no calendar and no time zone. A LocalDate, LocalTime or LocalDateTime is what a calendar or a wall clock shows, with no idea where in the world it is. A ZonedDateTime ties a local value to a DateTimeZone, so it also knows its exact instant.

Lengths of time are split the same way. A Duration is a fixed number of nanoseconds. A Period is a calendar amount such as “one month”, and its real length depends on where we start.

With those types, we can track events across various time zones, calculate durations, or manage dates, and each value says exactly what it is.

Why Does Noda Time Exist?

Noda Time is an alternative to the .NET built-in date and time API, and it addresses that API’s 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 on the global timeline, independent of any 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 (we compare it with DateTimeOffset in how DateTime and DateTimeOffset differ).

Immutable Noda Time Types

All Noda Time value 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. The library ships regularly, and each patch release carries fresh time zone data: version 3.3.5 came out on 30 September 2026 with TZDB 2026e.

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. Each file that uses Noda Time needs using NodaTime; at the top, plus using NodaTime.Text; for the text patterns and using NodaTime.Extensions; for the extension methods. The three blocks that check a result with Assert.Equal() are xUnit tests, so we put them, together with the variables they use, inside a [Fact] method of an xUnit test project (dotnet new xunit) where we add the NodaTime package the same way.

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. On .NET 8 and later, code that already takes a TimeProvider can hand Noda Time a clock with TimeProvider.System.ToClock() from the NodaTime.Extensions namespace. That also makes a fake clock in tests easy, as we show when overwriting DateTime.Now during testing.

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.FromUnixTimeSeconds() method to create a Noda Time Instant:

var instant = Instant.FromUnixTimeSeconds(1693153840);

This method creates an Instant from the number of seconds since the Unix epoch, in this case 27 August 2023, 16:30:40 UTC. FromUnixTimeMilliseconds() and FromUnixTimeTicks() do the same for smaller units.

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 ToUnixTimeSeconds() method:

var epochTime = 1693153840;

var instant = Instant.FromUnixTimeSeconds(epochTime);
var unixTimeSeconds = instant.ToUnixTimeSeconds();

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

Finally, we convert the Instant back to seconds since 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 .NET, a tick is 100 nanoseconds, so 600,000,000 ticks make one minute. Noda Time itself goes finer, down to single nanoseconds.

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 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 compare the LocalTime values directly. 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 the time we call the method on is later than the argument, we get 1. If it is earlier, 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.

The built-in types follow the same rules, which we cover in comparing DateTime values in C#.

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 ask the DateTimeZoneProviders.Tzdb provider for a time zone by its ID:

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.

ZonedDateTime

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

To create ZonedDateTime, we combine LocalDateTime and a DateTimeZone:

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

var zonedDateTime = 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 ZonedDateTime value.

Why InZoneLeniently()? Daylight saving changes make some local times happen twice and skip others entirely. InZoneLeniently() picks a sensible instant in both cases, while InZoneStrictly() throws an exception, so we choose strict mode when a bad local time should stop us.

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

How Do We Add a Duration to a LocalDateTime in Noda Time?

We can’t, at least not directly. LocalDateTime has no Plus(Duration) method and no + operator for a Duration, so localDateTime + duration fails to compile with error CS0019.

That is on purpose. A Duration is elapsed time on the global timeline, and a LocalDateTime isn’t on that timeline until we say which time zone it belongs to. One hour after 01:30 depends on whether the clocks change at 02:00.

So we pick what we mean. If we mean “move the clock hands”, we add a Period instead, with localDateTime + Period.FromHours(1) or the PlusHours(1) shortcut. If we mean “wait this long”, we put the value in a time zone first with InZoneStrictly() or InZoneLeniently(), and then add the Duration to the ZonedDateTime.

When we already hold a Duration and only want local arithmetic, PlusNanoseconds(duration.ToInt64Nanoseconds()) moves the clock by exactly that amount.

The Noda Time arithmetic guide says this about timeline arithmetic and calendar arithmetic: the library “deliberately separates the two, with the aim of avoiding subtle bugs”.

Let’s see the difference on a real date. On 8 March 2026, clocks in New York jump from 02:00 straight to 03:00. Let’s start at 01:30 that night and wait one hour:

var newYork = DateTimeZoneProviders.Tzdb["America/New_York"];
var start = new LocalDateTime(2026, 3, 8, 1, 30).InZoneStrictly(newYork);

var later = start + Duration.FromHours(1);

Here, we place 01:30 in the New York time zone and add a one-hour Duration to the resulting ZonedDateTime. Exactly one hour passes, but the wall clock now reads 03:30, because the hour between 02:00 and 03:00 doesn’t exist that night.

Timeline showing that adding a one-hour Duration to 01:30 in New York on 8 March 2026 gives 03:30, while adding a one-hour Period gives 02:30, a time skipped by daylight saving.

A Duration measures elapsed time, a Period moves the clock hands.

Now let’s move the clock hands instead, with a Period:

var localStart = new LocalDateTime(2026, 3, 8, 1, 30);

var localLater = localStart + Period.FromHours(1);

This time we get 02:30. That is correct calendar arithmetic, and it is also a local time that never happens in New York on that date. If we call localLater.InZoneStrictly(newYork), Noda Time throws a SkippedTimeException and tells us so.

Finally, when a Duration comes from somewhere else and we only need local arithmetic, we convert it to nanoseconds:

var duration = Duration.FromMinutes(90);

var localResult = localStart.PlusNanoseconds(duration.ToInt64Nanoseconds());

This gives us 03:00 on the same date, 90 minutes of clock movement with no time zone involved.

Here is which Noda Time types accept a Duration and which accept a Period:

TypeAdd a DurationAdd a Period
InstantYes: Plus(Duration) or +No
ZonedDateTimeYes: Plus(Duration) or +No
OffsetDateTimeYes: Plus(Duration) or +No
LocalDateTimeNo, compile error CS0019Yes: Plus(Period) or +
LocalDateNoYes, date units only

The LocalDateTime row answers our question: a Duration fails with CS0019, while a Period works. To add a Duration, we first turn the value into a ZonedDateTime, which accepts one.

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 from the NodaTime.Extensions namespace 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. Mind you, the DateTime has to be UTC. With DateTimeKind.Local or DateTimeKind.Unspecified, ToInstant() throws an ArgumentException.

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 static Period.Between() 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 LocalDate 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. That is why we first convert the DateTime with LocalDate.FromDateTime().

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 date and time 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. Finally, we’ve seen why a Duration never goes straight onto a LocalDateTime, and what to add instead. 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, including DateOnly and TimeOnly, 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.

Tested with .NET 10.0.10 and NodaTime 3.3.5.