From 9d3273191b584dc4a25e388b93f427f337129154 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 7 Jul 2026 15:24:59 +1000 Subject: [PATCH] =?UTF-8?q?fix(core):=20=D0=94=D0=BE=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B0=20=D0=BC=D0=B5=D1=82=D0=BE=D0=B4=D0=BE?= =?UTF-8?q?=D0=B2=20=D1=80=D0=B0=D1=81=D1=87=D0=B5=D1=82=D0=B0=20NextRun?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D0=B0=D0=BB=D0=B3=D0=BE=D1=80=D0=B8=D1=82?= =?UTF-8?q?=D0=BC=D1=83=20=D0=95=D0=A1=D0=9F=D0=9F,=20=D0=B4=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=BF=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B5=D1=80=D0=BA=D0=B0,=20=D1=87=D1=82=D0=BE=20=D1=80=D0=B0?= =?UTF-8?q?=D1=81=D1=87=D0=B8=D1=82=D0=B0=D0=BD=D0=BD=D0=B0=D1=8F=20=D0=B4?= =?UTF-8?q?=D0=B0=D1=82=D0=B0=20=D0=BD=D0=BE=D0=B2=D0=B5=D0=B5=20=D1=87?= =?UTF-8?q?=D0=B5=D0=BC=20refDate.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EsppScheduleTransformService.cs | 554 +++++++++++++----- 1 file changed, 417 insertions(+), 137 deletions(-) diff --git a/PARR.Core/Services/NextRunServices/Subservices/EsppScheduleTransformService.cs b/PARR.Core/Services/NextRunServices/Subservices/EsppScheduleTransformService.cs index 732ce214..aaa52040 100644 --- a/PARR.Core/Services/NextRunServices/Subservices/EsppScheduleTransformService.cs +++ b/PARR.Core/Services/NextRunServices/Subservices/EsppScheduleTransformService.cs @@ -70,23 +70,12 @@ namespace PARR.Core.Services.NextRunServices.Subservices var nextRun = referenceDate; - //var finalOffset = new TimeSpan(3, 0, 0) + (offset ?? TimeSpan.Zero); var finalOffset = offset ?? TimeSpan.Zero; var offsetReferenceDate = referenceDate.ToOffset(finalOffset); logger.LogDebug("Дата после добавления offset - {offsetReferenceDate}", offsetReferenceDate); - ////--------- - //// Для рассчета по МСК времени, потому что в ЮТС может быть еще ВСК, а по МСК это уже ПНД - //var offsetReferenceDate = referenceDate.ToOffset(new TimeSpan(3, 0, 0)); - ////--------- - - //// может быть offset ,это работает когда смещение для УЗ ЕСПП и применение ЗО РГ - //if (offset.HasValue) - // offsetReferenceDate = offsetReferenceDate.Off(offsetReferenceDate.Offset + offset); - // //offsetReferenceDate = offsetReferenceDate.Add(offset.Value); - switch (esppSchedule.TypeSchedule.Id) { @@ -108,6 +97,9 @@ namespace PARR.Core.Services.NextRunServices.Subservices case (int)EsppSchTypeScheduleEnum.Annually2: nextRun = GetNextDateAnnually2(esppSchedule.Values, offsetReferenceDate); break; + default: + logger.LogError("Неподдерживаемый тип расписания ЕСПП: {TypeId}", esppSchedule.TypeSchedule.Id); + throw new NotSupportedException($"Тип расписания {esppSchedule.TypeSchedule.Id} не поддерживается."); } logger.LogDebug("Рассчитанная дата, с учетом offset: {nextRun}", nextRun); @@ -139,6 +131,14 @@ namespace PARR.Core.Services.NextRunServices.Subservices return esppSchedule; } + + /// + /// Регулярно. Каждые 1 час, 2 часа, Каждые пол года... + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateRegularly(List values, DateTimeOffset referenceDate) { var regDict = new Dictionary @@ -159,27 +159,63 @@ namespace PARR.Core.Services.NextRunServices.Subservices {"Каждые полгода", 4380}, {"Каждые 1,5 года", 13140}, {"Каждые 3 года", 26280}, - }; - - if (!regDict.TryGetValue(values[0].Value.Value, out int regNum)) + var key = values[0].Value.Value; + if (!regDict.TryGetValue(key, out int regMinutes)) { - logger.LogError("Неизвестное значение: {Value}", values[0].Value.Value); - throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); + logger.LogError("Неизвестное значение: {Value}", key); + throw new ArgumentException($"Неизвестное значение: {key}"); } + #region Старая логика + + //var calcDay = referenceDate; + + ////TODO: вот это повторяется от метода к методу + ////Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию + //if (calcDay < DateTimeOffset.UtcNow /* || calcDay <= referenceDate*/) + // calcDay = GetNextDateRegularly(values, calcDay.AddHours(regMinutes)); + + //return calcDay; + + #endregion + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + var calcDay = referenceDate; - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и уходим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateRegularly(values, calcDay.AddHours(regNum)); + // Если referenceDate далеко в прошлом, не шагаем по минутам в цикле. + // Сразу вычисляем, сколько целых интервалов нужно прибавить, чтобы догнать текущее время. + if (calcDay < nowInTargetZone) + { + double totalMinutesPast = (nowInTargetZone - calcDay).TotalMinutes; - return calcDay; + // Считаем, сколько полных интервалов помещается в этот отрезок времени + long intervalsToSkip = (long)Math.Ceiling(totalMinutesPast / regMinutes); + + calcDay = calcDay.AddMinutes(intervalsToSkip * regMinutes); + } + + // Проверяем условия и делаем микро-шаги, если необходимо + while (true) + { + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + calcDay = calcDay.AddMinutes(regMinutes); + } } + /// + /// Еженедельно, каждый понедельник, вторник, среду... + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateWeekly(List values, DateTimeOffset referenceDate) { if (!dwDict.TryGetValue(values[0].Value.Value, out int dwNum)) @@ -188,71 +224,155 @@ namespace PARR.Core.Services.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); } + #region Старая логика + //var calcDay = referenceDate; + //if (calcDay < DateTimeOffset.UtcNow) + //{ + // var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + // calcDay = new DateTimeOffset( + // nowInTargetTz.Year, + // nowInTargetTz.Month, + // nowInTargetTz.Day, + // referenceDate.Hour, + // referenceDate.Minute, + // referenceDate.Second, + // referenceDate.Offset + // ); + //} + + + //if (((int)calcDay.DayOfWeek) != dwNum) + // calcDay = OffsetToDayOfWeek(dwNum, calcDay); + + ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию + //if (calcDay < DateTimeOffset.UtcNow || calcDay <= referenceDate) + // calcDay = GetNextDateWeekly(values, calcDay.AddDays(7));//а вот тут что-то новенькое + + //return calcDay; + #endregion + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + var calcDay = referenceDate; - if (calcDay < DateTimeOffset.UtcNow) + + // Сдвигаем дату вперед до нужного дня недели + if ((int)calcDay.DayOfWeek != dwNum) { - var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); - calcDay = new DateTimeOffset( - nowInTargetTz.Year, - //DateTimeOffset.UtcNow.Year, - nowInTargetTz.Month, - //DateTimeOffset.UtcNow.Month, - nowInTargetTz.Day, - //DateTimeOffset.UtcNow.Day, - referenceDate.Hour, - referenceDate.Minute, - referenceDate.Second, - referenceDate.Offset - ); + calcDay = OffsetToDayOfWeek(dwNum, calcDay); } + // Если получившаяся дата все еще глубоко в прошлом, + // не шагаем неделями в цикле, а мгновенно прыгаем ближе к текущему времени математикой. + if (calcDay < nowInTargetZone) + { + double totalDaysPast = (nowInTargetZone - calcDay).TotalDays; - if (((int)calcDay.DayOfWeek) != dwNum) - calcDay = OffsetToDayOfWeek(dwNum, calcDay); + // Вычисляем, сколько полных недель (по 7 дней) нужно прибавить + long weeksToSkip = (long)Math.Ceiling(totalDaysPast / 7); - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateWeekly(values, calcDay.AddDays(7));//а вот тут что-то новенькое + calcDay = calcDay.AddDays(weeksToSkip * 7); + } - return calcDay; + while (true) + { + // Условие выхода: дата в будущем относительно точки старта И текущего времени + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + + // Если условия не выполнены, делаем шаг ровно в одну неделю (7 дней) + calcDay = calcDay.AddDays(7); + } } + /// + /// Ежемесячно, 1, 2, 3... числа + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateMonthly(List values, DateTimeOffset referenceDate) { - if (!int.TryParse(values[0].Value.Value, out int dayNum)) + if (!int.TryParse(values[0].Value.Value, out int targetDay)) { logger.LogError("Неизвестное значение дня месяца: {Value}", values[0].Value.Value); throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); } - var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - if (referenceDate < DateTimeOffset.UtcNow) + #region старая логика, не учтено попадание например 31 числа на февраль. Нет сравнения что расчитанная дата новее или равна refDate + //var calcDay = new DateTimeOffset(referenceDate.Year, referenceDate.Month, targetDay, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + //if (referenceDate < DateTimeOffset.UtcNow) + //{ + // var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + // calcDay = new DateTimeOffset( + // nowInTargetTz.Year, + // nowInTargetTz.Month, + // targetDay, + // referenceDate.Hour, + // referenceDate.Minute, + // referenceDate.Second, + // referenceDate.Offset + // ); + //} + + ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию + //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/) + // calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1)); + + //return calcDay; + #endregion + + // Начинаем проверку с текущего месяца и года переданной referenceDate + var baseDate = referenceDate; + int monthsToAdd = 0; + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + + while (true) { - var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); - calcDay = new DateTimeOffset( - nowInTargetTz.Year, - nowInTargetTz.Month, - dayNum, - referenceDate.Hour, - referenceDate.Minute, - referenceDate.Second, - referenceDate.Offset - ); - //calcDay = new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, dayNum, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + // Каждую итерацию мы отталкиваемся от исходной точки и прибавляем +1, +2, +3 месяца + var nextMonthDate = baseDate.AddMonths(monthsToAdd); + int year = nextMonthDate.Year; + int month = nextMonthDate.Month; + + // Если в этом конкретном месяце НЕТ нужного нам дня (например, 31 числа в апреле или 30 в феврале) + if (targetDay > DateTime.DaysInMonth(year, month)) + { + // Пропускаем этот месяц и идем на следующий круг цикла + monthsToAdd++; + continue; + } + + // Если день существует, безопасно собираем дату + var calcDay = new DateTimeOffset( + year, month, targetDay, + referenceDate.Hour, referenceDate.Minute, referenceDate.Second, + referenceDate.Offset + ); + + // Условие выхода: дата строго в будущем относительно referenceDate и текущего UTC-времени + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + + // Если дата правильная, но она в прошлом — переходим к следующему месяцу + monthsToAdd++; } - - - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateMonthly(values, calcDay.AddMonths(1)); - - return calcDay; } + /// + /// Ежемесячно-2, Первый понедельник, Третий вторник + /// + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateMonthly2(List values, DateTimeOffset referenceDate) { if (!orderDict.TryGetValue(values[0].Value.Value, out int orderNum)) @@ -267,56 +387,113 @@ namespace PARR.Core.Services.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); } + #region Старая логика //var startDay = referenceDate < DateTimeOffset.UtcNow - // ? new DateTimeOffset(DateTimeOffset.UtcNow.Year, DateTimeOffset.UtcNow.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset) - // : new DateTimeOffset(referenceDate.Year, referenceDate.Month, 1, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + // ? new DateTimeOffset( + // DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Year, + // DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Month, + // 1, + // referenceDate.Hour, + // referenceDate.Minute, + // referenceDate.Second, + // referenceDate.Offset + // ) + // : new DateTimeOffset( + // referenceDate.Year, + // referenceDate.Month, + // 1, + // referenceDate.Hour, + // referenceDate.Minute, + // referenceDate.Second, + // referenceDate.Offset + // ); - var startDay = referenceDate < DateTimeOffset.UtcNow - ? new DateTimeOffset( - DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Year, - DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Month, - 1, - referenceDate.Hour, - referenceDate.Minute, - referenceDate.Second, - referenceDate.Offset - ) - : new DateTimeOffset( - referenceDate.Year, - referenceDate.Month, - 1, - referenceDate.Hour, - referenceDate.Minute, - referenceDate.Second, - referenceDate.Offset - ); + //var daysList = FindAllDaysInMonth(dwNum, startDay); - var daysList = FindAllDaysInMonth(dwNum, startDay); + //DateTimeOffset calcDay; + //if (orderNum == 5) // Последний + //{ + // calcDay = daysList.Last(); + //} + //else + //{ + // if (orderNum > daysList.Count) + // { + // logger.LogError("Запрошенный порядковый день недели ({OrderNum}) не существует в месяце {Month}/{Year}. Найдено только {Count} дней.", orderNum, startDay.Month, startDay.Year, daysList.Count); + // throw new InvalidOperationException($"Запрошенный порядковый день недели ({orderNum}) не существует в месяце {startDay.Month}/{startDay.Year}. Найдено только {daysList.Count} дней."); + // } + // else + // calcDay = daysList[orderNum - 1]; + //} - DateTimeOffset calcDay; - if (orderNum == 5) // Последний + //// Если дата <= текущей, переходим на следующий месяц + //if (calcDay <= DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/) + // return GetNextDateMonthly2(values, startDay.AddMonths(1)); + + //return calcDay; + #endregion + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + + // Начинаем расчет с текущего месяца referenceDate + int monthsToAdd = 0; + + while (true) { - calcDay = daysList.Last(); - } - else - { - if (orderNum > daysList.Count) + var currentMonthDate = referenceDate.AddMonths(monthsToAdd); + + // Безопасно формируем первое число для проверяемого месяца + var startDayOfMonth = new DateTimeOffset( + currentMonthDate.Year, + currentMonthDate.Month, + 1, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); + + // Ищем все нужные дни недели в этом месяце + var daysList = FindAllDaysInMonth(dwNum, startDayOfMonth); + + DateTimeOffset calcDay; + + if (orderNum == 5) // Последний день недели в месяце (например, последняя пятница) { - logger.LogError("Запрошенный порядковый день недели ({OrderNum}) не существует в месяце {Month}/{Year}. Найдено только {Count} дней.", orderNum, startDay.Month, startDay.Year, daysList.Count); - throw new InvalidOperationException($"Запрошенный порядковый день недели ({orderNum}) не существует в месяце {startDay.Month}/{startDay.Year}. Найдено только {daysList.Count} дней."); + calcDay = daysList.Last(); } else + { + // Умная защита: если в текущем месяце нет 5-го вторника, + // мы не выкидываем ошибку приложения, а мягко идем искать в следующем месяце + if (orderNum > daysList.Count) + { + monthsToAdd++; + continue; + } + calcDay = daysList[orderNum - 1]; + } + + // 3. Условие выхода (точно такое же, как во всех предыдущих методах) + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + + // Если дата правильная, но она в прошлом — переходим к следующему календарному месяцу + monthsToAdd++; } - - // Если дата <= текущей, переходим на следующий месяц - if (calcDay <= DateTimeOffset.UtcNow) - return GetNextDateMonthly2(values, startDay.AddMonths(1)); - - return calcDay; } + /// + /// Ежегодно, Январь 17, Февраль 20 + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateAnnually(List values, DateTimeOffset referenceDate) { @@ -332,26 +509,73 @@ namespace PARR.Core.Services.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[0].Value.Value}"); } - //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; - //сравниваем года в одном часовом поясе - var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); - var year = (referenceDate.Year < nowInTargetTz.Year) - ? nowInTargetTz.Year - : referenceDate.Year; + #region Старая логика + ////сравниваем года в одном часовом поясе + //var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + //var year = (referenceDate.Year < nowInTargetTz.Year) + // ? nowInTargetTz.Year + // : referenceDate.Year; - var calcDay = new DateTimeOffset( - year, monthNum, dayNum, - referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + //var calcDay = new DateTimeOffset( + // year, monthNum, dayNum, + // referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateAnnually(values, calcDay.AddYears(1)); + ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию + //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/) + // calcDay = GetNextDateAnnually(values, calcDay.AddYears(1)); - return calcDay; + //return calcDay; + #endregion + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + + // Начинаем расчет с года переданной referenceDate + int yearsToAdd = 0; + + while (true) + { + // Вычисляем текущий проверяемый год относительно исходной referenceDate + var currentYearDate = referenceDate.AddYears(yearsToAdd); + int year = currentYearDate.Year; + + // ЗАЩИТА: Если в этом году нет такого дня (например, 29 февраля в невисокосном году) + if (dayNum > DateTime.DaysInMonth(year, monthNum)) + { + // Просто пропускаем этот год и переходим к следующему + yearsToAdd++; + continue; + } + + // Если день существует, безопасно собираем дату + var calcDay = new DateTimeOffset( + year, + monthNum, + dayNum, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); + + // Проверяем условия выхода (абсолютно одинаково для всех методов) + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + + // Если дата в прошлом — переходим на следующий год + yearsToAdd++; + } } + /// + /// Ежегодно-2, Первый понедельник января + /// + /// + /// + /// + /// private DateTimeOffset GetNextDateAnnually2(List values, DateTimeOffset referenceDate) { var monthADict = new Dictionary @@ -386,34 +610,90 @@ namespace PARR.Core.Services.NextRunServices.Subservices throw new ArgumentException($"Неизвестное значение: {values[1].Value.Value}"); } - //var year = (referenceDate.Year < DateTimeOffset.UtcNow.Year) ? DateTimeOffset.UtcNow.Year : referenceDate.Year; - // сравниваем года в одном часовом поясе - var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); - var year = (referenceDate.Year < nowInTargetTz.Year) - ? nowInTargetTz.Year - : referenceDate.Year; - //if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month) - if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month) - year += 1; + #region Старая логика - var calcDay = new DateTimeOffset( - year, monthNum, 1, - referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); + //// сравниваем года в одном часовом поясе + //var nowInTargetTz = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + //var year = (referenceDate.Year < nowInTargetTz.Year) + // ? nowInTargetTz.Year + // : referenceDate.Year; - List daysList = FindAllDaysInMonth(dwNum, calcDay); + ////if (year == DateTimeOffset.UtcNow.Year && referenceDate.Month < DateTimeOffset.UtcNow.Month) + //if (year == nowInTargetTz.Year && referenceDate.Month < nowInTargetTz.Month) + // year += 1; - if (orderNum == 5)//5-последний - calcDay = daysList.Last(); - else - calcDay = daysList[orderNum - 1]; + //var calcDay = new DateTimeOffset( + // year, monthNum, 1, + // referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - //TODO: вот это повторяется от метода к методу - //Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию - if (calcDay < DateTimeOffset.UtcNow) - calcDay = GetNextDateAnnually2(values, calcDay.AddYears(1)); + //List daysList = FindAllDaysInMonth(dwNum, calcDay); - return calcDay; + //if (orderNum == 5)//5-последний + // calcDay = daysList.Last(); + //else + // calcDay = daysList[orderNum - 1]; + + ////Если итоговая дата указывает на прошлое, то повторяем расчёт и ухоим в рекурсию + //if (calcDay < DateTimeOffset.UtcNow /*|| calcDay <= referenceDate*/) + // calcDay = GetNextDateAnnually2(values, calcDay.AddYears(1)); + + //return calcDay; + + #endregion + + var nowInTargetZone = DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset); + + // Начинаем расчет с года переданной referenceDate + int yearsToAdd = 0; + + while (true) + { + // Сдвигаем проверяемый год относительно исходной даты + var currentYearDate = referenceDate.AddYears(yearsToAdd); + int year = currentYearDate.Year; + + // Формируем первое число целевого месяца для проверяемого года + var startDayOfMonth = new DateTimeOffset( + year, + monthNum, + 1, + referenceDate.Hour, + referenceDate.Minute, + referenceDate.Second, + referenceDate.Offset + ); + + // Ищем все нужные дни недели в этом месяце + var daysList = FindAllDaysInMonth(dwNum, startDayOfMonth); + + DateTimeOffset calcDay; + + if (orderNum == 5) // 5 означает "Последний день недели в месяце" + { + calcDay = daysList.Last(); + } + else + { + // Умная защита: если в этом году в этом месяце НЕТ 5-го выбранного дня недели + if (orderNum > daysList.Count) + { + // Мы не падаем с ошибкой, а просто переходим к проверке следующего года + yearsToAdd++; + continue; + } + + calcDay = daysList[orderNum - 1]; + } + + if (calcDay >= referenceDate && calcDay >= nowInTargetZone) + { + return calcDay; + } + + // Если вычисленная дата в прошлом — переходим на следующий год + yearsToAdd++; + } }