farblog

by Malcolm Rowe

Noda Time 1.2.0

Noda Time 1.2.0 finally came out last week, and since I promised I’d write a post about it, here’s a post about it — which I’ve also just partially self-plagiarised in order to make a post for the Noda Time blog, so apologies if you’ve read some of this already. I promise there’s new content below as well.

While the changes in Noda Time 1.1 were around making a Portable Class Library version and filling in the gaps from the first release, Noda Time 1.2 is all about serialization1 and text formatting.

On the serialization side, Noda Time now supports XML and binary serialization natively, and comes with an optional assembly (and NuGet package) to handle JSON serialization (using Json.NET).

On the text formatting side, Noda Time 1.2 now properly supports formatting and parsing of the Duration, OffsetDateTime, and ZonedDateTime types.

We also fixed a few bugs, and added a some more convenience methods — Interval.Contains() and ZonedDateTime.Calendar, among others — in response to requests we received from people using the library2.

Finally, it apparently wouldn’t be a proper Noda Time major release without fixing another spelling mistake in our API: we replaced Period.Millseconds in 1.1, but managed not to spot that we’d also misspelled Era.AnnoMartyrm, the era used in the Coptic calendar. That’s fixed in 1.2, and I think (hope) that we’re done now.

There’s more information about all of the above in the comprehensive serialization section of the user guide, the pattern documentation for the Duration, OffsetDateTime, and ZonedDateTime types, and the 1.2.0 release notes.

You can pick up Noda Time 1.2.0 from the NuGet repository as usual, or from the links on the Noda Time home page.

That’s the summary, anyway. Below, I’m going to going into a bit more detail about XML and JSON serialization, and what kind of things you can do with the new text support.

XML serialization

Using XML serialization is pretty straightforward, and mostly works as you’d expect. Here’s a complete example demonstrating XML serialization of a Noda Time property:

using System;
using System.IO;
using System.Xml;
using System.Xml.Serialization;
using NodaTime;

public class Person
{
  public string Name { get; set; }
  public LocalDate BirthDate { get; set; }
}

static class Program
{
  static void Main(string[] args)
  {
    var person = new Person {
      Name = "David",
      BirthDate = new LocalDate(1979, 3, 22)
    };

    var x = new XmlSerializer(person.GetType());
    var namespaces = new XmlSerializerNamespaces(
      new XmlQualifiedName[] { new XmlQualifiedName("", "urn:") } );

    var output = new StringWriter();
    x.Serialize(output, person, namespaces);
    Console.WriteLine(output);
  }
}

As you can see, there’s nothing special here, and the output is also as you’d expect:

<?xml version="1.0" encoding="utf-8"?>
<Person>
  <Name>David</Name>
  <BirthDate>1979-03-22</BirthDate>
</Person>

There are a couple of caveats to be aware of regarding XML serialization, though, most notably that the Period type requires special handling. Period is an immutable reference type, which XmlSerializer doesn’t really support, and so you’ll need to serialize via a proxy PeriodBuilder property instead.

The other notable issue (which also applies to binary serialization) is that .NET doesn’t provide any way to provide contextual configuration, and so when deserializing a ZonedDateTime, we need a way to find out which time zone provider to use.

By default, we’ll use the TZDB provider, but if you’re using the BCL provider (or any custom provider), you’ll need to set a static property:

DateTimeZoneProviders.Serialization = DateTimeZoneProviders.Bcl;

The serialization section in the user guide has more details about both of these issues.

There are also two other limitations of XmlSerializer that aren’t specific to Noda Time, but are good to know about if you’re just getting started:

JSON serialization

Noda Time’s JSON serialization makes use of Json.NET, which means that to use it, you’ll need to add references to both the Json.NET assembly (Newtonsoft.Json.dll) and the Noda Time support assembly (NodaTime.Serialization.JsonNet.dll).

The only setup you need to do in code is to inform Json.NET how to serialize Noda Time’s types (and again, which time zone provider to use). This can either be done by hand, or via a ConfigureForNodaTime extension method. Again, the user guide has all the details.

Once that’s done, using the serializer is straightforward:

using System;
using System.IO;
using Newtonsoft.Json;
using NodaTime;
using NodaTime.Serialization.JsonNet;

internal class Person
{
  public string Name { get; set; }
  public LocalDate BirthDate { get; set; }
}

static class Program
{
  static void Main(string[] args)
  {
    var person = new Person {
      Name = "David",
      BirthDate = new LocalDate(1979, 3, 22)
    };

    var json = new JsonSerializer();
    json.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb);

    var output = new StringWriter();
    json.Serialize(output, person);
    Console.WriteLine(output);
  }
}

Output:

{"Name":"David","BirthDate":"1979-03-22"}

Unlike the .NET XML serializer, the Json.NET serializer is significantly more configurable. The Json.NET documentation is probably a good place to start if you’re interested in doing that.

Better text support

Noda Time 1.2 adds parsing and formatting for the Duration, OffsetDateTime, and ZonedDateTime types, which previously only had placeholder ToString() implementations. Given a series of assignments like the following:

var paris = DateTimeZoneProviders.Tzdb["Europe/Paris"];

ZonedDateTime zdt = SystemClock.Instance.Now.InZone(paris);
OffsetDateTime odt = zdt.ToOffsetDateTime();
Duration duration = Duration.FromSeconds(12345);

the result of calling ToString() on each of the zdt, odt, and duration variables would produce something like the following in 1.1:

Local: 26/11/2013 19:35:28 Offset: +01 Zone: Europe/Paris
2013-11-26T19:35:28.00081+01
Duration: 123450000000 ticks

In 1.2, these types use a standard pattern by default instead: the general invariant pattern (‘G’), for ZonedDateTime and OffsetDateTime, and the round-trip pattern (‘o’) for Duration:

2013-11-26T19:35:28 Europe/Paris (+01)
2013-11-26T19:35:28+01
0:03:25:45

More usefully, we can now use custom patterns:

var pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
  "dd/MM/yyyy' 'HH:mm:ss' ('z')'", null);
Console.WriteLine(pattern.Format(zdt));

which will print “26/11/2013 19:35:28 (Europe/Paris)”.

The null above is an optional time zone provider. If not specified, as shown above, the resulting pattern can only be use for formatting, and not for parsing3. This is why the standard patterns are format-only: they don’t have a time zone provider.

If you do specify a time zone provider, however, you can parse your custom format just fine:

var pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
  "dd/MM/yyyy' 'HH:mm:ss' ('z')'", DateTimeZoneProviders.Tzdb);
var zdt = pattern.Parse("26/11/2013 19:35:28 (Europe/Paris)").Value;
Console.WriteLine(zdt);

which prints “2013-11-26T19:35:28 Europe/Paris (+01)”, as you would expect.

As well as formatting the time zone ID (the “z” specified in the format string above), you can also format the time zone abbreviation (using “x”), which given the above input would produce “CET”, for Central European Time.

Now, if you’ve seen Jon’s “Humanity: Epic fail” talk — or watched his recent presentation at DevDay Kraków, which covers some of the same content — then you’ll already know that time zone abbreviations aren’t unique. For that reason, if you include a time zone abbreviation when creating a ZonedDateTimePattern, the pattern will also be format-only.

In addition to the time zone identifiers, both ZonedDateTime and OffsetDateTime patterns accept a format specifier for the offset in effect. This uses a slightly unusual format, as Offset can be formatted independently: it’s “o<…>”, where the “…” is an Offset pattern specifier. For example:

var pattern = ZonedDateTimePattern.CreateWithInvariantCulture(
  "dd/MM/yyyy' 'HH:mm:ss' ('z o<+HH:mm>')'", null);
Console.WriteLine(pattern.Format(zdt));

which will unsurprisingly print “26/11/2013 19:35:28 (Europe/Paris +01:00)”.

For OffsetDateTime, the offset is a core part of the type, while for ZonedDateTime, it allows for the disambiguation of otherwise-ambiguous local times (as typically seen during a daylight saving transition).

If the offset is not included, the default behaviour for ambiguous times is to consider the input invalid. However, this can also be customised by providing the pattern with a custom resolver.

Finally, to Duration. Duration formatting is a bit more interesting, because we allow you to choose the granularity of reporting. For our duration above, of 12,345 seconds, the round-trip pattern shows the number of days, hours, minutes, seconds, and milliseconds (if non-zero), as “0:03:25:45”.

We can also format just the hours and minutes:

var pattern = DurationPattern.CreateWithInvariantCulture("HH:mm");
var s = pattern.Format(duration);
Console.WriteLine(s);

which prints “03:25” — or we can choose to format just the minutes and seconds:

var pattern = DurationPattern.CreateWithInvariantCulture("M:ss");
var s = pattern.Format(duration);
Console.WriteLine(s);

which does not print “25:45”, but instead prints “205:45”, reporting the total number of minutes and a ‘partial’ number of seconds. Had we instead used “mm:ss” as the pattern, we would indeed have seen the former result; the case of the format specifier determines whether a total or partial value is used.

Once again, there’s more information on all of the above in the relevant sections of the user guide.


  1. or serialisation. I apologise in advance for the spelling, but the term turns up in code all the time (e.g. ISerializable), and I find it makes for awkward reading to mix and match the two. 

  2. There’s definitely a balance to be had between the Pythonesque “only one way to do it” maxim and providing so many convenience methods that they cloud the basic concepts, and I think for 1.0 we definitely tended towards the former — which isn’t that bad: it’s easy to expand an API, but hard to reduce it. Some things that were a little awkward should be easier with 1.2, though. 

  3. The error message you’ll see is “UnparsableValueException: This pattern is only capable of formatting, not parsing.”