¿Cómo almacenarías en .NET una fecha, pero sin una hora asociada? ¿Y al revés, una hora sin una fecha? La respuesta corta es: no se puede. O al menos esto era así hasta .NET 6.
Te cuento cómo funcionan los nuevos tipos que incluye esta plataforma para solucionar el problema.
El estado de las cosas antes de .NET 6
Si algo sabemos en este blog desde hace tiempo es lo complicado que es manejar bien las fechas y las horas.
Desde los tiempos de Visual Basic, allá a principios de los años 90, almacenar fechas y horas de manera separada ha sido un problema peliagudo en las plataformas de desarrollo de Microsoft.
En VB clásico existía el tipo Date
que, a pesar de su nombre, servía para almacenar tanto fechas como horas, pero siempre conjuntamente.
Cuando en 2001 se lanzó la plataforma .NET "clásica", básicamente lo que hicieron fue definir una estructura DateTime
que, en esencia, era idéntica a la vetusta que ya existía en VB. Con ella puedes guardar fechas, pero siempre van acompañadas de su componente horario, aunque no lo indiques.
Y eso ¿qué problema tiene?
Bueno, aparte del obvio de ocupar más memoria (pero que no nos preocupa lo más mínimo en condiciones normales), el mayor reto que presenta esta estructura para manejar solo una de sus partes (fecha u hora) es que no lleva incluida información sobre la zona horaria. Y esto significa que pueden pasar "cosas" cuando, por ejemplo, se maneja la misma información en dos sitios diferentes (como cliente y servidor) o cuando se consulta esa información más adelante.
Precisamente para evitar problemas de varios tipos que pueden surgir al manejar fechas, la plataforma .NET nos ofrece 4 clases especializadas en manejo de fechas:
DateTime
, que es la más habitual, pero como hemos dicho no incluye información sobre zona horaria o desplazamiento horario.
DateTimeOffset
: que es equivalente a la anterior, pero incluye además información de desplazamiento horario para evitar problemas como el mencionado.
TimeSpan
: que se emplea para medir diferencias entre fechas. Este a veces se presenta como la solución para guardar solo horas, pero aparte de que es difícil de definir porque hay que hacerlo sumando o restando otras fechas, resulta que al llegar a las 24h no "da la vuelta" sino que también añade días, así que no vale para muchos casos.
TimeZoneInfo
: que representa zonas horarias y permite la conversión entre ellas.
Pero con ellas, solucionar el problema de representar tan solo una fecha o una hora no es nada sencillo. De hecho, la propia Microsoft ofrece un artículo en la documentación de la plataforma específico para aprender a elegir el mejor tipo según lo que queramos hacer: Elección entre DateTime, DateTimeOffset, TimeSpan y TimeZoneInfo. Y también tienen explicaciones detalladas sobre cómo convertir entre los dos primeros de la lista anterior: Conversión entre DateTime y DateTimeOffset.
Nota: de hecho, en .NET 2 se sacaron de la manga otra clase, DateTime.Kind
, para que pudieras especificar en las fechas si se referían a UTC porque las habías pasado antes a esta referencia o se refería a la hora local. Y lo que ocurrió es que liaron la cosa más todavía.
Al final, lo que muchos programadores hacen cuando tienen que manejar fechas en .NET para algo más que para cosas sencillas es recurrir a la biblioteca Noda Time, basada en Joda Time de Java, que es una maravilla y te da hechas la mayor parte de las cosas.
Todo esto está bien cuando quieres hacer cosas complejas, pero muchas veces lo único que quieres es almacenar una fecha o una hora. Debería ser más fácil...
Manejo de fechas y horas discretas con .NET 6
En .NET 6 se presentan dos nuevos tipos en forma de estructura que sirven para almacenar fechas y horas individuales, la una sin la otra.
Se llaman DateOnly
y TimeOnly
. Le han llamado así y no Date
y Time
a secas porque, por compatibilidad con VB6, en VB.NET existe también la palabra clave Date
y quedaría "raro", así que están pagando la deuda técnica de hace casi 30 años. De todos modos, los nombres son claros y no pasa nada por teclear un poco más (de hecho con Intellisense te da un poco igual porque lo teclea por ti 😜).
Nota: otro motivo de no usar el nombre Date
a secas, es que las propiedades Date
de DateTime
y DateTimeOffset
devuelven un DateTime. Con el nuevo tipo lo que esperaría cualquier desarrollador es que devolvieran un DateOnly
, ya que estás pidiendo solo la fecha, pero esto no lo pueden cambiar pues rompería la compatibilidad hacia atrás. Así que, ahora, hay una propiedad DateTime.DateOnly
para obtener solo la fecha.
Estas dos estructuras nuevas funcionan casi igual a DateTime,
pero al carecer de la parte que no necesitan, son más sencillas y se evitan los problemas de las anteriores.
En el momento de escribir esto no hay todavía documentación oficial, pero es muy fácil ver cómo son y cómo funcionan ya que tenemos tanto su código fuente como sus test:
Como puedes observar tienen los constructores típicos, como por ejemplo:
DateOnly dateOnly = new DateOnly(2021, 5, 10);
TimeOnly timeOnly = new TimeOnly(19, 18);
TimeOnly timeOnly2 = new TimeOnly(19, 18, 35, 500); //con milisegundos
Propiedades para obtener sus partes:
int anio = dateOnly.Year;
int minutos = timeOnly.Minute;
Los habituales métodos para añadir o quitar fracciones de tiempo: AddDays()
, AddMonths()
, AddYears()
, o bien AddHours()
, AddMinutes()
, AddSeconds()
, AddMilliseconds()
, AddTicks()
en el caso de las horas; para "parsear" (interpretar) valores: Parse()
, ParseExact()
, TryParse()
y TryParseExact()
; y por supuesto operadores sobrescritos para poder operar con ellas: ==
, <
, >
, <=
, >=
,!=
, +
y -
.
Es decir, si sabes trabajar con DateTime
sabes trabajar con DateOnly
y TimeOnly,
pues funcionan igual y se quedan tan solo con las cosas que les pertenecen según quieras manejar fechas u horas de forma independiente.
Sólo una última advertencia: en .NET 6 estas estructuras no implementan la interfaz ISerializable
, por lo que si las quieres utilizar para almacenarlas o intercambiarlas, o bien las gestionas de manera específica en tu serializador o te aseguras de que el serializador de terceros que utilices las soporta. Eso, o mejor evítalas si tienes esta necesidad.
Ahora que ya los conoces, ten en mente estos dos nuevos tipos cuando vayas a manejar fechas o tiempos en tus aplicaciones.