Antes de hablar sobre este tema, te recomiendo que veas este video en donde Tom Soctt habla sobre las dificultades de trabajar con relojes, tiempo y zonas horarias.


Ahora, y una vez que ya sabes un poco más acerca de lo difícil que puede resultar trabajar con fechas dentro de tu aplicación (especialmente si tu app es un producto utilizado al rededor del mundo y más aún si es una aplicación web con servidores al rededor de todo el mundo), podemos comenzar.

NodaTime

NodaTime es una librería que, una vez que comprendemos los conceptos básicos, nos puede ayudar bastante a trabajar con fechas dentro de nuestra aplicación. En este post únicamente hablaré de apenas la superficie de esta gran librería.

Instant

La estructura Instant es una de las más importantes dentro de esta librería, podemos ver a cada instancia de esta clase como un momento en el tiempo. Por ejemplo, el momento en el que inicia un partido de fútbol, el momento en el que nace una persona o el momento en el que se realiza una transacción.

Es importante señalar Instant es un momento genérico independiente de todo, no depende de una zona horaria ni de un retraso fijo en el reloj (podríamos decir que siempre está en UTC) ni de un calendario.

Por ejemplo, para obtener el instante actual debemos llamar a la propiedad Now en una instancia de SystemClock, o podemos crear uno con alguno e los métodos factory indicándole el año, mes, día, hora y minutos:

Instant now = SystemClock.Instance.Now; // UTC
var publicacionPost = Instant.FromUtc(2017, 2, 7, 23, 0); // UTC

Podrías pensar que no es tan distinto de hacer algo así:

var ahora = DateTime.UtcNow; // UTC
var postPublication = new DateTime(2017, 2, 7, 23, 0, 0); // ???

Si imprimes las dos, podrías ver cosas más o menos idénticas:

Console.WriteLine("UTC:\t\t" + ahora.ToString("o"));
Console.WriteLine("UTC:\t\t" + now);
UTC:		2017-02-06T22:58:27.6325390Z
UTC:		2017-02-06T22:58:27Z

Pero ya veremos más adelante que si hay diferencia.

ZonedDateTime y DateTimeZoneProviders

Ya que tenemos el tiempo actual en ahora y now vamos a convertirlo al tiempo de la computadora local. Usando el tradicional DateTime no es tan complicado:

var ahoraLocal = ahora.ToLocalTime(); // Hora local
Console.WriteLine("Hora local:\t" + ahoraLocal.ToString("o"));

Dará como resultado Hora local: 2017-02-06T16:58:27.6325390-06:00

Ahora, como vimos antes, un Instant es un moment en el tiempo. Por si mismo un momento en el tiempo no tiene mucho sentido, al menos para los humanos, para nosotros es necesario darle un contexto, como el de una zona horaria, para poder entenderlo de mejor manera. Y es aquí cuando entra en juego la estructura ZonedDateTime que nos ayuda a darle este contexto a cada momento. Entonces se requiere un par de pasos extra que nos forzarán a pensar un poco más sobre los tiempos y el contexto de los datos:

  • El primer paso es obtener una zona horaria (DateTimeZone) de un proveedor (DateTimeZoneProviders), la librería tiene por default dos proveedores: Bcl y Tzdb, para fines prácticos diremos que la primera son las zonas horarias del sistema y la segunda son las zonas horarias definidas por la (Internet Assigned Numbers Authority) IANA. Procura siempre usar el proveedor de IANA:
// Obtenemos la zona horaria local del proveedor Bcl (sistema)
DateTimeZone localTimeZone = DateTimeZoneProviders.Bcl.GetSystemDefault(); 
  • El segundo paso es simplemente convertir nuestro momento UTC now a la zona horaria mediante el método InZone:
// Conversión a la zona horaria local
ZonedDateTime localNow = now.InZone(localTimeZone);
Console.WriteLine("Hora local:\t" + localNow);

¿Por qué tiene todo que ser tan complicado? ¿no es más sencillo usar solo DateTime? la respuesta es... no, al menos si quieres una aplicación que funcione y muestre tiempos correctos sin importar el huso horario en el que esté funcionando. La complicación con DateTime aparece cuando queremos convertir el momento a una hora local.

Por ejemplo, si quisiera saber la hora local en que fue publicado este post en la Ciudad de México y Copenhagen tendría que hacer algo más o menos así:

  • Usando DateTime. Bah, muy fácil, si ya tengo el tiempo en UTC bastaría con sumarle o restarle las horas correspondientes para obtener las horas locales:
// Normalmente la Ciudad de México está en -6... ¿o en horario de verano -5?
DateTime mexicoAhora = postPublication.AddHours(-6);
Console.WriteLine("Mexico City:\t" + mexicoAhora.ToString("o"));

// Normalmente la Copenhagen está en +1... ¿o en horario de verano +2?
DateTime copenhagenAhora = postPublication.AddHours(1);
Console.WriteLine("Copenhagen:\t" + copenhagenAhora.ToString("o")

// Imprimirá
// Mexico City:	2017-02-07T17:00:00.0000000
// Copenhagen:	2017-02-08T00:00:00.0000000

Podría funcionar... en ciertos momentos, ya que el código anterior no toma en cuenta el horario de verano (o Daylight Saving/Summer Time), lo cual significa que tu código funcionaría correctamente una parte del año. Y como viste en el vídeo de un poco más arriba, la cosa se puede poner más complicada.

  • NodaTime al rescate. Aquí cobra vital importancia esta pequeña librería ya que nos permite hacer estas conversiones sin preocuparnos sobre zonas horarias, horarios de verano y demás. Basta con obtener una zona horaria de un proveedor (usaremos el proveedor de IANA) y realizar la conversión:
DateTimeZone mexicoTimeZone = DateTimeZoneProviders.Tzdb["America/Mexico_City"];
ZonedDateTime mexicoNow = publicacionPost.InZone(mexicoTimeZone);
Console.WriteLine("Mexico City:\t" + mexicoNow);

DateTimeZone copenhagenTimeZone = DateTimeZoneProviders.Tzdb["Europe/Copenhagen"];
ZonedDateTime copenhagenNow = publicacionPost.InZone(copenhagenTimeZone);
Console.WriteLine("Copenhagen:\t" + copenhagenNow);

// Imprimirá  
// Mexico City:	2017-02-07T17:00:00 America/Mexico_City (-06)
// Copenhagen:	2017-02-08T00:00:00 Europe/Copenhagen (+01)

Pareciera más complicado... pero a la larga resulta más mantenible y entendible. Como puedes ver se hace referencia clara a qué zona horaria queremos transformar nuestro momento, no tenemos que estar adivinando qué significa ese -6 o ese 1 en nuestro código. Como referencia, puedes visitar estos enlaces para obtener los nombres de cada zona horaria: IANA time zone database y List of tz database time zones.

LocalDate y LocalTime

NodaTime también nos facilita el manejo de datos cuando debemos manejar solo fechas u horas en un día, sin necesidad de preocuparnos por eliminar las componentes de tiempo o fecha nosotros mismos. Esto lo hace a través de las estructuras LocalDate y LocalTime

Period, Duration e Interval

NodaTime también incluye estructuras para trabajar con periodos de tiempo, duraciones e intervalos. Algo muy parecido a lo que se puede hacer con TimeSpan pero dándole un poco más de contexto para hacer más entendible el código de nuestra aplicación.

Ejemplo de uso e instalación

Como siempre, la mejor forma de aprender a manejar algo es experimentando con él, así que no te olvides de descargar el código fuente.

Para instalarlo puedes usar NuGet, ya sea la interfaz gráfica o el administrador de paquetes:

PM> Install-Package NodaTime

Y como casi siempre con estos #NuGetsRecomendados puedes descargar el código fuente de la librería y hasta contribuír a su desarrollo.