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 : ITemplateDistributor { private readonly ILogger logger; private readonly IWeekendDayService weekendDayService; /// /// Смещение nextRun на несколько лет вперед, если не смог его рассчитать /// private const int offsetYearsIfErrorCalcNextRun = 5; public TemplateDistributor( ILogger logger, IWeekendDayService weekendDayService ) { this.logger = logger; this.weekendDayService = weekendDayService; } public async Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates, bool excludeWeekends, bool isNew) { //if (offset.HasValue) // referenceDate = referenceDate.ToOffset(offset.Value); //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); var workDays = await GetWorkDaysV2Async(_startDate, _endDate, excludeWeekends, referenceDate); 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) { //if (offset.HasValue) //referenceDate = referenceDate.ToOffset(offset.Value); var _startDate = GetStartPeriodDate(startDate, referenceDate); var _endDate = GetEndPeriodDate(_startDate, duration); logger.LogInformation("Начинаем распределение {Count} шаблонов на {duration} дней, с {_startDate} по {_endDate}. ReferenceDate: {referenceDate}. Исключить выходные и праздники: {excludeWeekends}.", allTemplates.Count, duration, _startDate, _endDate, referenceDate, excludeWeekends); // Получаем список рабочих дней (исключая выходные и праздники) //var workDays = await GetWorkDaysAsync(_startDate, _endDate, excludeWeekends); var workDays = await GetWorkDaysV2Async(_startDate, _endDate, excludeWeekends, referenceDate); if (workDays.Count == 0) { logger.LogWarning("Нет доступных рабочих дней в диапазоне {_startDate} - {_endDate}. Возвращаем список шаблонов со смещением: {offsetYearsIfErrorCalcNextRun} лет.", _startDate, _endDate, offsetYearsIfErrorCalcNextRun); // смещаем все nextRun вперед 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)); return result; } var totalWorks = allTemplates.Count; // интервал между шаблонами var interval = GetInterval(workDays.Count, totalWorks); // строим план, дни на которые должны быть назанчены шаблоны var plan = GetPlan(totalWorks, workDays); // Подсчитываем, сколько работ должно быть на каждый день var expectedDailyLoad = new Dictionary(); foreach (var day in plan) expectedDailyLoad[day] = expectedDailyLoad.GetValueOrDefault(day, 0) + 1; #region Группируем шаблоны по дням, на которые они уже назначены var templatesByDay = new Dictionary>(); foreach (var template in allTemplates) { if (template.NextRun.HasValue) { var day = DateOnly.FromDateTime(template.NextRun.Value.Date); if (!templatesByDay.ContainsKey(day)) { templatesByDay[day] = new List(); } templatesByDay[day].Add(template); } } #endregion #region Подсчитываем текущую нагрузку на каждый день var currentDailyLoad = new Dictionary(); foreach (var day in workDays) { currentDailyLoad[day] = 0; } foreach (var kvp in templatesByDay) { var day = kvp.Key; var count = kvp.Value.Count; currentDailyLoad[day] = count; } #endregion #region Определяем, какие шаблоны нужно оставить, а какие — переназначить var templatesToKeep = new List(); var templatesToReassign = new List(); foreach (var kvp in templatesByDay) { var day = kvp.Key; var templates = kvp.Value; // Если день — нерабочий, то все шаблоны нужно переназначить if (!workDays.Contains(day)) { foreach (var template in templates) { templatesToReassign.Add(template); logger.LogDebug("Шаблон {TemplateId} назначен на нерабочий день {NextRun} — будет переназначен.", template.Id, template.NextRun); } continue; } var expectedCount = expectedDailyLoad.GetValueOrDefault(day, 0); var currentCount = templates.Count; if (currentCount <= expectedCount) { // Все шаблоны в этом дне можно оставить foreach (var template in templates) { templatesToKeep.Add(new TemplateNextRunResultDto ( Id: template.Id, NextRun: template.NextRun.Value, NextRunOld: template.NextRun )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } } else { // Оставляем только expectedCount штук, остальные — переназначить for (int i = 0; i < expectedCount; i++) { var template = templates[i]; templatesToKeep.Add(new TemplateNextRunResultDto ( Id: template.Id, NextRun: template.NextRun.Value, NextRunOld: template.NextRun )); logger.LogDebug("Шаблон {TemplateId} оставлен на дату {NextRun}, т.к. нагрузка в день не превышена.", template.Id, template.NextRun); } for (int i = expectedCount; i < currentCount; i++) { var template = templates[i]; templatesToReassign.Add(template); logger.LogDebug("Шаблон {TemplateId} переназначается, т.к. нагрузка в день превышена.", template.Id); } } } #endregion #region Добавляем шаблоны, у которых не было даты — тоже нужно распределить foreach (var template in allTemplates) { if (!template.NextRun.HasValue) { templatesToReassign.Add(template); logger.LogDebug("Шаблон {TemplateId} не имел даты — будет распределён.", template.Id); } } #endregion #region Распределяем шаблоны, которые нужно переназначить, учитывая нагрузку var updatedTemplates = new List(templatesToKeep); foreach (var template in templatesToReassign) { var nextRun = FindNextAvailableDateWithLoad(currentDailyLoad, workDays, referenceDate, expectedDailyLoad); if (nextRun.HasValue) { var day = DateOnly.FromDateTime(nextRun.Value.Date); currentDailyLoad[day]++; updatedTemplates.Add(new TemplateNextRunResultDto ( Id: template.Id, NextRun: nextRun.Value, NextRunOld: template.NextRun )); logger.LogDebug("Шаблону {TemplateId} назначен NextRun: {NextRun}, NextRunOld: {NextRunOld}", template.Id, nextRun, template.NextRun); } else { logger.LogWarning("Не удалось назначить дату для шаблона {TemplateId}", template.Id); // Возвращаем с ошибкой updatedTemplates.Add(new TemplateNextRunResultDto ( Id: template.Id, NextRun: CreateDateTimeOffset(DateTimeOffset.UtcNow.AddYears(offsetYearsIfErrorCalcNextRun), referenceDate), NextRunOld: template.NextRun )); } } #endregion updatedTemplates = updatedTemplates.OrderBy(t => t.NextRun).ToList(); logger.LogDebug("--- Результат распределения ---"); foreach (var item in updatedTemplates) logger.LogDebug("Шаблон {templateId}, NextRun: {NextRun}, NextRunOld: {NextRunOld}", item.Id, item.NextRun, item.NextRunOld); logger.LogInformation("Распределение завершено. Возвращаем {Count} записей. NextRun не изменился у {oldCount} шт. Обновлен NextRun у {changedCount} шт.", updatedTemplates.Count, updatedTemplates.Count(t => t.NextRun == t.NextRunOld), updatedTemplates.Count(t => t.NextRun != t.NextRunOld)); return updatedTemplates; } /// /// Заполнить 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 /// /// /// /// /// /// private DateTimeOffset? FindNextAvailableDateWithLoad(Dictionary currentDailyLoad, List workDays, DateTimeOffset referenceDate, Dictionary expectedDailyLoad) { foreach (var day in workDays) { var expected = expectedDailyLoad.GetValueOrDefault(day, 0); var current = currentDailyLoad.GetValueOrDefault(day, 0); if (current < expected) { return CreateDateTimeOffset(day, referenceDate); } } // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план foreach (var day in workDays) { var expected = expectedDailyLoad.GetValueOrDefault(day, 0); var current = currentDailyLoad.GetValueOrDefault(day, 0); if (current < expected) { return CreateDateTimeOffset(day, referenceDate); } } // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой var minLoad = currentDailyLoad.Values.Min(); var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; return CreateDateTimeOffset(minDay, referenceDate); } //--------------------------------------------------------------------------------------------------------------------------------------------------------------------- /// /// Получить план: дни, на которые должны быть назначены шаблоны /// /// /// /// private List GetPlan(int totalWorks, List workDays) { var totalDays = workDays.Count(); var interval = GetInterval(totalDays, totalWorks); var plan = new List(); for (int i = 0; i < totalWorks; i++) { #region Вынести в метод // Вычисляем индекс дня, на который нужно назначить i-ю работу //var dayIndex = (int)Math.Round(i * interval); var dayIndex = (int)Math.Floor(i * interval); // Защита от возможного выхода за границы массива из-за округления if (dayIndex >= totalDays) dayIndex = totalDays - 1; #endregion var date = workDays[dayIndex]; plan.Add(date); } logger.LogDebug("--- План распределения ---"); foreach (var item in plan.GroupBy(t => t)) logger.LogDebug("Дата: {date}, кол-во работ: {workCount}", item.Key, item.Count()); return plan; } /// /// Рассчитать интервал между шаблонами /// /// Кол-во рабочих дней /// Кол-во шаблонов /// private double GetInterval(int totalDays, int totalWorks) { var interval = (double)totalDays / totalWorks; logger.LogDebug("Интевал между шаблонами: {interval}", interval); return interval; } /// /// Создать DateTimeOffset (из DateTimeOffset + refDate) /// /// Откуда взять дату /// Откуда взять время /// private DateTimeOffset CreateDateTimeOffset(DateTimeOffset date, DateTimeOffset refDate) { return CreateDateTimeOffset(DateOnly.FromDateTime(date.Date), refDate); } /// /// Создать DateTimeOffset (из DateOnly + refDate) /// /// /// /// private DateTimeOffset CreateDateTimeOffset(DateOnly date, DateTimeOffset refDate) { return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, refDate.Offset); } /// /// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты /// /// /// /// private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate) { // дата начала не может быть раньше referenceDate // и не может быть раньше чем сегодня #region old //var dateStart = startDate; //// если dateStart < чем refDate, то берем refDate //if (dateStart < DateOnly.FromDateTime(referenceDate.Date)) // dateStart = DateOnly.FromDateTime(referenceDate.Date); //// если 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; #endregion // 1. Конвертируем startDate (UTC) в целевой часовой пояс var startDateUtc = new DateTimeOffset( startDate.Year, startDate.Month, startDate.Day, 0, 0, 0, TimeSpan.Zero); var startDateInTargetTz = startDateUtc.ToOffset(referenceDate.Offset); var dateStart = DateOnly.FromDateTime(startDateInTargetTz.DateTime); logger.LogDebug("startDate UTC ({startDate} 00:00 UTC) конвертирована в целевой часовой пояс {offset}: {dateStart}", startDate, referenceDate.Offset, dateStart); // 2. Дата начала не может быть раньше даты из referenceDate (в его часовом поясе) var refDateInTargetTz = DateOnly.FromDateTime(referenceDate.Date); if (dateStart < refDateInTargetTz) { logger.LogDebug("Дата начала {dateStart} раньше даты из referenceDate {refDate}. Используем дату из referenceDate.", dateStart, refDateInTargetTz); dateStart = refDateInTargetTz; } // 3. Дата начала не может быть раньше "сегодня" в целевом часовом поясе var todayInTargetTz = DateOnly.FromDateTime(DateTimeOffset.UtcNow.ToOffset(referenceDate.Offset).Date); if (dateStart < todayInTargetTz) { logger.LogDebug("Дата начала {dateStart} раньше 'сегодня' в целевом поясе {today}. Используем 'сегодня'.", dateStart, todayInTargetTz); dateStart = todayInTargetTz; } // 4. Если момент времени "начало дня в целевом поясе" уже прошёл — добавляем 1 день var dateTimeStart = CreateDateTimeOffset(dateStart, referenceDate); if (dateTimeStart < DateTimeOffset.UtcNow) { logger.LogDebug("Момент времени {dateTimeStart} уже прошёл (сейчас {now}). Добавляем 1 день.", dateTimeStart, DateTimeOffset.UtcNow); dateStart = dateStart.AddDays(1); } logger.LogDebug("Итоговая дата начала распределения: {dateStart} (в часовом поясе {offset})", dateStart, referenceDate.Offset); return dateStart; } /// /// Получить дату конца периода распределения /// /// /// /// private DateOnly GetEndPeriodDate(DateOnly startDate, int duration) { // duration — количество дней, включая startDate // поэтому вычитаем 1, чтобы получить сдвиг return startDate.AddDays(duration - 1); } ///// ///// Получить список выходных дней ///// ///// ///// ///// //private async Task> GetWeekendsAsync(DateOnly startDate, DateOnly endDate) //{ // 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(); //} /// /// Получить список выходных дней /// /// /// /// public async Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate, TimeSpan? offset = null) { if (offset.HasValue) referenceDate = referenceDate.ToOffset(offset.Value); // проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd //bool isPreviousDays = IsPreviousDays(referenceDate); //if (isPreviousDays) //{ // // Это предыдущие сутки, корректируем // startDate = startDate.AddDays(-1); // // так как мы потом будем сдвигать выходные дни влево, то надо взять на один день дальше, чтоб знать какой он // // вроде бы это не костыль // endDate = endDate.AddDays(1); // //endDate = endDate.AddDays(-1); // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты для получения выходных дней, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, startDate, endDate); //} var weekends = await weekendDayService.GetWeekends(startDate, endDate).ToListAsync(); var weekendsResult = new List(); //if (isPreviousDays) //{ // // так как должны быть предыдущие дни, отнимаем 1 день // foreach (var weekend in weekends) // { // var previous = weekend.AddDays(-1); // logger.LogDebug("Выходной день, -1, так как день должен быть предыдущий, старое значение {weekend}, новое {previous}", weekend, previous); // weekendsResult.Add(previous); // } //} //else //{ // weekendsResult = weekends; //} logger.LogDebug("Найдено не рабочих дней: {Count}", weekendsResult.Count); foreach (var weekend in weekendsResult) logger.LogDebug("Не рабочий день: {weekend}", weekend); return weekendsResult.ToHashSet(); } ///// ///// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. ///// ///// ///// ///// Исключить выходные и праздники ///// //public 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; // while (date <= end) // { // var dayOfWeek = date.DayOfWeek; // if (!weekends.Contains(date)) // { // result.Add(date); // } // date = date.AddDays(1); // } // logger.LogDebug("Найдено рабочих дней: {Count}", result.Count); // foreach (var workDay in result) // logger.LogDebug("Рабочий день: {workDay}", workDay); // return result; //} public async Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate, TimeSpan? offset = null) { if (offset.HasValue) referenceDate = referenceDate.ToOffset(offset.Value); logger.LogDebug("Формирование списка рабочих дней, start: {start}, end: {end}, excludeWeekends: {excludeWeekends}, referenceDate: {referenceDate}", start, end, excludeWeekends, referenceDate); //// проверяем это текущие или прошлые сутки относительно refDate, если надо, корректируем dateStart, dateEnd //if (IsPreviousDays(referenceDate)) //{ // // Это предыдущие сутки, корректируем // start = start.AddDays(-1); // end = end.AddDays(-1); // logger.LogDebug("Это предыдущие сутки относительно referenceDate: {referenceDate}, корректируем даты, новые значения dateStart: {dateStart}, dateEnd: {dateEnd}", referenceDate, start, end); //} // Получаем список выходных/праздничных дней var weekends = new HashSet(); if (excludeWeekends) weekends = await GetWeekendsV2Async(start, end, referenceDate); 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); } logger.LogDebug("Найдено рабочих дней: {Count}", result.Count); foreach (var workDay in result) logger.LogDebug("Рабочий день: {workDay}", workDay); return result; } public Task> GetWeekendsV2Async(DateOnly startDate, DateOnly endDate, DateTimeOffset referenceDate) { throw new NotImplementedException(); } public Task> GetWorkDaysV2Async(DateOnly start, DateOnly end, bool excludeWeekends, DateTimeOffset referenceDate) { throw new NotImplementedException(); } ///// ///// Это предыдущие сутки? Относительно referenceDate и МСК времени ///// ///// ///// //private bool IsPreviousDays(DateTimeOffset referenceDate) //{ // // смещение часов относительно UTC - MSK // int offsetMskHour = 3; // // если время больше 21 часа, то это текущие сутки, если меньше, то прошлые // int maxHour = 24 - offsetMskHour; // if (referenceDate.Hour >= maxHour && referenceDate.Hour <= 23) // { // // это прошлые сутки // logger.LogDebug("ReferenceDate {refDate} - прошлые стуки", referenceDate); // return true; // } // else // { // // это текущие сутки // logger.LogDebug("ReferenceDate {refDate} - текущие стуки", referenceDate); // return false; // } //} } }