Re[2]: Excel 29.02.1900 и .NET/C#
От: Михаил Романов Удмуртия https://mihailromanov.wordpress.com/
Дата: 03.07.22 11:21
Оценка: 135 (2)
Здравствуйте, Михаил Романов, Вы писали:

Прошу прощения, что немного затянул — оказалось, что не так всё и просто, да и выходные (сами понимаете — хочется немного выдохнуть между рабочими неделями).
МР>Давайте начнем с конца — как подсунуть свой календарь (например, попробуем везде подсовывать юлианский календарь).

Давайте теперь расскажу, что у меня набралось по самим календарям.
По идее, если вам нужен новый нестандартный календарь, то вы просто наследуетесь от Calendar или одного из его потомков (если он вам подходит больше), например, от GregorianCalendar, перекрываете те методы, которые вам нужны и (ура!) у нас есть новый нестандартный календарь под наши задачи.

Я попытался изобразить нечто похожее на базе GregorianCalendar — как самом близком к тому, что мы хотим.
Получилась вот такая (увы!) не полная и очень костыльная реализация.
Идея там примитивная:
— если нам приходит дата 29.02.1900 мы переводим её в валидную для григорианского календаря 1.03.1900,
— а затем, когда выводим обратно, учитываем эту поправку, т.е. проверяем: если дата перевалила за границу 1.03.1900 — то вычитаем 1 день, а иначе оставляем как есть (ну и конкретно с днем еще есть явная проверка, что если это 1.03.1900, то день в месяце — 29)
public class BadCalendar : GregorianCalendar 
{
    static DateTime TresholdDateTime = (new DateTime(1900, 3, 1)).AddTicks(-1);

    public override DateTime ToDateTime(int year, int month, int day, int hour, 
        int minute, int second, int millisecond, int era)
    {
        if (year == 1900 && month == 2 && day == 29)
            return base.ToDateTime(year, month, 28, hour, minute, 
                second, millisecond, era).AddDays(1);
        return base.ToDateTime(year, month, day, hour, minute, 
            second, millisecond, era);
    }

    public override int GetMonth(DateTime time)
    {
        if (time > TresholdDateTime)
           return base.GetMonth(time.AddDays(-1));
        return base.GetMonth(time);
    } 

    public override int GetYear(DateTime time)
    {
        if (time > TresholdDateTime)
            return base.GetYear(time.AddDays(-1));
        return base.GetYear(time);
    }

    public override int GetDayOfMonth(DateTime time)
    {
        if (time.Year == 1900 && time.Month == 3 && time.Day == 1)
            return 29;

        if (time > TresholdDateTime)
        {
            return base.GetDayOfMonth(time.AddDays(-1));
        }
        return base.GetDayOfMonth(time);
    }
}


Благодаря этому костылю, я могу делать вот такое:
var calendar = new BadCalendar();
var formatInfo = new DateTimeFormatInfo();
var calendarField = formatInfo.GetType().GetField(
    "calendar",
    System.Reflection.BindingFlags.NonPublic
    | System.Reflection.BindingFlags.Instance);
calendarField.SetValue(formatInfo, calendar);

var date = new DateTime(1900, 2, 29, calendar);
Console.WriteLine(date.ToString(formatInfo));


Теперь о не очень радостных моментах этого решения
— я не стал переопределять никакую арифметику на датах, прикинув "на пальцах", что той поправки, которую я описал должно хватить. Но я в этом не до конца уверен. А вот реализовывать все операции, мне очень не хочется (можете глянуть для примера как это сделано можно глянуть на метод AddMonths)
— есть проблемы с парсингом даты из строки, т.е. если к последнему коду добавить строчку
var date1 = DateTime.Parse("02/29/1900", formatInfo);

она вернет ошибку

System.FormatException: String '02/29/1900' was not recognized as a valid DateTime.


В общем, там проблема в том, что метод парсинга после того, как разобрал строку с датой передает все распарсенные элементы календарю для проверки и создания DateTime, и вызывает для этого метод TryToDateTime, который, вызывает ToDateTime, который мы реализовали...
Но (!) в дочернем GregorianCalendar этот метод переопределен и вызывает создание из DateTime... которая ничего про отличные от грегорианского календари не знает Печаль.

Я попытался обойти это разными способами:
— думал переопределять юлианский календарь, но там и так дата 29.02.1900 является допустимой (но там вся арифметика иная, а значит нужно сидеть и аккуратно переделывать всё).
— хотел уже плюнуть и, скопипастив код арифметики (и немного подправив её) сделать просто наследник от Calendar, но там всё упирается в свойство ID которое объявлено вот так:
internal virtual CalendarId ID => CalendarId.UNINITIALIZED_VALUE;

internal enum CalendarId : ushort
{
    UNINITIALIZED_VALUE = 0,
    GREGORIAN = 1,     // Gregorian (localized) calendar
    GREGORIAN_US = 2,     // Gregorian (U.S.) calendar
    JAPAN = 3,     // Japanese Emperor Era calendar
    /* SSS_WARNINGS_OFF */
    TAIWAN = 4,     // Taiwan Era calendar /* SSS_WARNINGS_ON */
    KOREA = 5,     // Korean Tangun Era calendar
    HIJRI = 6,     // Hijri (Arabic Lunar) calendar
    THAI = 7,     // Thai calendar
    HEBREW = 8,     // Hebrew (Lunar) calendar
    GREGORIAN_ME_FRENCH = 9,     // Gregorian Middle East French calendar
    GREGORIAN_ARABIC = 10,     // Gregorian Arabic calendar
    GREGORIAN_XLIT_ENGLISH = 11,     // Gregorian Transliterated English calendar
    GREGORIAN_XLIT_FRENCH = 12,
    // Note that all calendars after this point are MANAGED ONLY for now.
    JULIAN = 13,
    JAPANESELUNISOLAR = 14,
    CHINESELUNISOLAR = 15,
    SAKA = 16,     // reserved to match Office but not implemented in our code
    LUNAR_ETO_CHN = 17,     // reserved to match Office but not implemented in our code
    LUNAR_ETO_KOR = 18,     // reserved to match Office but not implemented in our code
    LUNAR_ETO_ROKUYOU = 19,     // reserved to match Office but not implemented in our code
    KOREANLUNISOLAR = 20,
    TAIWANLUNISOLAR = 21,
    PERSIAN = 22,
    UMALQURA = 23,
    LAST_CALENDAR = 23      // Last calendar ID
}


Как видите, этот идентификатор хоть и виртуальный, но объявлен internal, т.е. без рефлексии не подменить.
А используется он для извлечения системных ресурсов, связанных с датой и временем (например, названиями дней и месяцев) которые и используются при парсинге.

В общем, на дальнейшие изыскания у меня просто не хватило времени и моральных сил.
Но, надеюсь хоть чем-то смог вам помочь.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.