From 2038f7b57d070c9956699d065707a137767d8c30 Mon Sep 17 00:00:00 2001 From: Mikhail Trubnikov Date: Tue, 20 Jan 2026 15:04:36 +1000 Subject: [PATCH] =?UTF-8?q?feat(dal):=20template=20distributor=20-=20?= =?UTF-8?q?=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20NextRun.=20=D0=A0=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- PARR.DAL/NextRunServices/INextRunService.cs | 3 +- .../Models/TemplateNextRunDto.cs | 12 +- .../Models/TemplateNextRunResultDto.cs | 14 +- PARR.DAL/NextRunServices/NextRunService.cs | 14 +- .../Subservices/ITemplateDistributor.cs | 13 +- .../Subservices/TemplateDistributor.cs | 282 ++++++++++---- .../Subservices/TemplateDistributor_old.cs | 345 ------------------ PARR.Test/NextRun/NextRunTest.cs | 50 ++- 8 files changed, 282 insertions(+), 451 deletions(-) delete mode 100644 PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs diff --git a/PARR.DAL/NextRunServices/INextRunService.cs b/PARR.DAL/NextRunServices/INextRunService.cs index d62fd757..3800f9f9 100644 --- a/PARR.DAL/NextRunServices/INextRunService.cs +++ b/PARR.DAL/NextRunServices/INextRunService.cs @@ -22,8 +22,9 @@ /// Получить NextRun по id шаблона /// /// + /// Новый шаблон или шаблон переведенный из unused /// - Task GetNextRunForTemplate(Guid templateId); + Task GetNextRunForTemplate(Guid templateId, bool isNew); } } diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs index 839fe5c6..e2a498f6 100644 --- a/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunDto.cs @@ -3,10 +3,12 @@ /// /// Модель шаблона, для расчета NextRun /// - public class TemplateNextRunDto - { - public Guid Id { get; set; } + public record TemplateNextRunDto(Guid Id, DateTimeOffset? NextRun); - public DateTimeOffset? NextRun { get; set; } - } + //public class TemplateNextRunDto + //{ + // public Guid Id { get; set; } + + // public DateTimeOffset? NextRun { get; set; } + //} } diff --git a/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs index 3477327c..4852c7b4 100644 --- a/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs +++ b/PARR.DAL/NextRunServices/Models/TemplateNextRunResultDto.cs @@ -3,12 +3,14 @@ /// /// Модель, результат распределения шаблона /// - public class TemplateNextRunResultDto - { - public Guid Id { get; set; } + public record TemplateNextRunResultDto(Guid Id, DateTimeOffset NextRun, DateTimeOffset? NextRunOld); - public DateTimeOffset NextRun { get; set; } + //public class TemplateNextRunResultDto + //{ + // public Guid Id { get; set; } - public DateTimeOffset? NextRunOld { get; set; } - } + // public DateTimeOffset NextRun { get; set; } + + // public DateTimeOffset? NextRunOld { get; set; } + //} } diff --git a/PARR.DAL/NextRunServices/NextRunService.cs b/PARR.DAL/NextRunServices/NextRunService.cs index 796e6599..ff5348b7 100644 --- a/PARR.DAL/NextRunServices/NextRunService.cs +++ b/PARR.DAL/NextRunServices/NextRunService.cs @@ -1,6 +1,7 @@  using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using PARR.DAL.NextRunServices.Subservices; using PARR.DAL.Services.Interfaces; using PARR.DAL.Services.Interfaces.Job; using PARR.DAL.TransformServices; @@ -13,25 +14,29 @@ namespace PARR.DAL.NextRunServices private readonly ITemplateService templateService; private readonly IJobGroupService jobGroupService; private readonly IEsppScheduleTransformService esppScheduleTransformService; + private readonly ITemplateDistributor templateDistributor; public NextRunService( ILogger logger, ITemplateService templateService, IJobGroupService jobGroupService, - IEsppScheduleTransformService esppScheduleTransformService + IEsppScheduleTransformService esppScheduleTransformService, + ITemplateDistributor templateDistributor ) { this.logger = logger; this.templateService = templateService; this.jobGroupService = jobGroupService; this.esppScheduleTransformService = esppScheduleTransformService; + this.templateDistributor = templateDistributor; } - public Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) + public async Task> GetNextRunForJobGroupWithAutoDistributionAsync(Guid jobGroupId) { //TODO: throw new NotImplementedException(); + //var ditributedTemplateList = await templateDistributor.DistributeTemplatesAsync(); } @@ -50,7 +55,7 @@ namespace PARR.DAL.NextRunServices } - public async Task GetNextRunForTemplate(Guid templateId) + public async Task GetNextRunForTemplate(Guid templateId, bool isNew) { var template = await templateService.Get() .Include(t => t.Job).ThenInclude(t => t.Group) @@ -67,7 +72,8 @@ namespace PARR.DAL.NextRunServices { // включено автораспределение //TODO: !!!!!!!!!!!!!!!! добавить метод рассчета с учетом распределения - throw new NotImplementedException("добавить метод рассчета с учетом распределения"); + throw new NotImplementedException(); + //return await templateDistributor.GetValidNextRunForTemplateAsync(); } else { diff --git a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs index 0936d6a9..8ef6474a 100644 --- a/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/ITemplateDistributor.cs @@ -17,10 +17,21 @@ namespace PARR.DAL.NextRunServices.Subservices /// Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends); + + /// /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. /// Возвращает обновлённый шаблон с новой датой выполнения. /// - Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends); + /// + /// + /// + /// + /// + /// + /// Новый шаблон + /// + Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew); } } + diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs index bbcf2c4b..be239755 100644 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs +++ b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor.cs @@ -25,60 +25,100 @@ namespace PARR.DAL.NextRunServices.Subservices } - public Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends) + public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) { - //TODO: - throw new NotImplementedException(); + //todo: allTemplates - пока не используется. Будет использоваться когда каждый раз будем строить план + + // Пока работает так: + // Получаем список рабочих дней, если работа попадает в эти дни, то считаем дату валидной + // Если нет, то перемещаем на последний день периода распределения или первый в зависимости от параметра isNew + + var _startDate = GetStartPeriodDate(startDate, referenceDate); + var _endDate = GetEndPeriodDate(_startDate, duration); + + logger.LogDebug("Получаю актуальный nextRun для шаблона {templateId}. Текущий nextRun: {nextRun}. Период {duration} дней, с {_startDate} по {_endDate}. Исключить выходные и праздники: {excludeWeekends}. Для нового шаблона: {isNew}", targetTemplate.Id, targetTemplate.NextRun, duration, _startDate, _endDate, excludeWeekends, isNew); + + // Получаем список рабочих дней (исключая выходные и праздники) + var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); + + if (workDays.Count == 0) + { + logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем шаблон со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); + + // смещаем nextRun вперед + var result = FillTemplateResultWithOffsetError(targetTemplate, _startDate, referenceDate); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + // если isNew, то берем первый день периода + if (isNew) + { + var result = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, true), //CreateDateTimeOffset(workDays.First(), referenceDate), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. Это было создание нового, поместили его в начало периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + // есть ли NextRun и попадает ли он на рабочий день + // проверяем чтоб было не меньше чем UtcNow + //TODO: тут еще сделать проверку нагрузки на день + if (targetTemplate.NextRun.HasValue && workDays.Contains(DateOnly.FromDateTime(targetTemplate.NextRun.Value.Date)) && targetTemplate.NextRun.Value > DateTimeOffset.UtcNow) + { + var result = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: CreateDateTimeOffset(targetTemplate.NextRun.Value, referenceDate), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun актуальный, возвращаем его. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", result.Id, result.NextRun, result.NextRunOld); + + return result; + } + + + //TODO: Временный костыль, перемещаем на конец периода + + var endPeriodResult = new TemplateNextRunResultDto + ( + Id: targetTemplate.Id, + NextRun: FindNextAvailableDateIgnoreLoad(workDays, referenceDate, false), + NextRunOld: targetTemplate.NextRun + ); + + logger.LogInformation("Получение актуального NextRun завершено, templateId: {templateId}. NextRun перемещен на конец периода. NextRun {nextRun}. OldNextRun у {oldNextRun} шт.", endPeriodResult.Id, endPeriodResult.NextRun, endPeriodResult.NextRunOld); + + return endPeriodResult; } - - - public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates, bool excludeWeekends) { var _startDate = GetStartPeriodDate(startDate, referenceDate); var _endDate = GetEndPeriodDate(_startDate, duration); - logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}", allTemplates.Count, duration, _startDate, _endDate, referenceDate); - - // Получаем список выходных/праздничных дней - var weekends = new HashSet(); - if (excludeWeekends) - { - weekends = await GetWeekendsAsync(_startDate, _endDate); - logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); - - foreach (var weekend in weekends) - logger.LogDebug("Не рабочий день: {weekend}", weekend); - } + logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends); // Получаем список рабочих дней (исключая выходные и праздники) - var workDays = GetWorkDays(_startDate, _endDate, weekends); - - logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count); - - foreach (var workDay in workDays) - logger.LogDebug("Рабочий день: {workDay}", workDay); + var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); if (workDays.Count == 0) { logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); // смещаем все nextRun вперед - var result = allTemplates.Select(t => new TemplateNextRunResultDto - { - Id = t.Id, - NextRun = t.NextRun.HasValue - // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun - ? CreateNextRun(t.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate) - // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun - : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), - NextRunOld = t.NextRun - }).OrderBy(t => t.NextRun).ToList(); - - foreach (var item in result) - logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id}, nextRun: {NextRun}, nextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); + var result = allTemplates.Select(t => + FillTemplateResultWithOffsetError(t, _startDate, referenceDate) + ).OrderBy(t => t.NextRun).ToList(); logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", result.Count, result.Count(t => t.NextRun == t.NextRunOld), result.Count(t => t.NextRun != t.NextRunOld)); @@ -86,13 +126,12 @@ namespace PARR.DAL.NextRunServices.Subservices } var totalWorks = allTemplates.Count; - var totalDays = workDays.Count; // интервал между шаблонами - var interval = GetInterval(totalDays, totalWorks); + var interval = GetInterval(workDays.Count, totalWorks); // строим план, дни на которые должны быть назанчены шаблоны - var plan = GetPlan(totalWorks, totalDays, workDays); + var plan = GetPlan(totalWorks, workDays); // Подсчитываем, сколько работ должно быть на каждый день @@ -168,11 +207,11 @@ namespace PARR.DAL.NextRunServices.Subservices foreach (var template in templates) { templatesToKeep.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = template.NextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: template.NextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } } @@ -183,11 +222,11 @@ namespace PARR.DAL.NextRunServices.Subservices { var template = templates[i]; templatesToKeep.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = template.NextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: template.NextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } @@ -228,11 +267,11 @@ namespace PARR.DAL.NextRunServices.Subservices currentDailyLoad[day]++; updatedTemplates.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = nextRun.Value, - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: nextRun.Value, + NextRunOld: template.NextRun + )); logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun); } @@ -242,11 +281,11 @@ namespace PARR.DAL.NextRunServices.Subservices // Возвращаем с ошибкой updatedTemplates.Add(new TemplateNextRunResultDto - { - Id = template.Id, - NextRun = CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), - NextRunOld = template.NextRun - }); + ( + Id: template.Id, + NextRun: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld: template.NextRun + )); } } @@ -255,7 +294,7 @@ namespace PARR.DAL.NextRunServices.Subservices updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList(); - logger.LogDebug("--- Результат распределения: ---"); + logger.LogDebug("--- Результат распределения ---"); foreach (var item in updatedTemplates) logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); @@ -265,6 +304,77 @@ namespace PARR.DAL.NextRunServices.Subservices } + /// + /// Заполнить TemplateNextRunResultDto, если нет рабочих дней со смещением + /// + /// + /// + private TemplateNextRunResultDto FillTemplateResultWithOffsetError(TemplateNextRunDto template, DateOnly startDate, DateTimeOffset referenceDate) + { + var result = new TemplateNextRunResultDto + ( + Id: template.Id, + //NextRun = template.NextRun.HasValue + // // если есть nextRun, то прибавляем к нему offsetYearsIfErrorCalcNextRun + // ? CreateNextRun(template.NextRun.Value.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate) + // // если нет nextRun, берем сегодня и прибавляем offsetYearsIfErrorCalcNextRun + // : CreateNextRun(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRun: CreateDateTimeOffset(startDate.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), + NextRunOld: template.NextRun + ); + + logger.LogDebug("Нет рабочих дней, сдвинул nextRun у шаблона {Id} на {offsetYearsIfErrorCalcNextRun} лет вперед, nextRun: {NextRun}, nextRunOld: {NextRunOld}", result.Id, offsetYearsIfErrorCalcNextRun, result.NextRun, result.NextRunOld); + + return result; + } + + + + /// + /// Получает NextRun не учитывая нагрузку на день + /// + /// + /// + /// Искать с начала = true, false - искать с конца + /// + private DateTimeOffset FindNextAvailableDateIgnoreLoad(List workDays, DateTimeOffset referenseDate, bool isSearchFirst) + { + // возможно это временная реализация метода, используется для GetValidNextRunForTemplateAsync + + if (!isSearchFirst) + { + // берем первый рабочий день с конца и возвращаем его + var lastDate = CreateDateTimeOffset(workDays.Last(), referenseDate); + logger.LogDebug("Берем первый рабочий день с конца. Сгенерированный NextRun: {lastDate}", lastDate); + return lastDate; + } + + // будем искать сначала + // берем первый день, и смотрим не прошло ли уже время + var firstDate = CreateDateTimeOffset(workDays.First(), referenseDate); + if (firstDate > DateTimeOffset.UtcNow) + { + logger.LogDebug("Берем первый день с начала. Сгенерированный NextRun: {firstDate}", firstDate); + return firstDate; + } + + + // тогда берем второй день + if (workDays.Count > 1) + { + var secondDate = CreateDateTimeOffset(workDays.ElementAt(1), referenseDate); + logger.LogDebug("Берем второй день сначала, так как у первого дня уже время прошло. Сгенерированный NextRun: {secondDate}", secondDate); + return secondDate; + } + + logger.LogWarning("Вернули nextRun: {firstDate} из первого дня распределения, хотя время уже просрочено. Всего рабочих дней: {workDays}", firstDate, workDays.Count); + // если вдруг список рабочих дней был не >1, то тогда берем первый и вренем его. Так никогда не должно быть + return firstDate; + } + + + + /// /// Находит следующий день с доступной нагрузкой, учитывая expectedDailyLoad /// @@ -282,7 +392,7 @@ namespace PARR.DAL.NextRunServices.Subservices if (current < expected) { - return CreateNextRun(day, referenceDate); + return CreateDateTimeOffset(day, referenceDate); } } @@ -294,7 +404,7 @@ namespace PARR.DAL.NextRunServices.Subservices if (current < expected) { - return CreateNextRun(day, referenceDate); + return CreateDateTimeOffset(day, referenceDate); } } @@ -302,7 +412,7 @@ namespace PARR.DAL.NextRunServices.Subservices var minLoad = currentDailyLoad.Values.Min(); var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; - return CreateNextRun(minDay, referenceDate); + return CreateDateTimeOffset(minDay, referenceDate); } @@ -311,11 +421,12 @@ namespace PARR.DAL.NextRunServices.Subservices /// Получить план: дни, на которые должны быть назначены шаблоны /// /// - /// /// /// - private List GetPlan(int totalWorks, int totalDays, List workDays) + private List GetPlan(int totalWorks, List workDays) { + var totalDays = workDays.Count(); + var interval = GetInterval(totalDays, totalWorks); var plan = new List(); @@ -336,8 +447,11 @@ namespace PARR.DAL.NextRunServices.Subservices plan.Add(date); } + logger.LogDebug("--- План распределения ---"); + foreach (var item in plan.GroupBy(t => t)) + logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count()); + return plan; - // return new HashSet(plan); } @@ -356,23 +470,23 @@ namespace PARR.DAL.NextRunServices.Subservices /// - /// Создать NextRun (из DateTimeOffset + refDate) + /// Создать DateTimeOffset (из DateTimeOffset + refDate) /// /// Откуда взять дату /// Откуда взять время /// - private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate) + private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate) { - return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate); + return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate); } /// - /// Создать NextRun (из DateOnly + refDate) + /// Создать DateTimeOffset (из DateOnly + refDate) /// /// /// /// - private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate) + private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate) { return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero); } @@ -395,10 +509,15 @@ namespace PARR.DAL.NextRunServices.Subservices if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) dateStart = DateOnly.FromDateTime(referenceDate.Date); - // если dateStart < чем текущая дата, то берем ьекущую дату + // если dateStart < чем текущая дата, то берем текущую дату if (dateStart < DateOnly.FromDateTime(DateTime.UtcNow.Date)) dateStart = DateOnly.FromDateTime(DateTime.UtcNow.Date); + // если текущее время > referenceDate, то прибавляем 1 день + var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); + if (dateTimeStart < DateTimeOffset.UtcNow) + dateStart = dateStart.AddDays(1); + return dateStart; } @@ -428,6 +547,10 @@ namespace PARR.DAL.NextRunServices.Subservices { var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); + logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); + foreach (var weekend in weekends) + logger.LogDebug("Не рабочий день: {weekend}", weekend); + return weekends.ToHashSet(); } @@ -437,10 +560,15 @@ namespace PARR.DAL.NextRunServices.Subservices /// /// /// - /// + /// Исключить выходные и праздники /// - private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) + private async Task> GetWorkDaysAsync(DateOnly start, DateOnly end, bool excludeWeekends) { + // Получаем список выходных/праздничных дней + var weekends = new HashSet(); + if (excludeWeekends) + weekends = await GetWeekendsAsync(start, end); + var result = new List(); var date = start; @@ -454,6 +582,10 @@ namespace PARR.DAL.NextRunServices.Subservices date = date.AddDays(1); } + logger.LogDebug("Найдено рабочих дней: {Count}", result.Count); + foreach (var workDay in result) + logger.LogDebug("Рабочий день: {workDay}", workDay); + return result; } diff --git a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs b/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs deleted file mode 100644 index 93c7ba2f..00000000 --- a/PARR.DAL/NextRunServices/Subservices/TemplateDistributor_old.cs +++ /dev/null @@ -1,345 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using PARR.DAL.NextRunServices.Models; -using PARR.DAL.Services.Interfaces; - -namespace PARR.DAL.NextRunServices.Subservices -{ - internal class TemplateDistributor_old //: ITemplateDistributor - { - private readonly ILogger logger; - private readonly IWeekendDayService weekendDayService; - - public TemplateDistributor_old( - ILogger logger, - IWeekendDayService weekendDayService - ) - { - this.logger = logger; - this.weekendDayService = weekendDayService; - } - - /// - /// Распределяет все шаблоны на указанный период, учитывая план нагрузки. - /// Возвращает список всех шаблонов с обновлёнными датами выполнения. - /// - public async Task> DistributeTemplatesAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List allTemplates) - { - logger.LogInformation("Начинаем распределение {Count} шаблонов с {StartDate} по {EndDate}", allTemplates.Count, startDate, endDate); - - // Получаем список выходных/праздничных дней - var weekends = await GetWeekendsAsync(startDate, endDate); - - // Получаем список рабочих дней (исключая выходные и праздники) - var workDays = GetWorkDays(startDate, endDate, weekends); - - logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); - - if (workDays.Count == 0) - { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); - return allTemplates; - } - - var totalWorks = allTemplates.Count; - var totalDays = workDays.Count; - - //------------- - - // Вычисляем интервал между работами: сколько "дней" приходится на одну работу - var interval = (double)totalDays / totalWorks; - - // Создаём новый список шаблонов с обновлёнными датами - var updatedTemplates = new List(); - - for (int i = 0; i < totalWorks; i++) - { - // Вычисляем индекс дня, на который нужно назначить i-ю работу - var dayIndex = (int)Math.Round(i * interval); - - // Защита от возможного выхода за границы массива из-за округления - if (dayIndex >= totalDays) dayIndex = totalDays - 1; - - var date = workDays[dayIndex]; - - // Формируем дату выполнения: дата + время из referenceDate - var nextRun = new DateTimeOffset(date.Year, date.Month, date.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - //------------------ - // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) - if (nextRun < referenceDate) - { - logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайший рабочий день после referenceDate (включая её).", allTemplates[i].Id, nextRun, referenceDate); - - // Ищем ближайший день после referenceDate (включая его) - var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), date); - if (newDate.HasValue) - { - nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - } - else - { - // Если не нашли — используем первый рабочий день - logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первый рабочий день.", allTemplates[i].Id); - var firstDay = workDays[0]; - nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - } - } - //------------------ - - var updatedTemplate = new TemplateNextRunDto - { - Id = allTemplates[i].Id, - NextRun = nextRun - }; - - updatedTemplates.Add(updatedTemplate); - - logger.LogDebug("Шаблон {TemplateId} назначен на {NextRun}", updatedTemplate.Id, updatedTemplate.NextRun); - } - - logger.LogInformation("Распределение завершено. Возвращаем {Count} записей.", updatedTemplates.Count); - - return updatedTemplates; - } - - /// - /// Возвращает актуальную дату выполнения для одного шаблона, учитывая план нагрузки. - /// Возвращает обновлённый шаблон с новой датой выполнения. - /// - public async Task GetValidNextRunForTemplateAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) - { - logger.LogDebug("Запрос актуальной даты для шаблона {TemplateId}", targetTemplate.Id); - - // Получаем список выходных/праздничных дней - var weekends = await GetWeekendsAsync(startDate, endDate); - - // Получаем список рабочих дней - var workDays = GetWorkDays(startDate, endDate, weekends); - - logger.LogDebug("Найдено {Count} рабочих дней", workDays.Count); - - if (workDays.Count == 0) - { - logger.LogWarning("Нет доступных рабочих дней в диапазоне {StartDate} - {EndDate}", startDate, endDate); - - // Возвращаем дату, равную referenceDate, если не нашли рабочих дней - logger.LogWarning("Используем referenceDate как дату выполнения для шаблона {TemplateId}", targetTemplate.Id); - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = referenceDate }; - } - - var now = DateTimeOffset.UtcNow; - - // Проверяем, устарела ли дата - if (targetTemplate.NextRun.HasValue && targetTemplate.NextRun.Value < now) - { - logger.LogDebug("Дата шаблона {TemplateId} ({Date}) устарела", targetTemplate.Id, targetTemplate.NextRun); - - // Строим план на основе оставшихся шаблонов (без устаревшего) - var remainingTemplates = allTemplates.Where(t => t.Id != targetTemplate.Id && t.NextRun.HasValue).ToList(); - - // Вычисляем план для оставшихся - var plan = await BuildPlanAsync(startDate, endDate, referenceDate, remainingTemplates, weekends, workDays); - - // Находим следующую дату в плане, не занятую - var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(now.DateTime), plan); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти новую дату для устаревшего шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); - - // Используем ближайшую дату после now, не в плане - var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(now.DateTime), DateOnly.FromDateTime(now.DateTime)); - if (fallbackDate.HasValue) - { - var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - } - - // Если дата не устарела, но не соответствует плану — переносим - var currentPlan = await BuildPlanAsync(startDate, endDate, referenceDate, allTemplates, weekends, workDays); - var expectedDate = currentPlan.FirstOrDefault(d => d == DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (expectedDate == default) - { - logger.LogDebug("Дата шаблона {TemplateId} ({Date}) не соответствует плану. Переносим на ближайшую дату в плане.", targetTemplate.Id, targetTemplate.NextRun); - - var newDate = FindNextAvailableDateNotInPlan(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), currentPlan); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти новую дату для шаблона {TemplateId} по плану. Используем ближайшую дату.", targetTemplate.Id); - - // Используем ближайшую дату, не в плане - var fallbackDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (fallbackDate.HasValue) - { - var nextRun = new DateTimeOffset(fallbackDate.Value.Year, fallbackDate.Value.Month, fallbackDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на резервную дату {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти резервную дату для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - } - - logger.LogDebug("Шаблон {TemplateId} имеет актуальную дату: {NextRun}", targetTemplate.Id, targetTemplate.NextRun); - - // Проверяем, не раньше ли дата, чем referenceDate (не включая саму referenceDate) - if (targetTemplate.NextRun < referenceDate) - { - logger.LogWarning("Шаблон {TemplateId} имеет дату {NextRun}, которая раньше referenceDate {ReferenceDate}. Переносим на ближайшую дату.", targetTemplate.Id, targetTemplate.NextRun, referenceDate); - - var newDate = FindNextAvailableDate(workDays, DateOnly.FromDateTime(referenceDate.DateTime), DateOnly.FromDateTime(targetTemplate.NextRun.Value.DateTime)); - if (newDate.HasValue) - { - var nextRun = new DateTimeOffset(newDate.Value.Year, newDate.Value.Month, newDate.Value.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - logger.LogDebug("Шаблон {TemplateId} перенесен на {NewDate}", targetTemplate.Id, nextRun); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - else - { - logger.LogWarning("Не удалось найти дату после referenceDate для шаблона {TemplateId}. Используем первую рабочую дату.", targetTemplate.Id); - - var firstDay = workDays[0]; - var nextRun = new DateTimeOffset(firstDay.Year, firstDay.Month, firstDay.Day, referenceDate.Hour, referenceDate.Minute, referenceDate.Second, referenceDate.Offset); - - return new TemplateNextRunDto { Id = targetTemplate.Id, NextRun = nextRun }; - } - } - - return targetTemplate; - } - - /// - /// Строит план работ на основе списка шаблонов - /// - private async Task> BuildPlanAsync( - DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, List templates, HashSet weekends, List workDays) - { - var totalWorks = templates.Count; - var totalDays = workDays.Count; - - if (totalDays == 0) return new List(); - - var interval = (double)totalDays / totalWorks; - var plan = new List(); - - for (int i = 0; i < totalWorks; i++) - { - var dayIndex = (int)Math.Round(i * interval); - if (dayIndex >= totalDays) dayIndex = totalDays - 1; - - var date = workDays[dayIndex]; - plan.Add(date); - } - - return plan.OrderBy(d => d).ToList(); - } - - /// - /// Ищет следующую дату в плане, которая не занята - /// - private DateOnly? FindNextAvailableDateNotInPlan(List workDays, DateOnly afterDate, List plan) - { - var planSet = new HashSet(plan); - - // Ищем вперед - foreach (var day in workDays) - { - if (day > afterDate && !planSet.Contains(day)) - { - return day; - } - } - - // Ищем назад - foreach (var day in workDays.AsEnumerable().Reverse()) - { - if (day <= afterDate && !planSet.Contains(day)) - { - return day; - } - } - - return null; - } - - /// - /// Ищет следующую доступную дату после указанной, если не находит — ищет назад. - /// - private DateOnly? FindNextAvailableDate(List workDays, DateOnly afterDate, DateOnly originalDate) - { - var future = workDays.FirstOrDefault(d => d >= afterDate); - if (future != default) return future; - - var past = workDays.Where(t => t >= originalDate).OrderByDescending(t => t).FirstOrDefault(); - return past != default ? (DateOnly?)past : null; - } - - /// - /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. - /// - private List GetWorkDays(DateOnly start, DateOnly end, HashSet weekends) - { - var result = new List(); - var date = start; - - while (date <= end) - { - var dayOfWeek = date.DayOfWeek; - if (!weekends.Contains(date)) - { - result.Add(date); - } - date = date.AddDays(1); - } - - return result; - } - - /// - /// Получить список выходных дней - /// - private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) - { - var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); - return weekends.ToHashSet(); - } - } -} \ No newline at end of file diff --git a/PARR.Test/NextRun/NextRunTest.cs b/PARR.Test/NextRun/NextRunTest.cs index 5791f4a2..c815937f 100644 --- a/PARR.Test/NextRun/NextRunTest.cs +++ b/PARR.Test/NextRun/NextRunTest.cs @@ -11,15 +11,37 @@ namespace PARR.Test.NextRun { this.templateDistributor = templateDistributor; } - + int periodDays = 10; - DateTimeOffset referenceDate = new DateTimeOffset(2025, 12, 1, 7, 0, 0, TimeSpan.Zero); + DateTimeOffset referenceDate = new DateTimeOffset(2025, 1, 20, 5, 0, 0, TimeSpan.Zero); public async Task Test() { - await DistributeTemplatesAsync(); + await DistributeTemplatesAsync(); + //await GetValidNextRunForTemplateAsync(); } + /// + /// Получить актуальный NextRun + /// + /// + private async Task GetValidNextRunForTemplateAsync() + { + // исключать выходные и праздники + var excludeWeekends = true; + bool isNew = false; + + var targetTemplate = new TemplateNextRunDto + ( + Id: Guid.Parse("11F047E6-3631-4676-8959-FC75FF6BC963"), + //NextRun = new DateTimeOffset(2026, 1, 20, 4, 0, 0, TimeSpan.Zero) + NextRun: null + ); + + var template = await templateDistributor.GetValidNextRunForTemplateAsync(GetDateStart(), periodDays, referenceDate, targetTemplate, new List(), excludeWeekends, isNew); + } + + /// /// распределить шаблоны /// @@ -72,18 +94,18 @@ namespace PARR.Test.NextRun { var templates = new List(); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("55F047E6-3631-4676-8959-FC75FF6BC963"), NextRun = new DateTimeOffset(2027, 1, 15, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DD15E9F6-AAD4-4FE6-9441-7E7FF67781A3"), NextRun = new DateTimeOffset(2025, 12, 24, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("55F047E6-3631-4676-8959-FC75FF6BC963"), NextRun: new DateTimeOffset(2027, 1, 15, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DD15E9F6-AAD4-4FE6-9441-7E7FF67781A3"), NextRun: new DateTimeOffset(2025, 12, 24, 3, 0, 0, TimeSpan.Zero))); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("78EE4A94-DBEE-41D1-B51A-CE3BD5AEE4D4"), NextRun = new DateTimeOffset(2025, 12, 29, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun = new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); - templates.Add(new TemplateNextRunDto { Id = Guid.Parse("25F53A28-D657-4B9A-82F3-CE9AF4D53385"), NextRun = new DateTimeOffset(2025, 12, 30, 3, 0, 0, TimeSpan.Zero) }); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("78EE4A94-DBEE-41D1-B51A-CE3BD5AEE4D4"), NextRun: new DateTimeOffset(2025, 12, 29, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B397F1FE-DF2B-4B50-B8D6-2DABECD3D5A1"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("619D9621-2B45-4108-AF54-CF96862A8E9B"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("D74D831C-D435-46BF-9B5F-A24884AAA7DC"), NextRun: new DateTimeOffset(2025, 12, 25, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("DE7241F1-5C62-4164-A3DC-99B946350E10"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("94A7375E-D1BD-49B7-9186-D26E85F1CF74"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("B3681F5F-10A5-425C-BBEA-EA2B85FE0967"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("3259B408-48A3-4995-AF9A-097477ECAB81"), NextRun: new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero))); + templates.Add(new TemplateNextRunDto(Id: Guid.Parse("25F53A28-D657-4B9A-82F3-CE9AF4D53385"), NextRun: new DateTimeOffset(2025, 12, 30, 3, 0, 0, TimeSpan.Zero))); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("F75F6B1A-875F-4CB2-98C8-6721FD06C18C"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("2C06C63D-BE92-4312-98FB-0547E74B8A33"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) }); //templates.Add(new TemplateNextRunDto { Id = Guid.Parse("B22D314E-9F86-49BB-826F-20919EC55A7F"), NextRun = new DateTimeOffset(2025, 12, 18, 3, 0, 0, TimeSpan.Zero) });