Японская эра, праздники и даты закрытия периода в бизнес-приложениях — устойчивый к смене эры дизайн, 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, способные работать с несколькими эрами, — это JapaneseCalendarJapaneseLunisolarCalendar), а спецификатор формата 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. Заключение

Японская эра, праздники и даты закрытия периода объединяет одно: все они имеют дело со «спецификациями, которые меняются позже». Поэтому принцип проектирования для всех трёх одинаков: не записывать в код то, что может измениться, а изолировать это в данных и в слое преобразования. Унифицируйте внутреннюю обработку и хранение на григорианском календаре, оставив японскую эру только преобразованием для отображения; ведите праздники в справочнике с процессом обновления; документируйте «конец месяца», «рабочий день» и «корректировку срока при выпадении на праздник» как договорную спецификацию, прежде чем рассчитывать их от даты отсчёта. Сделав всё это, вы превратите следующую смену эры и следующее изменение закона о праздниках в события, которые ваше приложение переживёт всего лишь «обновлением справочника» и «установкой обновления ОС».

Мы занимаемся проектированием и реализацией обработки дат вокруг отчётов и выставления счетов, инвентаризацией и доработкой жёстко прописанных названий эры и праздников в существующих приложениях, а также обзорами проектных решений, включающими прояснение спецификации даты закрытия периода. Мы можем помочь даже на этапе, когда вы ещё не знаете, в скольких местах кода требуется доработка в связи со сменой эры.

Похожие статьи

Похожие направления консультаций

ООО «Komura Soft» (合同会社小村ソフト) занимается проектированием и реализацией обработки дат для бизнес-приложений в соответствии с японскими деловыми обычаями (отчёты в японской эре, расчёт рабочих дней, обработка дат закрытия периода), а также инвентаризацией и доработкой поддержки смены эры и праздников в существующих приложениях.

Ссылки

  1. Microsoft Learn, Work with eras - .NET. О том, что JapaneseCalendar и JapaneseLunisolarCalendar — единственные классы календаря, распознающие несколько эр, о форматировании эры через спецификатор формата «g» и о том, что информация об эре предоставляется платформой, а не зависит от жёсткого кодирования в приложении.  2 3

  2. Cabinet Office Японии, «О национальных праздниках». О перечне праздников согласно Закону о национальных праздниках, о том, что дни весеннего и осеннего равноденствия фиксируются и объявляются в предыдущем году, о положениях о переносе выходного дня и национальном празднике, а также о доступности CSV с датами и названиями праздников с 30-го года Сёва по следующий год.  2 3 4 5

  3. Microsoft Learn, DateTime.AddMonths(Int32) Method. О том, что когда результирующий день превышает число дней в результирующем месяце, результат корректируется до последнего дня этого месяца (например, 31 января плюс месяц становится 28/29 февраля).  2

  4. Microsoft Learn, JapaneseCalendar Class. О диапазоне дат, поддерживаемом JapaneseCalendar — начиная с 8 сентября 1-го года Мэйдзи (1868 год н. э.), — и об обработке эр. 

  5. Microsoft Learn, Work with calendars - .NET. О поведении по умолчанию, форматирующем первый год эры как «Ган-нэн», о переключателе Switch.System.Globalization.FormatJapaneseFirstYearAsANumber, восстанавливающем числовое обозначение, и о том, что проверка диапазона годов эры по умолчанию ослаблена, но может быть сделана строгой через Switch.System.Globalization.EnforceJapaneseEraYearRanges.  2

  6. Microsoft Learn, CultureInfo.CurrentCulture Property. О том, что CurrentCulture по умолчанию отражает изменения региональных настроек пользователя Windows (переопределение пользователя), и что форматирование по умолчанию зависит от этой культуры. 

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Значки в области уведомлений и всплывающие (toast) уведомления в Windows-приложениях — подводные камни NotifyIcon и выбор правильного AppNotification

Практическое руководство о том, как удерживать бизнес-приложение Windows в области уведомлений (system tray) и оповещать пользователя с п...

Не оборачивайте HttpClient в using ── практика HTTP-взаимодействия в бизнес-приложениях на C# (паттерны создания, тайм-ауты, повторные попытки)

HttpClient в C#, создаваемый через using при каждом запросе, приводит к исчерпанию сокетов, а статический HttpClient перестаёт учитывать ...

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Разработка приложений для 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, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.

Публичные ссылки

Вернуться в блог