Японская эра, праздники и даты закрытия периода в бизнес-приложениях — устойчивый к смене эры дизайн, JapaneseCalendar и расчёт рабочих дней на практике
· Го Комура · C#, .NET, Японская эра (вареки), Праздники, Дата закрытия периода, Обработка даты и времени, Бизнес-приложения, Разработка Windows, Техническая консультация
«Дата в отчёте должна быть указана в японской эре». «Оплата — при закрытии периода 20-го числа, платёж до конца следующего месяца, а если последний день выпадает на выходной — в предыдущий рабочий день». «Итоги должны агрегироваться по финансовым годам» — бизнес-приложения в Японии в стандартной комплектации несут требования к датам, которые нигде больше в мире не имеют смысла. И в этой области скрыта совсем другая разновидность ловушек, чем при обычной обработке даты и времени. Названия эры множатся с каждой сменой, праздники сдвигаются при каждом изменении законодательства, а «конец месяца» имеет разную длину в зависимости от месяца. Иными словами, всё это — «спецификации, которые меняются позже».
В прошлый раз, в статье «Дата, время и часовые пояса в бизнес-приложениях», мы разобрали принцип хранения в UTC и работу с часовыми поясами. На этот раз, в продолжение темы, разберём три главных специфичных для Японии требования: японскую эру, праздники и даты закрытия периода. Рассмотрим устойчивый к смене эры дизайн, причины, по которым праздники нельзя жёстко прописывать в коде, и как хранить их как данные, а также расчёт дат закрытия периода с учётом округления AddMonths — всё с примерами кода и таблицами решений.
1. Сначала — главный вывод
- Внутренняя обработка и хранение остаются в григорианском календаре; японская эра существует только в момент отображения. Хранение строк в японской эре оборачивается тройной проблемой: невозможность сортировки, смешение обозначений после смены эры и риск проникновения некорректных дат.
- Делегируйте преобразование эры
JapaneseCalendar, а не самописной таблице соответствия. Определения эры предоставляются операционной системой, поэтому для соответствия смене эры достаточно обновления платформы.1 Отстают только жёстко прописанные в приложении строки «Хэйсэй» или «Рэйва». - Праздники нельзя определить формулой. Дни весеннего и осеннего равноденствия фиксируются лишь в предыдущем году, а сами праздники могут переноситься обычным изменением законодательства или специальным законом.2 Проектируйте систему вокруг справочника праздников плюс процесса обновления, а в качестве источника данных можно использовать CSV с праздниками, публикуемый Cabinet Office Японии.
- «Конец месяца», «рабочий день» и «финансовый год» должны быть определены в спецификации раньше, чем в коде. В частности,
AddMonthsавтоматически округляет конец месяца, поэтому расчёт даты закрытия периода всегда должен вестись от фиксированной даты отсчёта.3
Сначала — сводка о том, как хранить каждый вид данных.
| Данные | Способ хранения | Момент преобразования/оценки |
|---|---|---|
| Сама дата (дата заказа, срок платежа) | Григорианские DateOnly / DateTime (в БД — колонка типа date) |
— |
| Обозначение в японской эре (отчёты, экраны) | Не хранить | Преобразование через JapaneseCalendar только в момент отображения/печати |
| Праздники | Справочник праздников (таблица или файл конфигурации) | Обращение при расчёте рабочих дней |
| Корпоративные выходные (летний перерыв, годовщина основания) | Отдельный от праздников справочник выходных | То же самое (не смешивать с праздниками) |
| Дата закрытия периода / условия оплаты | Атрибут в карточке контрагента («закрытие 20-го числа, оплата до конца следующего месяца») | Расчёт от даты отсчёта при выставлении счёта |
| Финансовый год | Не хранить (производное значение) | Расчёт из даты при отображении/агрегации |
2. Японская эра — оставить только для отображения
2.1 Почему внутри следует унифицировать на григорианском календаре
Японская эра — это обозначение, ориентированное на человека, а не представление, пригодное для вычислений. У «Хэйсэй 9» и «Рэйва 9» нет определённого порядка при сравнении строк, а любой расчёт периода, пересекающего границу эры, всё равно в итоге требует преобразования в григорианский календарь. Более того, система эр расширяется с каждой сменой, поэтому данные, оставшиеся в формате эры, накапливают смесь старых и новых обозначений при каждом переходе. Проникновение некорректных данных вроде «Сёва 64, 10 января» (никогда не существовавшей даты, поскольку с 8 января уже наступила Хэйсэй) — классическая проблема систем, хранящих введённую эру «как есть».
Есть ещё одно легко упускаемое ограничение: JapaneseCalendar в .NET поддерживает только даты начиная с 8 сентября 1-го года Мэйдзи (1868 года).4 Более ранние даты — например, старая дата рождения из семейного реестра — не могут корректно обрабатываться, если остаются в формате эры. Унифицировав внутреннюю обработку на григорианском календаре, все эти проблемы удаётся замкнуть исключительно в слое отображения.
2.2 Отображение и ввод с помощью JapaneseCalendar
Единственные классы календаря в .NET, способные работать с несколькими эрами, — это JapaneseCalendar (и JapaneseLunisolarCalendar), а спецификатор формата g соответствует названию эры.1 Минимальный код для отображения даты в японской эре выглядит так:
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
Для ввода данных в японской эре безопаснее спроектировать интерфейс, в котором пользователь выбирает эру из выпадающего списка, а год, месяц и день вводит цифрами, а не разбирать произвольную текстовую строку. Если свободный ввод текста всё же необходим, зафиксируйте формат через 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
Обозначение первого года эры требует внимания. В окружениях, где применены обновления для поддержки эры, первый год каждой эры по умолчанию форматируется как «Рэйва Ган-нэн» (元年, буквально «год основания»), а не числом, и предусмотрен переключатель совместимости — Switch.System.Globalization.FormatJapaneseFirstYearAsANumber — для возврата к числовому обозначению «Рэйва 1».5 Перед реализацией проверьте как часть спецификации, требует ли формат отчёта «Рэйва 1» или «Рэйва Ган-нэн». Ещё один момент: по умолчанию проверка диапазона годов для каждой эры ослаблена, поэтому несуществующее значение вроде «Хэйсэй 33» принимается и преобразуется в реально соответствующую дату (Рэйва 3). Если нужна строгая валидация ввода, отклоняющая такие значения, её можно включить через Switch.System.Globalization.EnforceJapaneseEraYearRanges.5 Оставить ли проверку ослабленной или сделать строгой, зависит от того, насколько «грязны» существующие данные, — в частности, как вы хотите обрабатывать значение вроде «Хэйсэй 32», напечатанное или введённое до смены эры.
2.3 Код, устойчивый к смене эры, и то, что остаётся позади
Из опыта перехода к эре Рэйва (2019 год) можно сделать вывод: платформа поспевает, а собственная реализация приложения — нет. Определение названий эры не зашито жёстко в .NET — механизм обращается к информации об эре, которую хранит операционная система, поэтому, продолжая устанавливать обновления ОС и .NET, преобразование и форматирование JapaneseCalendar автоматически следуют новой эре.1 Позади остаются такие ресурсы на стороне приложения, как:
- Самописные таблицы или ветвления преобразования эры, например
if (year >= 2019) era = "令和"; - Названия эры, записанные прямо в шаблоны отчётов, форматы ячеек Excel или предварительно напечатанные бланки
- Код, обрабатывающий по собственным правилам однобуквенное сокращение эры, такое как «R» или «H» (часто встречается в форматах обмена с базовыми системами)
- Сами хранимые данные, оставшиеся в формате японской эры
При оценке трудозатрат на доработку в связи со сменой эры начните с механического поиска по всему исходному коду и шаблонам через grep со словами «Сёва», «Хэйсэй», «Рэйва» и «Ган-нэн». В бизнес-приложениях, работающих ещё со времён VB6 или Access, почти наверняка где-то зарыта самописная таблица преобразования, поэтому этот вопрос часто рассматривается вместе с решением о продлении жизни приложения, описанным в статье «Продление жизни или миграция бизнес-приложения на VB6 / Access».
2.4 Скрытая засада: региональные настройки Windows
Ещё одна легко упускаемая ловушка. В региональных настройках Windows пользователь может переключить тип календаря ОС на японскую эру. Поскольку CultureInfo.CurrentCulture по умолчанию отражает изменение настроек пользователя (так называемое «переопределение пользователя»),6 на машине с этой настройкой DateTime.ToString() со стандартным форматом культуры внезапно начинает возвращать строки в японской эре, например «令和08/07/11». Если код записывает даты в журналы, CSV или базу данных через стандартный для культуры ToString(), то только на этой конкретной машине японская эра проникает в сохранённые данные, ломая последующий парсинг.
Решение сводится к тому же выводу, что и в предыдущей статье: любая строка с датой, пересекающая границу — хранение, передачу данных, журналирование — должна всегда записываться с использованием CultureInfo.InvariantCulture и явного формата. Формат культуры по умолчанию предназначен только для отображения на экране. Более широкий взгляд на влияние культуры на отображение см. также в статье «Локализация приложений WinForms/WPF».
3. Праздники — почему их нельзя жёстко прописывать в коде, и как вести справочник
3.1 Праздники «меняются»
Никогда не пишите определение праздников в виде switch (месяц, день). Праздники установлены Законом о национальных праздниках, и они неоднократно менялись из-за законодательных изменений и правоприменительной практики.2 Вот только недавние примеры:
| Год | Изменение |
|---|---|
| 2016 | Учреждён День гор (11 августа) |
| 2019 | В связи со сменой эры день рождения императора переносится с 23 декабря на 23 февраля. В 2019 году дня рождения императора не было вовсе |
| 2019 | По специальному режиму, связанному с престолонаследием, 1 мая (день интронизации) и другие даты становятся временными праздниками |
| 2020 | День здоровья и спорта переименован в День спорта |
| 2020–2021 | По специальному олимпийскому закону День моря, День спорта и День гор перенесены только на эти годы |
Кроме того, у Дня весеннего и Дня осеннего равноденствия попросту нет даты, закреплённой в законе. Они фиксируются и объявляются в предыдущем году на основе астрономических расчётов, поэтому единственный надёжный способ узнать будущее равноденствие — дождаться официального объявления.2 Формула расчёта, предполагающая, что «праздники одинаковы каждый год», принципиально неработоспособна.
3.2 Таблица решений для источников данных и способа хранения
Проектируйте определение праздников как «данные плюс процесс обновления». Сравнение основных вариантов:
| Подход | Содержание | Подходящие случаи | Замечания |
|---|---|---|---|
| Справочник праздников (собственное ведение) | Таблица с датой и названием, обновляемая через экран администрирования | Бизнес-системы в целом, которые хотят вести его вместе с корпоративными выходными | Встроить обновление в операционный процесс (превратить ежегодную проверку в напоминание в календаре) |
| Импорт CSV от Cabinet Office | Импорт CSV с праздниками, публикуемого Cabinet Office, для обновления справочника | Когда нужен авторитетный первичный источник для обновлений справочника (рекомендуется) | Содержит только уже опубликованный период. Внимание к кодировке символов |
| Библиотека праздников (NuGet и т. п.) | Определение через логику расчёта плюс встроенные данные | Небольшие сценарии, например внутренние инструменты, не способные поддерживать процесс обновления | Будущие изменения законодательства зависят от обновления библиотеки. Нужно проверять встроенные правила |
| Жёсткое кодирование | Даты записаны прямо в исходном коде | Нет | Каждое изменение законодательства требует переустановки на всех клиентах |
В качестве первичного источника можно использовать CSV с датами и названиями праздников с 30-го года Сёва (1955) по следующий год, публикуемый Cabinet Office на странице «О национальных праздниках».2 При импорте важны две практические предосторожности: (1) поскольку публикуется только уже подтверждённый период, определите как часть спецификации поведение при запросе праздника на два и более года вперёд (вернуть ошибку либо заполнить предварительным значением на основе закона с флагом «предварительно»); и (2) кодировка этого CSV относится к семейству Shift_JIS, поэтому при чтении нужно явно указывать кодировку, как описано в статье «CSV — это не просто «текст»».
3.3 Перенос выходного дня, национальный праздник и расчёт рабочих дней
Помимо списка дат, закон о праздниках определяет два производных правила: перенос выходного дня (если праздник выпадает на воскресенье, ближайший следующий день, который сам не является праздником, становится выходным) и национальный праздник (будний день, зажатый между двумя праздниками, становится выходным).2 CSV от Cabinet Office включает их тоже в виде строк, поэтому при подходе с импортом CSV достаточно обращаться к справочнику. Даже если вы реализуете эту логику самостоятельно, она относительно стабильна, поскольку опирается на закон.
Пишите расчёт рабочих дней с тремя условиями: не суббота и не воскресенье, не входит в справочник праздников, не входит в справочник корпоративных выходных.
public sealed class BusinessDayCalendar
{
private readonly HashSet<DateOnly> _publicHolidays; // Справочник праздничных дней (включая перенесённые выходные)
private readonly HashSet<DateOnly> _companyHolidays; // Справочник нерабочих дней компании
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);
// Если срок выпадает на выходной, переносится на предыдущий рабочий день. Перенос вперёд определяется условиями договора
public DateOnly ToPreviousBusinessDay(DateOnly d)
{
while (!IsBusinessDay(d)) d = d.AddDays(-1);
return d;
}
}
Разделение праздников и корпоративных выходных на отдельные справочники сделано намеренно. «Определение срока банковского перевода смотрит только на государственные праздники (наш летний перерыв тут ни при чём)», «дата отгрузки смотрит на оба» — набор нерабочих дней, который следует учитывать, зависит от сценария использования. Если объединить их в одну таблицу, разделение позже превратится в археологические раскопки в уже сохранённых данных.
4. Дата закрытия периода, условия оплаты, финансовый год — корректная обработка «конца месяца» в расчётах
4.1 Поведение округления AddMonths
Первая ловушка в расчёте даты закрытия периода — AddMonths. DateTime.AddMonths / DateOnly.AddMonths округляют до последнего дня получившегося месяца, когда наивный расчёт дал бы день, которого в этом месяце не существует.3
var jan31 = new DateOnly(2026, 1, 31);
var feb = jan31.AddMonths(1); // 2026-02-28 ── округляется до конца месяца (правильно)
var mar = feb.AddMonths(1); // 2026-03-28 ── то, что задумывалось как «конец месяца», незаметно превращается в 28-е число
Само округление специфицировано корректно, но если применить очередной AddMonths к уже округлённому результату, замысел «конца месяца» теряется. На практике это проявляется, когда ежемесячный график выставления счетов строится как «дата предыдущего запуска плюс месяц»: как только проходит февраль, расчёт начинает бесповоротно смещаться на 28-е число каждого месяца. Есть два противодействия:
- Для повторяющихся дат всегда вычислять от определения точки отсчёта («конец каждого месяца», «20-е число каждого месяца»), а не от предыдущего результата
- Выводить «конец месяца» через
DateTime.DaysInMonth(y, m), а не через литерал даты
4.2 Реализация «закрытие 20-го числа, оплата до конца следующего месяца»
Дата закрытия периода и условия оплаты — это атрибуты каждого конкретного контрагента. Реализация «закрытие 20-го числа, оплата до конца следующего месяца» в коде выглядит так:
public static class PaymentTerms
{
// Дата операции → дата закрытия (пример: закрытие 20-го числа каждого месяца)
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);
// Примечание: closingDay=31 (закрытие в конце месяца) корректно становится концом следующего месяца благодаря округлению AddMonths
}
// Дата закрытия → срок оплаты (пример: оплата в конце следующего месяца). Перед применением проверьте спецификацию корректировки на выходные (перенос назад/вперёд)
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); // Для договоров с правилом «если выходной — перенести на предыдущий рабочий день»
}
}
Важнее кода — предварительное закрепление спецификации. При «закрытии в конце месяца» конец месяца считается по календарным дням или по рабочим? Если срок выпадает на выходной, переносится ли он раньше или позже (у плательщика обычно принято переносить раньше, у получателя — переносить ожидаемую дату поступления позже; практика различается в зависимости от стороны)? Как трактовать в феврале договор с закрытием на 29–31-е число? Пока эти моменты не задокументированы, код разработчика становится, как есть, «спецификацией, которую никто не согласовывал». В большинстве случаев расследование багов вокруг даты закрытия периода приводит не к ошибке в коде, а к отсутствующей спецификации.
4.3 Финансовый год и квартал
В Японии «финансовый год» бизнес-приложения почти всегда начинается с апреля. Не храните его — обращайтесь с ним как со значением, производным от даты.
public static int GetFiscalYear(DateOnly d) => d.Month >= 4 ? d.Year : d.Year - 1;
// 2026-03-31 → 2025 финансовый год, 2026-04-01 → 2026 финансовый год
public static int GetFiscalQuarter(DateOnly d) => ((d.Month + 8) % 12) / 3 + 1;
// апр–июн → Q1, июл–сен → Q2, окт–дек → Q3, янв–мар → Q4
Следите, чтобы не хранить финансовый год избыточно и без необходимости в базе данных. Хранение в отдельной колонке значения, которое всегда можно вывести из даты, создаёт риск: при исправлении даты с забытым обновлением получаются данные, где дата и финансовый год расходятся. Если по соображениям производительности агрегации хранить его всё же необходимо, сделайте его вычисляемой (генерируемой) колонкой и запретите приложению записывать в неё напрямую.
4.4 Тестирование — заранее ловить баги, которые проявляются «только в этот день»
Баги, связанные с эрой, праздниками и датами закрытия периода, проявляются только в конкретные даты. Опираясь на внедрение текущего времени через TimeProvider, представленное в предыдущей статье (глава 7 статьи «Дата, время и часовые пояса в бизнес-приложениях»), вот минимум пять дат, которые стоит протестировать в этой области:
- Границы смены эры: отображение и обратное преобразование ввода в японской эре около 1989-01-07/08 (Сёва → Хэйсэй) и 2019-04-30/05-01 (Хэйсэй → Рэйва)
- Первый год эры: проверка отображения «Рэйва Ган-нэн» в отчётах (раздел 2.2)
- Високосные годы: расчёты закрытия периода и срока платежа, охватывающие 29 февраля (2024, 2028)
- Цепочка концов месяца: генерация двенадцати месяцев дат закрытия периода начиная с 31 января с проверкой, что конец месяца сохраняется (раздел 4.1)
- Границы финансового года: определение финансового года и квартала для 31 марта и 1 апреля
Кроме того, при код-ревью следующие паттерны можно механически отлавливать через grep.
| Найдено — стоит насторожиться | Что может произойти | Как исправить |
|---|---|---|
Литералы эры вроде "平成" или "令和" |
Отстанет при следующей смене эры | Перейти на JapaneseCalendar плюс формат g (раздел 2.3) |
Литералы дат праздников (например, 5, 3) |
Ошибочное определение после изменения законодательства или специального закона | Перейти на обращение к справочнику праздников (раздел 3.2) |
Путь сохранения/вывода, использующий ToString() без аргументов |
Японская эра проникает в сохранённые данные на машине с настройкой отображения в эре | InvariantCulture плюс явный формат (раздел 2.4) |
Повторение предыдущаяДата.AddMonths(1) |
Конец месяца продолжает смещаться после прохождения февраля | Каждый раз вычислять от даты отсчёта (раздел 4.1) |
new DateTime(y, m, 31) |
Выбрасывает исключение для месяцев с 30 и менее днями | Выводить через DateTime.DaysInMonth |
5. Заключение
Японская эра, праздники и даты закрытия периода объединяет одно: все они имеют дело со «спецификациями, которые меняются позже». Поэтому принцип проектирования для всех трёх одинаков: не записывать в код то, что может измениться, а изолировать это в данных и в слое преобразования. Унифицируйте внутреннюю обработку и хранение на григорианском календаре, оставив японскую эру только преобразованием для отображения; ведите праздники в справочнике с процессом обновления; документируйте «конец месяца», «рабочий день» и «корректировку срока при выпадении на праздник» как договорную спецификацию, прежде чем рассчитывать их от даты отсчёта. Сделав всё это, вы превратите следующую смену эры и следующее изменение закона о праздниках в события, которые ваше приложение переживёт всего лишь «обновлением справочника» и «установкой обновления ОС».
Мы занимаемся проектированием и реализацией обработки дат вокруг отчётов и выставления счетов, инвентаризацией и доработкой жёстко прописанных названий эры и праздников в существующих приложениях, а также обзорами проектных решений, включающими прояснение спецификации даты закрытия периода. Мы можем помочь даже на этапе, когда вы ещё не знаете, в скольких местах кода требуется доработка в связи со сменой эры.
Похожие статьи
- Дата, время и часовые пояса в бизнес-приложениях — от ловушек DateTime до принципа хранения в UTC и проектирования тестов
- CSV — это не просто «текст» — практика работы с CSV в бизнес-приложениях на C# (кодировка символов, совместимость с Excel, защита от инъекций)
- Локализация приложений WinForms/WPF — resx, сателлитные сборки и переключение культуры на практике
- Продление жизни или миграция бизнес-приложения на VB6 / Access — таблица решений между сохранением, обёрткой и заменой
- Как реализовать вывод отчётов Excel - COM / Open XML / шаблоны
Похожие направления консультаций
ООО «Komura Soft» (合同会社小村ソフト) занимается проектированием и реализацией обработки дат для бизнес-приложений в соответствии с японскими деловыми обычаями (отчёты в японской эре, расчёт рабочих дней, обработка дат закрытия периода), а также инвентаризацией и доработкой поддержки смены эры и праздников в существующих приложениях.
- Разработка Windows-приложений
- Доработка и поддержка существующего Windows-ПО
- Техническая консультация и обзор проектных решений
- Контакты
Ссылки
-
Microsoft Learn, Work with eras - .NET. О том, что JapaneseCalendar и JapaneseLunisolarCalendar — единственные классы календаря, распознающие несколько эр, о форматировании эры через спецификатор формата «g» и о том, что информация об эре предоставляется платформой, а не зависит от жёсткого кодирования в приложении. ↩ ↩2 ↩3
-
Cabinet Office Японии, «О национальных праздниках». О перечне праздников согласно Закону о национальных праздниках, о том, что дни весеннего и осеннего равноденствия фиксируются и объявляются в предыдущем году, о положениях о переносе выходного дня и национальном празднике, а также о доступности CSV с датами и названиями праздников с 30-го года Сёва по следующий год. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, DateTime.AddMonths(Int32) Method. О том, что когда результирующий день превышает число дней в результирующем месяце, результат корректируется до последнего дня этого месяца (например, 31 января плюс месяц становится 28/29 февраля). ↩ ↩2
-
Microsoft Learn, JapaneseCalendar Class. О диапазоне дат, поддерживаемом JapaneseCalendar — начиная с 8 сентября 1-го года Мэйдзи (1868 год н. э.), — и об обработке эр. ↩
-
Microsoft Learn, Work with calendars - .NET. О поведении по умолчанию, форматирующем первый год эры как «Ган-нэн», о переключателе Switch.System.Globalization.FormatJapaneseFirstYearAsANumber, восстанавливающем числовое обозначение, и о том, что проверка диапазона годов эры по умолчанию ослаблена, но может быть сделана строгой через Switch.System.Globalization.EnforceJapaneseEraYearRanges. ↩ ↩2
-
Microsoft Learn, CultureInfo.CurrentCulture Property. О том, что CurrentCulture по умолчанию отражает изменения региональных настроек пользователя Windows (переопределение пользователя), и что форматирование по умолчанию зависит от этой культуры. ↩
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification
Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...
До каких пор будут работать приложения VB6 ── состояние поддержки среды выполнения и практичный путь миграции на .NET
До каких пор будут работать приложения VB6? В статье разбирается асимметрия между политикой поддержки среды выполнения VB6 (поддерживаетс...
Когда не стоит переводить Windows-приложение в веб: таблица решений и «разделение» как реальный выход
Запросов на перевод корпоративных Windows-приложений в веб становится всё больше, но для приложений с интеграцией оборудования, локальной...
Интернационализация приложений WinForms/WPF — resx, сателлитные сборки и переключение культуры на практике
Разбираем интернационализацию десктопных приложений Windows на практике: различие между CurrentCulture и CurrentUICulture, устройство рес...
Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)
HttpClient в C#, создаваемый через using при каждом запросе, приводит к исчерпанию сокетов, а статический HttpClient перестаёт учитывать ...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Реализация специфичных для Японии бизнес-требований, таких как отображение эры в отчётах, расчёт рабочих дней и обработка даты закрытия периода, относится к основной области консультаций по разработке бизнес-приложений для Windows.
Сопровождение и модернизация ПО Windows
Инвентаризация жёстко прописанных названий эры и праздников в существующем приложении и их доработка так, чтобы они выдержали следующую смену эры или изменение законодательства, относится к доработке и сопровождению существующего Windows-ПО.
Технические консультации и ревью дизайна
Определение того, насколько формализовать и вынести в данные такие понятия, как «конец месяца», «рабочий день» и «финансовый год», относится к технической консультации, включающей обзор проектных решений.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Плохо ли хранить даты в базе данных прямо в формате японской эры?
- Мы этого не рекомендуем по четырём причинам. Во-первых, строки эры вроде «Хэйсэй 9» и «Рэйва 9» теряют корректный порядок при сравнении строк, как только пересекается граница эры, — сортировка и запросы по диапазону становятся невозможны. Во-вторых, в момент следующей смены эры старые и новые обозначения оказываются перемешаны в уже сохранённых данных. В-третьих, легко проникают «грязные» данные с датами, которые никогда не существовали, — например, «Сёва 64, 10 января» (в этот день уже наступила Хэйсэй 1, поскольку эра сменилась 8 января), — превращаясь в бомбу замедленного действия для парсинга. В-четвёртых, JapaneseCalendar в .NET поддерживает только даты начиная с 8 сентября 1-го года Мэйдзи (1868 год); более ранние даты — например, старая дата рождения из семейного реестра — не могут корректно обрабатываться, если оставлены в формате эры. Правило таково: внутреннюю обработку и хранение унифицировать на григорианском календаре, а преобразование в японскую эру выполнять только в момент отображения или печати.
- Как приложению подготовиться к следующей смене эры?
- Определение названий эры не зашито жёстко в сам .NET — механизм обращается к информации об эре, предоставляемой операционной системой, поэтому достаточно продолжать устанавливать обновления ОС и .NET, чтобы возможность «отображать в новой эре» подключалась автоматически. При переходе к эре Рэйва основная часть реакции действительно свелась к установке обновлений платформы. Настоящая проблема — в коде и данных на стороне приложения: ветвления, жёстко прописывающие «Хэйсэй» или «Рэйва» строками, самописные функции преобразования с собственной таблицей соответствия эры и григорианского года, названия эры, записанные прямо в шаблоны отчётов, или поля «Рэйва __» на предварительно напечатанных бланках — ничто из этого не подхватывается автоматически. Разумны четыре практических меры: (1) унифицировать внутреннюю обработку и хранение на григорианском календаре, чтобы японская эра оставалась только средством отображения; (2) отказаться от собственной логики преобразования эры в пользу JapaneseCalendar; (3) механически пройтись по всему исходному коду через grep в поисках «Сёва», «Хэйсэй» и «Рэйва», чтобы составить инвентаризацию; и (4) подготовить тесты, предполагающие гипотетическую будущую смену эры.
- Разве праздники нельзя определить формулой? Почему их обязательно нужно хранить как данные?
- Полную формулу построить принципиально невозможно, по трём причинам. Во-первых, День весеннего и День осеннего равноденствия устанавливаются решением кабинета министров на основе астрономических расчётов только в предыдущем году — они не закреплены в законе как фиксированные даты. Во-вторых, праздники добавляются, переносятся или переименовываются в результате изменений законодательства: в 2016 году был учреждён День гор, в 2019 году день рождения императора перенесли с 23 декабря на 23 февраля в связи со сменой эры (**в 2019 году дня рождения императора не было вовсе**), а День здоровья и спорта переименовали в День спорта. В-третьих, в 2020 и 2021 годах специальным законом, связанным с Олимпийскими играми, День моря, День спорта и День гор были перенесены **только для этих лет**, что показывает: даже правило вроде «всегда третий понедельник» может быть разово отменено. Поэтому правильный дизайн — «справочник праздников плюс процесс обновления», а логику расчёта следует ограничить вспомогательными правилами, такими как перенос выходного дня.
- После использования AddMonths(1) срок платежа сместился от конца месяца. Почему?
- Потому что DateTime.AddMonths по спецификации округляет результат до последнего дня получившегося месяца, если наивный расчёт дал бы день, которого в этом месяце не существует. Применение AddMonths(1) к 31 января даёт 28 февраля (или 29-е в високосный год) — само по себе это поведение корректно. Но если затем перенести этот округлённый результат в ещё один AddMonths, применение AddMonths(1) к 28 февраля даст 28 марта, и далее смещение продолжается бесконечно: то, что задумывалось как «конец месяца», незаметно и навсегда превращается в «28-е число». Решение состоит из двух частей: для повторяющихся расчётов всегда вычислять от фиксированной точки отсчёта (даты договора или определения «конец каждого месяца»), а не прибавлять к предыдущему результату, а «конец месяца» выводить через DateTime.DaysInMonth, а не через литерал даты.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки