Era japonesa, festivos y fechas de cierre en aplicaciones empresariales — diseño resiliente a los cambios de era, JapaneseCalendar y cálculo de días hábiles en la práctica
· Actualizado el: · Go Komura · C#, .NET, Era japonesa (wareki), Festivos, Fecha de cierre, Tratamiento de fechas, Aplicaciones empresariales, Desarrollo Windows, Consultoría técnica
Historial de revisiones (1 actualizaciones, última el 22 Aug 2026)
Registro de los cambios realizados en este artículo. Cuando se archivó una versión previa, sigue siendo legible mediante un enlace permanente con DOI.
- Se ha sustituido la traducción, que estaba abreviada, por una traducción completa del artículo japonés en su versión actual: el texto crece un 25 %. Se han incorporado 5 filas de tabla y 5 bloques de código que no estaban en la edición anterior. El contenido no cambia respecto al original japonés; esta edición simplemente ya no lo resume. Además, los enlaces a otros artículos que ya tienen edición en español apuntan ahora a esa edición en lugar de a la japonesa. Leer la versión anterior a esta actualización (DOI: 10.5281/zenodo.21638383)
- Primera publicación
Citar este artículo(DOI: 10.5281/zenodo.21638382)
Este artículo está archivado en Zenodo. A continuación se muestran tanto el DOI que siempre resuelve a la última versión como el DOI fijado a la versión que está leyendo.
Go Komura (2026). Era japonesa, festivos y fechas de cierre en aplicaciones empresariales — diseño resiliente a los cambios de era, JapaneseCalendar y cálculo de días hábiles en la práctica. KomuraSoft LLC. https://doi.org/10.5281/zenodo.21638382 https://comcomponent.com/es/blog/japanese-calendar-holidays-closing-date-guide/
- DOI (última versión)
- 10.5281/zenodo.21638382
- DOI (esta versión)
- 10.5281/zenodo.22053473
«La fecha del informe, en era japonesa» — «la fecha de vencimiento del pago: cierre el día 20, se paga a fin del mes siguiente, y si el último día es festivo, el día hábil anterior» — «los totales, por ejercicio fiscal»: las aplicaciones empresariales japonesas vienen de serie con requisitos de fecha que solo tienen sentido en este país. Y en este terreno se esconde una clase de trampa distinta de la del tratamiento de fecha y hora en general: las eras aumentan con cada cambio de era, los festivos se mueven con cada reforma legal, y el «fin de mes» tiene una longitud distinta según el mes. Es decir, todos ellos son «especificaciones que cambian más adelante».
En el artículo anterior, «Fecha, hora y zona horaria en aplicaciones empresariales», repasamos el principio de guardar en UTC y el tratamiento de las zonas horarias. Este artículo es su continuación y aborda los tres grandes requisitos propios de Japón: la era japonesa, los festivos y las fechas de cierre. Veremos el diseño resiliente a los cambios de era, por qué no hay que codificar los festivos de forma fija y cómo guardarlos como datos, y el cálculo de fechas de cierre teniendo en cuenta el redondeo de AddMonths, todo con ejemplos de implementación y tablas de decisión.
1. Ante todo, la conclusión
- El procesamiento interno y el almacenamiento van en calendario occidental (gregoriano); la era japonesa aparece solo en el instante de mostrarse. Guardar en cadenas de era japonesa acarrea una triple penalización: imposibilidad de ordenar, mezcla de notaciones tras cada cambio de era y aparición de fechas inválidas.
- La conversión a era japonesa se delega en
JapaneseCalendar, no en una tabla de correspondencia propia. La definición de las eras la suministra el sistema operativo, de modo que en un cambio de era basta con aplicar las actualizaciones de la plataforma.1 Lo único que se queda atrás es el código de la aplicación que codificó «Heisei» o «Reiwa» de forma fija. - Los festivos no se pueden determinar mediante una fórmula de cálculo. El equinoccio de primavera y el de otoño se fijan el año anterior, y los propios festivos se desplazan por reformas legales y leyes de medidas especiales.2 El diseño correcto es una tabla maestra de festivos más un proceso de actualización, y como fuente de datos puede usarse el CSV de festivos que publica el Cabinet Office (Gabinete de Japón).
- El «fin de mes», el «día hábil» y el «ejercicio fiscal» son conceptos que se definen en la especificación antes que en el código. En particular,
AddMonthsredondea automáticamente el fin de mes, así que el cálculo de la fecha de cierre debe partir siempre de una fecha de referencia y recalcularse cada vez.3
Antes de nada, resumimos cómo guardar cada tipo de dato.
| Dato | Cómo guardarlo | Momento de conversión o determinación |
|---|---|---|
| La fecha en sí (fecha del pedido, fecha de vencimiento del pago) | DateOnly / DateTime en calendario occidental (tipo date en la base de datos) |
— |
| Notación en era japonesa (informes, pantalla) | No se guarda | Se convierte con JapaneseCalendar en el momento de mostrarla o imprimirla |
| Festivos | Tabla maestra de festivos (tabla o archivo de configuración) | Se consulta la tabla maestra al calcular días hábiles |
| Días de cierre de la empresa (vacaciones de verano, aniversario de fundación) | Tabla maestra de días de cierre, separada de la de festivos | Igual que arriba (no se mezcla con los festivos) |
| Fecha de cierre y plazo de pago | Atributo de la tabla maestra de clientes/proveedores («cierre el día 20, se paga a fin del mes siguiente») | Se calcula desde la fecha de referencia al procesar la facturación |
| Ejercicio fiscal | No se guarda (valor derivado) | Se calcula a partir de la fecha al mostrarlo o agregarlo |
2. La era japonesa — que exista solo para mostrarse
2.1 Por qué unificar el interior en calendario occidental
La era japonesa es una notación pensada para las personas, no una representación apta para el cálculo. «Heisei 9» y «Reiwa 9» no se pueden ordenar mediante comparación de cadenas, y cualquier cálculo de un periodo que cruce una era acaba necesitando, de todos modos, la conversión al calendario occidental. Además, como el sistema de eras crece con cada cambio de era, los datos guardados en era japonesa terminan mezclando la notación antigua y la nueva cada vez que hay un cambio de era. La aparición de datos inválidos como «Showa 64, 10 de enero» (fecha inexistente: desde el 8 de enero ya es Heisei 1) es también un problema clásico en los sistemas que guardan tal cual la entrada en era japonesa.
Hay otra restricción que suele pasarse por alto: JapaneseCalendar de .NET solo admite fechas desde el 8 de septiembre de la era Meiji 1 (1868).4 Las fechas anteriores a esa —por ejemplo, una fecha de nacimiento antigua procedente de un registro familiar— no se pueden tratar correctamente si se mantienen en notación de era. Si se unifica el procesamiento interno en el calendario gregoriano occidental, todos estos problemas quedan confinados a «un asunto exclusivo de la capa de visualización».
2.2 Visualización y entrada de datos con JapaneseCalendar
En .NET, la única clase de calendario capaz de manejar varias eras es JapaneseCalendar (junto con JapaneseLunisolarCalendar), y el especificador de formato g corresponde al nombre de la era.1 El código mínimo para mostrar una fecha en era japonesa es el siguiente.
using System.Globalization;
var jaJpWareki = new CultureInfo("ja-JP");
jaJpWareki.DateTimeFormat.Calendar = new JapaneseCalendar();
var date = new DateOnly(2026, 7, 11);
Console.WriteLine(date.ToString("ggy年M月d日", jaJpWareki)); // 令和8年7月11日
Console.WriteLine(date.ToString("ggyy/MM/dd", jaJpWareki)); // 令和08/07/11
Para recibir entradas en era japonesa, lo seguro no es un análisis de cadena libre, sino una interfaz que «deje elegir la era en un cuadro combinado y reciba el año, el mes y el día como números». En pantalla basta con la siguiente disposición.
[Era ▼] [ 8 ] Año [ 7 ] Mes [ 11 ] Día Año occidental: 2026-07-11
- Era — Cuadro combinado. El valor predeterminado es la era actual. Si la pantalla se usa para introducir datos antiguos, se muestran como opciones solo las eras del rango que se vaya a tratar.
- Año, mes y día — Entrada numérica. El límite superior del año varía según la era, así que el rango admitido cambia con la era seleccionada.
- Vista previa en calendario occidental — Se muestra en el acto el año occidental resultante de la conversión. Solo con esta vista previa puede la persona que introduce los datos advertir combinaciones como la de «Showa 64, 10 de enero» que se explica a continuación.
- Valor que se conserva — Lo que guarda la pantalla es el año occidental. Los tres campos de la era japonesa son solo una herramienta de entrada.
Si de todos modos hay que recibirla como cadena, se fija el formato con ParseExact.
var input = "令和8年7月11日";
var parsed = DateTime.ParseExact(input, "ggy年M月d日", jaJpWareki);
Console.WriteLine(parsed.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)); // 2026-07-11
La notación del primer año merece atención. En entornos con las actualizaciones de soporte al cambio de era aplicadas, el primer año de cada era se formatea por defecto como 元年 (literalmente «año fundacional», es decir «Reiwa gannen»), y existe un conmutador de compatibilidad (Switch.System.Globalization.FormatJapaneseFirstYearAsANumber) para volver a la notación numérica «Reiwa 1».5 Antes de implementar, hay que confirmar como especificación cuál de las dos exige el formato del informe. Otro detalle: por defecto, la comprobación del rango de años de la era está relajada (relaxed), así que una especificación de un año inexistente como «Heisei 33» se acepta y se convierte a la fecha real (Reiwa 3). Si se quiere rechazar esto de forma estricta como validación de entrada, puede forzarse con Switch.System.Globalization.EnforceJapaneseEraYearRanges.5 Mantener el modo relajado o pasar al estricto depende de cuán «sucios» estén los datos existentes (cómo tratar un «Heisei 32» impreso o introducido antes del cambio de era).
Estos conmutadores no se configuran desde el código, sino mediante un archivo de configuración.5 En .NET (Core)/.NET 5 en adelante se escriben en configProperties, dentro de la configuración de tiempo de ejecución.
{
"runtimeOptions": {
"configProperties": {
"Switch.System.Globalization.FormatJapaneseFirstYearAsANumber": true,
"Switch.System.Globalization.EnforceJapaneseEraYearRanges": true
}
}
}
Si se gestiona a nivel de proyecto, escribir la parte de configProperties en runtimeconfig.template.json hace que se refleje en el <nombre-de-la-app>.runtimeconfig.json que se genera al compilar. En .NET Framework 4.6 en adelante, el equivalente es AppContextSwitchOverrides dentro de app.config.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<AppContextSwitchOverrides value="Switch.System.Globalization.EnforceJapaneseEraYearRanges=true" />
</runtime>
</configuration>
Si se mantiene .NET Framework 4.5.2 o anterior, el mecanismo pasa por colocar una entrada con el mismo nombre, de tipo REG_SZ, en HKEY_LOCAL_MACHINE\Software\Microsoft\.NETFramework\AppContext.5
Ahora bien, qué evita este conmutador y qué no evita solo se entiende bien con ejemplos concretos; si no, es fácil juzgar mal. La documentación oficial recoge que, en modo relajado, especificar «Showa 65, 9 de enero» se trata como Heisei 2, 9 de enero (1990-01-09 en calendario occidental), y que, al pasar al modo estricto, produce una ArgumentOutOfRangeException con el mensaje «los valores válidos van de 1 a 64».5 Aplicando esta misma regla se obtiene lo siguiente.
| Especificación en era japonesa | Predeterminado (relajado) | Con el modo estricto activado |
|---|---|---|
| Heisei 33, día ◯ del mes ◯ | Se acepta, y se convierte en la fecha equivalente a Reiwa 3 | ArgumentOutOfRangeException |
| Showa 65, 9 de enero | Se acepta, y se convierte en 1990-01-09 (Heisei 2, 9 de enero) | ArgumentOutOfRangeException (el rango válido de Showa es 1–64) |
| Showa 64, 10 de enero | Se acepta, y se convierte en 1989-01-10 | Pasa igualmente (como año, está dentro del rango 1–64) |
La tercera fila merece atención. Como Showa llega hasta el año 64, la sola comprobación del rango de años no basta para rechazar «Showa 64, 10 de enero»: Showa terminó el 7 de enero de 1989, pero lo único que se valida es el valor del año. Los datos corruptos como «Showa 64, 10 de enero» mencionados en 2.1 entran precisamente por esta rendija. Si se quiere validar con rigor incluso las fechas del mes en que ocurre el cambio de era, hay que escribir por cuenta propia una validación que guarde la fecha de inicio y fin de cada era y compare fecha a fecha, en lugar de confiar en el conmutador.
2.3 Código resiliente a los cambios de era, y lo que se queda atrás
La experiencia del cambio de era a Reiwa (2019) deja una lección clara: la plataforma sigue el ritmo, pero la implementación propia de la aplicación no. La definición de la era no está codificada de forma fija en .NET, sino que el mecanismo consulta la información de era que suministra el sistema operativo, así que, si se aplican las actualizaciones del sistema operativo y de .NET, la conversión y el formateo de JapaneseCalendar siguen automáticamente la nueva era.1 Lo que se queda atrás son activos del lado de la aplicación como los siguientes.
- Tablas y ramas de conversión de era propias, del tipo
if (year >= 2019) era = "Reiwa"; - Nombres de era escritos directamente en plantillas de informes, formatos de celda de Excel o formularios preimpresos
- Código que trata con reglas propias un único carácter alfabético de era («R», «H», etc.), algo frecuente en los formatos de integración con sistemas troncales
- Los propios datos guardados en notación de era japonesa
En una estimación de la modificación para adaptarse al cambio de era, lo primero es hacer inventario recorriendo mecánicamente todo el código fuente y las plantillas con grep buscando «Showa», «Heisei», «Reiwa» y «gannen» (primer año). Las aplicaciones empresariales que llevan funcionando desde la época de VB6 o Access casi con seguridad tienen tablas de conversión propias enterradas en algún sitio, por lo que este punto suele tratarse junto con el criterio de continuidad que expusimos en «Prolongar la vida útil o migrar aplicaciones VB6 / Access».
2.4 La configuración regional de Windows, un enemigo oculto
Hay otra trampa fácil de pasar por alto. En la configuración regional de Windows, el usuario puede cambiar el tipo de calendario del sistema operativo a «era japonesa». Como CultureInfo.CurrentCulture refleja por defecto el cambio de configuración del usuario (la anulación de usuario),6 en los equipos con esta configuración el DateTime.ToString() con la cultura predeterminada empieza de pronto a devolver cadenas en era japonesa como «Reiwa 08/07/11». Si hay código que escribe fechas en registros, CSV o la base de datos usando el ToString() con la cultura predeterminada, únicamente en ese equipo los datos guardados acaban contaminados con notación de era japonesa, y el análisis posterior se rompe.
La solución llega a la misma conclusión que en el artículo anterior: toda cadena de fecha que cruce un límite (almacenamiento, comunicación, registro) debe escribirse siempre con CultureInfo.InvariantCulture y un formato explícito. El formato predeterminado de la cultura es exclusivo para la visualización en pantalla. Para el efecto de la cultura sobre la visualización en general, también resulta útil «Internacionalización de aplicaciones WinForms/WPF».
3. Los festivos — por qué no codificarlos y cómo gestionar la tabla maestra
3.1 Los festivos «cambian»
No hay que escribir la determinación de festivos como un switch (mes, día). Los festivos están fijados por la Ley de Festivos Nacionales, y han cambiado repetidamente por reformas legales y por la práctica administrativa.2 Solo con los ejemplos recientes basta para verlo.
| Año | Cambio |
|---|---|
| 2016 | Se crea el Día de la Montaña (11 de agosto) |
| 2019 | Con el cambio de era, el cumpleaños del emperador pasa del 23 de diciembre al 23 de febrero. En 2019 no existió el cumpleaños del emperador |
| 2019 | Por una medida especial ligada a la sucesión imperial, el 1 de mayo (día de la entronización), entre otros, se convierte en festivo o día no laborable con carácter extraordinario |
| 2020 | El Día del Deporte y la Salud se renombra Día del Deporte |
| 2020 y 2021 | Por la ley de medidas especiales de los Juegos Olímpicos, el Día del Mar, el Día del Deporte y el Día de la Montaña se desplazan solo en esos años |
Además, el Día del Equinoccio de Primavera y el Día del Equinoccio de Otoño ni siquiera tienen una fecha escrita en la ley. El mecanismo es que se fijan y publican el año anterior a partir de cálculos astronómicos, así que la única forma fiable de conocer con certeza un equinoccio futuro es esperar a la publicación oficial.2 Una fórmula de cálculo que parta de la premisa de que «los festivos son los mismos todos los años» es, en principio, inviable.
3.2 Tabla de decisión sobre fuentes de datos y forma de guardarlos
La determinación de festivos se diseña con «datos más proceso de actualización». Comparamos las principales opciones.
| Método | Contenido | Caso al que se ajusta | Puntos de atención |
|---|---|---|---|
| Tabla maestra de festivos (gestión propia) | Guardar fecha y nombre en una tabla, actualizada desde una pantalla de administración | Sistemas empresariales en general que quieran gestionarla junto con los días de cierre de la empresa | Incorporar la actualización a la operativa del negocio (convertir la revisión anual en un hito del calendario) |
| Importación del CSV del Cabinet Office | Importar el CSV de festivos que publica el Cabinet Office para actualizar la tabla maestra | Cuando se quiere una fuente primaria fiable para actualizar la tabla maestra (recomendado) | Solo contiene el periodo ya publicado. Cuidado con la codificación de caracteres |
| Biblioteca de festivos (NuGet, etc.) | Determinación mediante lógica de cálculo más datos incluidos | Usos a pequeña escala, como herramientas internas, sin capacidad para operar actualizaciones | Las futuras reformas legales dependen de que se actualice la biblioteca. Hay que verificar las reglas incluidas |
| Codificación fija | Escribir las fechas directamente en el código fuente | Ninguno | Cada reforma legal obliga a redistribuir a todos los clientes |
Como fuente primaria puede usarse el CSV con la fecha y el nombre de los festivos desde Showa 30 (1955) hasta el año siguiente al actual, publicado por el Cabinet Office en su página sobre «los festivos nacionales».2 En la importación hay dos precauciones prácticas: (1) como solo se publica el periodo ya confirmado, hay que decidir como especificación qué hacer cuando se pregunte por festivos de dentro de dos años o más (dar error, o rellenar con un valor provisional basado en la ley y marcarlo como «provisional»); (2) la codificación de este CSV es de la familia Shift_JIS, así que la lectura debe declarar explícitamente la codificación, tal como explicamos en «El CSV no es «solo texto»».
Concretemos también la forma de la tabla maestra. Un festivo no es más que «una fecha y su nombre», así que la tabla puede ser sencilla. Conviene añadir dos columnas: indicador de provisionalidad y origen de la importación.
-- Tabla maestra de festivos. El festivo sustitutivo y el "día nacional" también se guardan como una fila cada uno
CREATE TABLE public_holidays (
holiday_date date NOT NULL PRIMARY KEY, -- Una fila por día. La propia fecha es la clave primaria
holiday_name nvarchar(64) NOT NULL, -- El nombre tal cual viene del CSV del Cabinet Office ('元日', '休日', etc.)
is_provisional bit NOT NULL, -- 1 = provisional (fila rellenada de antemano según la ley, antes de la publicación oficial)
source nvarchar(32) NOT NULL, -- Origen de la importación: 'cao-csv' / 'manual', etc.
updated_at datetime2(0) NOT NULL
);
-- Tabla maestra de días de cierre de la empresa. Se guarda separada de los festivos (el motivo, al final de 3.3)
CREATE TABLE company_holidays (
holiday_date date NOT NULL PRIMARY KEY,
reason nvarchar(64) NOT NULL, -- '夏季休業' (vacaciones de verano), '創立記念日' (aniversario de fundación), etc.
updated_at datetime2(0) NOT NULL
);
is_provisional es la clave. Como el CSV del Cabinet Office solo contiene el periodo ya confirmado, en los sistemas que necesiten tratar años posteriores se insertan filas generadas mecánicamente a partir de las reglas legales con is_provisional = 1, y la operativa consiste en sobrescribirlas con la fila definitiva (0) en cuanto se publique. De este modo se puede explicar más adelante «si en la determinación de esta fecha de vencimiento de pago se mezcló algún festivo provisional». A la inversa, en operaciones que no admitan filas provisionales (por ejemplo, la fecha definitiva de una transferencia), basta con que quien consulta filtre por is_provisional = 0; no hace falta separar la tabla.
El lado de la importación tiene esta forma. El CSV del Cabinet Office trae encabezado en la primera línea, y las fechas no llevan ceros a la izquierda, como en 1955/1/1, así que se fija el formato al analizarlas.
using System.Globalization;
using System.Text;
// Lee el syukujitsu.csv publicado por el Cabinet Office en "Acerca de los festivos nacionales".
// Línea 1: encabezado con el nombre de las columnas de fecha y nombre del festivo
// Línea 2 en adelante: 1955/1/1,元日 (Año Nuevo) — la codificación es Shift_JIS (CP932)
public static List<(DateOnly Date, string Name)> ReadCabinetOfficeHolidayCsv(string path)
{
// En .NET (Core)/.NET 5 en adelante, sin este registro GetEncoding lanza una excepción (ver el artículo de CSV, sección 4.2)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var sjis = Encoding.GetEncoding("shift_jis");
var holidays = new List<(DateOnly, string)>();
var isFirstLine = true;
foreach (var line in File.ReadLines(path, sjis))
{
if (isFirstLine) { isFirstLine = false; continue; } // Descartar la línea de encabezado
if (string.IsNullOrWhiteSpace(line)) continue;
var columns = line.Split(',');
if (columns.Length < 2) continue;
// Formato "1955/1/1". Fijar el formato en lugar de confiar en la cultura predeterminada (mismo motivo que en 2.4)
var date = DateOnly.ParseExact(columns[0].Trim(), "yyyy/M/d", CultureInfo.InvariantCulture);
holidays.Add((date, columns[1].Trim()));
}
return holidays;
}
El contenido del proceso de actualización consiste en volcar las filas leídas en public_holidays con is_provisional = 0 y source = 'cao-csv', sobrescribiendo cualquier fila provisional que exista en la misma fecha. El festivo sustitutivo y el «día nacional» ya vienen incluidos desde el principio en el CSV como filas con el nombre «休日» (día festivo).2
3.3 El festivo sustitutivo, el «día nacional» y el cálculo de días hábiles
Además de la lista de fechas, la ley de festivos contiene dos reglas derivadas: el festivo sustitutivo (cuando un festivo cae en domingo, se toma como día no laborable el día «no festivo» más próximo posterior) y el «día nacional» (un día laborable comprendido entre un festivo el día anterior y otro el día siguiente se convierte en día no laborable).2 Como el CSV del Cabinet Office ya incluye estas filas, con el método de importación por CSV basta con consultar la tabla maestra. Aun si se calculan por cuenta propia, esta lógica proviene de la ley y es comparativamente estable.
El cálculo de días hábiles se escribe con tres condiciones: «no es sábado ni domingo», «no está en la tabla maestra de festivos» y «no está en la tabla maestra de días de cierre de la empresa».
public sealed class BusinessDayCalendar
{
private readonly HashSet<DateOnly> _publicHolidays; // Tabla maestra de festivos (incluye el festivo sustitutivo)
private readonly HashSet<DateOnly> _companyHolidays; // Tabla maestra de días de cierre de la empresa
public BusinessDayCalendar(IEnumerable<DateOnly> publicHolidays,
IEnumerable<DateOnly> companyHolidays)
{
_publicHolidays = [.. publicHolidays];
_companyHolidays = [.. companyHolidays];
}
public bool IsBusinessDay(DateOnly d) =>
d.DayOfWeek is not (DayOfWeek.Saturday or DayOfWeek.Sunday)
&& !_publicHolidays.Contains(d)
&& !_companyHolidays.Contains(d);
// Si el vencimiento cae en día no laborable, "retrocede al día hábil anterior". Si se avanza en su lugar, lo decide la especificación del contrato
public DateOnly ToPreviousBusinessDay(DateOnly d)
{
while (!IsBusinessDay(d)) d = d.AddDays(-1);
return d;
}
}
Que los festivos y los días de cierre de la empresa estén en tablas maestras separadas es intencional. Es porque el conjunto de días no laborables que hay que consultar depende del uso: «la determinación del vencimiento de una transferencia bancaria solo mira los festivos (las vacaciones de verano de la propia empresa no importan)», mientras que «la determinación de la fecha de envío mira ambas cosas». Si se mezclan en una sola tabla maestra, una futura modificación para separarlas se convierte en arqueología sobre datos ya guardados.
4. Fecha de cierre, plazo de pago y ejercicio fiscal — tratar correctamente el «fin de mes» con cálculo
4.1 El redondeo de AddMonths
La primera trampa del cálculo de la fecha de cierre es AddMonths. DateTime.AddMonths / DateOnly.AddMonths, cuando el resultado cae en un día que no existe en el mes de destino, redondean al último día de ese mes.3
var jan31 = new DateOnly(2026, 1, 31);
var feb = jan31.AddMonths(1); // 2026-02-28 — se redondea al fin de mes (correcto)
var mar = feb.AddMonths(1); // 2026-03-28 — lo que debía ser "fin de mes" se convierte en el día 28
El redondeo en sí es un comportamiento correcto por especificación, pero si al resultado ya redondeado se le aplica de nuevo AddMonths, se pierde la intención de «fin de mes». Esto se manifiesta cuando el calendario mensual de facturación se construye como «fecha de la última ejecución + 1 mes»: en cuanto se pasa febrero, la ejecución queda convertida para siempre en el día 28. Hay dos medidas.
- Las fechas repetidas se calculan cada vez a partir de la definición de referencia («fin de cada mes», «el día 20 de cada mes»), no a partir del resultado anterior
- El «fin de mes» se deriva de
DateTime.DaysInMonth(y, m), no de un literal de fecha
4.2 Implementación de «cierre el día 20, se paga a fin del mes siguiente»
La fecha de cierre y el plazo de pago son atributos de cada cliente o proveedor. Convertir a código «cierre el día 20, se paga a fin del mes siguiente» queda así.
public static class PaymentTerms
{
// Fecha de la operación → fecha de cierre (ejemplo de cierre el día 20 de cada mes)
public static DateOnly GetClosingDate(DateOnly tradeDate, int closingDay)
{
var dayInMonth = Math.Min(closingDay, DateTime.DaysInMonth(tradeDate.Year, tradeDate.Month));
var closing = new DateOnly(tradeDate.Year, tradeDate.Month, dayInMonth);
return tradeDate <= closing ? closing : closing.AddMonths(1);
// Nota: con closingDay = 31 (cierre a fin de mes), el redondeo de AddMonths produce correctamente el fin del mes siguiente
}
// Fecha de cierre → fecha de vencimiento del pago (ejemplo de pago a fin del mes siguiente). El ajuste por día no laborable depende de la especificación (adelantar/retrasar); confírmela antes de aplicarla
public static DateOnly GetPaymentDueDate(DateOnly closingDate, BusinessDayCalendar calendar)
{
var nextMonth = closingDate.AddMonths(1);
var endOfMonth = new DateOnly(nextMonth.Year, nextMonth.Month,
DateTime.DaysInMonth(nextMonth.Year, nextMonth.Month));
return calendar.ToPreviousBusinessDay(endOfMonth); // Para el caso de un contrato de "si es no laborable, al día hábil anterior"
}
}
Justo después de advertir contra AddMonths en 4.1, este código lo usa en dos sitios. No es una contradicción; conviene aclarar por qué aquí es seguro. Lo peligroso en 4.1 era tomar el resultado ya redondeado como entrada directa del siguiente cálculo. Ninguno de los dos casos de arriba hace eso.
GetClosingDatereconstruye cada vez la fecha de cierre del mes a partir de la fecha de la operación.AddMonths(1)se usa una sola vez, para «pasar a la fecha de cierre del mes siguiente porque ya se superó la de este mes»; no reutiliza el valor devuelto la vez anterior. Además, cuandoclosingDay = 31(cierre a fin de mes), aplicarAddMonths(1)a una fecha de cierre construida a partir del 31 de enero da el 28 de febrero, y este es precisamente un caso en el que el resultado del redondeo coincide exactamente con el sentido de «cierre a fin de mes». Es decir, aquí no se anula el redondeo, sino que se aprovecha.GetPaymentDueDatesolo lee el año y el mes del resultado deAddMonths(1). El día se reconstruye conDateTime.DaysInMonth, así que aunque el redondeo haya dejado el día en 28, no afecta al resultado. Para una fecha de cierre del 31 de enero se obtiene, como «fin de febrero», el 28 de febrero; para una del 31 de agosto se obtiene, como «fin de septiembre», el 30 de septiembre.
En resumen, AddMonths puede usarse con seguridad si se limita a la finalidad de «avanzar el año y el mes en uno», y el día siempre se reconstruye a partir de la definición de referencia (fin de mes, día 20 de cada mes). El movimiento prohibido es «seguir sumando al resultado anterior».
Más importante que el código es la fijación previa de la especificación. El «fin de mes» de un «cierre a fin de mes», ¿es el fin de mes calendario o el fin de mes hábil? Cuando el vencimiento cae en día no laborable, ¿se adelanta o se retrasa? (la costumbre difiere según la posición: quien paga suele adelantar, y la fecha de cobro prevista de quien recibe suele retrasarse). Un contrato con cierre entre los días 29 y 31, ¿cómo se interpreta en febrero? Cuando esto no está documentado, el código de quien lo implementa se convierte, tal cual, en «una especificación que nadie ha acordado». Buena parte de las investigaciones de errores en torno a la fecha de cierre acaban llegando, no a un fallo del código, sino a la ausencia de especificación.
4.3 Ejercicio fiscal y trimestre
El «ejercicio fiscal» de las aplicaciones empresariales japonesas empieza en abril en la inmensa mayoría de los casos. El ejercicio fiscal no se guarda: se trata como un valor derivado de la fecha.
public static int GetFiscalYear(DateOnly d) => d.Month >= 4 ? d.Year : d.Year - 1;
// 2026-03-31 → ejercicio 2025, 2026-04-01 → ejercicio 2026
public static int GetFiscalQuarter(DateOnly d) => ((d.Month + 8) % 12) / 3 + 1;
// abril-junio → T1, julio-septiembre → T2, octubre-diciembre → T3, enero-marzo → T4
El punto de atención es no guardar de forma redundante el «ejercicio fiscal» en la base de datos sin necesidad. Si un valor que siempre se puede derivar de la fecha se guarda además en una columna aparte, un olvido al corregir la fecha genera datos en los que la fecha y el ejercicio fiscal ya no coinciden. Si por rendimiento de la agregación resulta imprescindible mantenerlo, hay que convertirlo en una columna calculada (columna generada) y prohibir que la aplicación escriba en ella directamente.
4.4 Pruebas — atajar primero los errores que «solo» ocurren en cierta fecha
Los errores de era japonesa, festivos y fechas de cierre solo se manifiestan en fechas concretas. Partiendo de la inyección de la hora actual mediante TimeProvider que presentamos en el artículo anterior (capítulo 7 de «Fecha, hora y zona horaria en aplicaciones empresariales»), estas son las cinco fechas mínimas que conviene probar en este ámbito.
- Frontera de cambio de era: la visualización del 7/8-01-1989 (Showa → Heisei) y del 30-04/01-05-2019 (Heisei → Reiwa), y la conversión inversa de una entrada en era japonesa
- Primer año de era: comprobación de la notación «Reiwa gannen» (primer año) en los informes (sección 2.2)
- Año bisiesto: cálculos de cierre y plazo de pago que incluyan el 29 de febrero (2024, 2028)
- Encadenamiento de fines de mes: generar la fecha de cierre de 12 meses consecutivos a partir del 31 de enero y comprobar que el fin de mes se mantiene (sección 4.1)
- Frontera de ejercicio fiscal: la determinación del ejercicio fiscal y del trimestre para el 31 de marzo y el 1 de abril
Además, en la revisión de código se pueden detectar mecánicamente con grep los siguientes patrones.
| Código encontrado → sospeche | Qué puede pasar | Cómo corregirlo |
|---|---|---|
Literales de era como "平成" (Heisei), "令和" (Reiwa) |
Se quedan atrás en el próximo cambio de era | Pasar a JapaneseCalendar con el formato g (sección 2.3) |
Literales de fecha de festivos (por ejemplo, 5, 3) |
Determinación errónea por reforma legal o ley de medidas especiales | Pasar a consultar la tabla maestra de festivos (sección 3.2) |
Rutas de guardado o salida con ToString() sin argumentos |
El equipo con configuración en era japonesa contamina los datos guardados | InvariantCulture más formato explícito (sección 2.4) |
Repetición de fechaAnterior.AddMonths(1) |
El fin de mes sigue desplazándose tras pasar febrero | Calcular cada vez desde la fecha de referencia (sección 4.1) |
new DateTime(y, m, 31) |
Excepción en meses de 30 días o menos | Derivarlo de DateTime.DaysInMonth |
5. Resumen
La era japonesa, los festivos y las fechas de cierre son, todos ellos, procesos que tratan con «especificaciones que cambian más adelante». Por eso el principio de diseño también es común a los tres: lo que cambia no se escribe en el código, sino que se aísla en los datos y en la capa de conversión. El interior y el almacenamiento se unifican en calendario occidental, dejando la era japonesa como una conversión exclusiva de la visualización; los festivos se gestionan con una tabla maestra y un proceso de actualización; y el «fin de mes», el «día hábil» y el «ajuste del vencimiento en día no laborable» se documentan como especificación contractual antes de calcularse a partir de una fecha de referencia. Con esto resuelto, tanto el próximo cambio de era como la próxima reforma de la ley de festivos se convierten, para la aplicación, en eventos que se resuelven con «una actualización de la tabla maestra» y «la aplicación de actualizaciones del sistema operativo».
En nuestra empresa nos encargamos del diseño e implementación del tratamiento de fechas en informes y facturación, del inventario y la modificación de los nombres de era y festivos codificados de forma fija en aplicaciones existentes, y de revisiones de diseño que incluyen la clarificación de la especificación de las fechas de cierre. Podemos ayudar incluso desde la fase de «no sabemos cuántos puntos de adaptación al cambio de era tenemos ni dónde están».
Artículos relacionados
- Fecha, hora y zona horaria en aplicaciones empresariales — de las trampas de DateTime al principio de UTC y el diseño de pruebas
- El CSV no es «solo texto»: guía práctica del manejo de CSV en aplicaciones empresariales con C# (codificación de caracteres, compatibilidad con Excel, protección contra inyección)
- Internacionalización de aplicaciones WinForms/WPF: la práctica de resx, ensamblados satélite y cambio de cultura
- Prolongar la vida útil o migrar aplicaciones VB6 / Access — Tabla de decisión: mantener, envolver o reemplazar
- Cómo crear la salida de informes de Excel - COM/Open XML/plantillas
Áreas de consultoría relacionadas
KomuraSoft LLC se ocupa del diseño e implementación del tratamiento de fechas de las aplicaciones empresariales conforme a los usos comerciales japoneses (informes en era japonesa, cálculo de días hábiles, tratamiento de fechas de cierre), así como del inventario y la modificación de la adaptación al cambio de era y a los festivos en aplicaciones existentes.
- Desarrollo de aplicaciones Windows
- Modificación y mantenimiento de software Windows existente
- Consultoría técnica y revisión de diseño
- Contacto
Referencias
-
Microsoft Learn, Work with eras - .NET. Sobre que JapaneseCalendar y JapaneseLunisolarCalendar son las únicas clases de calendario que reconocen varias eras, el formateo de la era mediante el especificador «g», y que la información de era la suministra la plataforma en lugar de depender de código fijo en la aplicación. ↩ ↩2 ↩3
-
内閣府 (Cabinet Office / Gabinete de Japón), 「国民の祝日」について. Sobre la lista de festivos basada en la Ley de Festivos Nacionales, que el Día del Equinoccio de Primavera y el de Otoño se fijan y publican el año anterior, las reglas del festivo sustitutivo y del «día nacional», y la disponibilidad de un CSV con la fecha y el nombre de los festivos desde Showa 30 hasta el año siguiente. ↩ ↩2 ↩3 ↩4 ↩5 ↩6
-
Microsoft Learn, DateTime.AddMonths(Int32) Method. Sobre que, cuando el día del resultado excede el número de días del mes resultante, se ajusta al último día de ese mes (por ejemplo, el 31 de enero más un mes da el 28 o el 29 de febrero). ↩ ↩2
-
Microsoft Learn, JapaneseCalendar Class. Sobre que el rango de fechas admitido por JapaneseCalendar es desde el 8 de septiembre de la era Meiji 1 (1868) en adelante, y sobre el tratamiento de las eras. ↩
-
Microsoft Learn, Work with calendars - .NET. Sobre el comportamiento predeterminado que formatea el primer año de cada era como «gannen» (primer año) y el conmutador Switch.System.Globalization.FormatJapaneseFirstYearAsANumber para volver a la notación numérica, y sobre que la comprobación del rango de años de era está relajada por defecto y puede endurecerse con Switch.System.Globalization.EnforceJapaneseEraYearRanges. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, CultureInfo.CurrentCulture Property. Sobre que CurrentCulture refleja por defecto los cambios de configuración regional del usuario en Windows (la anulación de usuario), y que el valor predeterminado del formateo depende de esta cultura. ↩
Artículos relacionados
Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.
Cuando su aplicación Windows de desarrollo propio es tratada como virus — cómo abordar los falsos positivos de Microsoft Defender y su impacto en el rendimiento
Procedimiento oficial para resolver falsos positivos de Microsoft Defender en apps Windows propias: por qué ocurren, cómo reportarlos, re...
¿Las aplicaciones empresariales funcionan en Windows para Arm? — La realidad de la emulación x64 (Prism), las DLL nativas y COM
Respondemos si las aplicaciones empresariales funcionan en Windows para Arm: la emulación x64 (Prism), las capas que no funcionan (contro...
Iconos de la bandeja del sistema y notificaciones toast en aplicaciones Windows — los escollos de NotifyIcon y cómo elegir el AppNotification adecuado
Organiza la implementación de la residencia en la bandeja del sistema y las notificaciones toast en aplicaciones Windows empresariales: e...
¿Hasta cuándo seguirán funcionando las aplicaciones VB6? — Estado del soporte del runtime y un camino práctico hacia la migración a .NET
¿Hasta cuándo seguirán funcionando las aplicaciones VB6? Este artículo organiza la situación actual: el runtime sigue dentro del soporte ...
Buenas prácticas de multithreading en la práctica — Edición .NET: qué decidir antes de aumentar los hilos
Reglas de diseño en .NET/C# para evitar fallos y bloqueos intermitentes con hilos: usar Task en lugar de hilos propios, reducir el estado...
Temas relacionados
Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.
Temas técnicos de Windows
Portal sobre desarrollo de Windows, investigación de fallos y aprovechamiento de activos existentes.
Servicios relacionados con este tema
El artículo está directamente relacionado con los siguientes servicios.
Desarrollo de aplicaciones para Windows
Implementar requisitos empresariales propios de Japón, como la visualización de fechas en la era japonesa en los informes, el cálculo de días hábiles y el tratamiento de fechas de cierre, forma parte del núcleo de la consultoría de desarrollo de aplicaciones empresariales Windows.
Mantenimiento y modernización de software Windows
Inventariar los nombres de era y festivos codificados de forma fija en una aplicación existente, y reformarlos para que resistan el próximo cambio de era o reforma legal, corresponde a la modificación y el mantenimiento de software Windows existente.
Consultoría técnica y revisión de diseño
Definir hasta qué punto formalizar y gestionar mediante datos conceptos como «fin de mes», «día hábil» o «ejercicio fiscal» es una consultoría técnica que implica una revisión de diseño.
Preguntas frecuentes
Preguntas habituales en las consultas sobre el tema del artículo.
- ¿Está mal guardar las fechas directamente en formato de era japonesa en la base de datos?
- No lo recomendamos, por cuatro razones. Primero, cadenas de era como «Heisei 9» y «Reiwa 9» pierden todo orden coherente al compararse como texto en cuanto se cruza una frontera de era, lo que hace imposible ordenar o hacer búsquedas por rango. Segundo, en el momento en que llega el siguiente cambio de era, las notaciones antiguas y nuevas acaban mezcladas en los datos ya almacenados. Tercero, es fácil que se cuelen datos corruptos con fechas que nunca existieron, como «Showa 64, 10 de enero» (ese día ya era Heisei 1, puesto que la era cambió el 8 de enero), convirtiéndose en una bomba de tiempo para el análisis. Cuarto, JapaneseCalendar de .NET solo admite fechas desde el 8 de septiembre de la era Meiji 1 (1868); las anteriores —por ejemplo, una fecha de nacimiento antigua procedente de un registro familiar— no pueden tratarse correctamente si se mantienen en notación de era. La norma es unificar el procesamiento interno y el almacenamiento en el calendario gregoriano, y convertir a la era japonesa únicamente en el momento de la visualización o la impresión.
- ¿Cómo debería prepararse una aplicación para el próximo cambio de era?
- La definición de los nombres de era no está codificada de forma fija en el propio .NET: el mecanismo hace referencia a la información de era que proporciona el sistema operativo, de modo que basta con seguir aplicando las actualizaciones del sistema operativo y de .NET para que la capacidad de «mostrar la nueva era» se siga automáticamente. En la transición a Reiwa, de hecho, el grueso de la respuesta consistió simplemente en aplicar actualizaciones de la plataforma. El verdadero problema está en el código y los datos del lado de la aplicación: ramas que codifican de forma fija «Heisei» o «Reiwa» como cadenas de texto, funciones de conversión propias con su propia tabla de correspondencia entre era y año gregoriano, nombres de era escritos directamente en las plantillas de los informes, o campos «Reiwa __» en formularios preimpresos; nada de esto se actualiza automáticamente. Cuatro medidas prácticas resultan razonables: (1) unificar internamente y en el almacenamiento sobre el calendario gregoriano, de modo que la era japonesa quede reservada a la visualización; (2) dejar de mantener una lógica de conversión de era propia y delegarla en JapaneseCalendar; (3) recorrer todo el código fuente con grep buscando «Showa», «Heisei» y «Reiwa» para hacer inventario; y (4) preparar pruebas que simulen un cambio de era hipotético.
- ¿No se pueden determinar los festivos con una fórmula de cálculo? ¿Por qué es necesario guardarlos como datos?
- Una fórmula completa es, en principio, imposible de construir, por tres razones. Primero, el Día del Equinoccio de Primavera y el del Equinoccio de Otoño se fijan el año anterior mediante decisión del gabinete basada en cálculos astronómicos; no están recogidos en la ley como fechas fijas. Segundo, los festivos se añaden, se mueven o se renombran mediante reformas legales: en 2016 se creó el Día de la Montaña, en 2019 el cumpleaños del emperador pasó del 23 de diciembre al 23 de febrero a raíz del cambio de era (**en 2019 no hubo cumpleaños del emperador**), y el Día del Deporte y la Salud pasó a llamarse Día del Deporte. Tercero, en 2020 y 2021, una ley de medidas especiales ligada a los Juegos Olímpicos desplazó el Día del Mar, el Día del Deporte y el Día de la Montaña **solo para esos años**, lo que demuestra que incluso una regla como «siempre el tercer lunes» puede quedar anulada de forma puntual. Por tanto, el diseño correcto es una «tabla maestra de festivos más un proceso de actualización», dejando la lógica de cálculo limitada a reglas auxiliares como el festivo sustitutivo.
- Mi fecha de vencimiento de pago se desplazó del fin de mes después de usar AddMonths(1). ¿Por qué?
- Porque DateTime.AddMonths está especificado para redondear al último día del mes resultante cuando el cálculo ingenuo daría un día que no existe en ese mes. Aplicar AddMonths(1) al 31 de enero da el 28 de febrero (o el 29 en años bisiestos); ese comportamiento en sí es correcto. Pero si después arrastra ese resultado redondeado a un nuevo AddMonths, aplicar AddMonths(1) al 28 de febrero da el 28 de marzo, y a partir de ahí el desplazamiento continúa de forma indefinida: lo que debía ser «fin de mes» se convierte silenciosa y permanentemente en «el día 28». La solución tiene dos partes: para cálculos repetidos, calcular siempre a partir de un punto de referencia fijo (la fecha del contrato, o la definición de «fin de cada mes»), en lugar de sumar al resultado anterior, y derivar «fin de mes» a partir de DateTime.DaysInMonth en lugar de un literal de fecha.
Perfil del autor
Página de presentación del autor del artículo.
Go Komura
Representante de KomuraSoft LLC
Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.