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 Task GetValidNextRunForTemplateAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, TemplateNextRunDto targetTemplate, List allTemplates) { //TODO: throw new NotImplementedException(); } public async Task> DistributeTemplatesAsync(DateOnly startDate, int duration, DateTimeOffset referenceDate, List allTemplates) { 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 = await GetWeekendsAsync(_startDate, _endDate); logger.LogDebug("Найдено не рабочих дней: {Count}", weekends.Count); foreach (var weekend in weekends) logger.LogDebug("Не рабочий день: {weekend}", weekend); // Получаем список рабочих дней (исключая выходные и праздники) var workDays = GetWorkDays(_startDate, _endDate, weekends); logger.LogDebug("Найдено рабочих дней: {Count}", workDays.Count); foreach (var workDay in workDays) logger.LogDebug("Рабочий день: {workDay}", workDay); 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); 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 totalDays = workDays.Count; // интервал между шаблонами var interval = GetInterval(totalDays, totalWorks); // строим план, дни на которые должны быть назанчены шаблоны var plan = GetPlan(totalWorks, totalDays, 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 = CreateNextRun(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; } /// /// Находит следующий день с доступной нагрузкой, учитывая 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 CreateNextRun(day, referenceDate); } } // Если нет дня с доступной нагрузкой — ищем день с минимальной текущей нагрузкой, но не превышающий план foreach (var day in workDays) { var expected = expectedDailyLoad.GetValueOrDefault(day, 0); var current = currentDailyLoad.GetValueOrDefault(day, 0); if (current < expected) { return CreateNextRun(day, referenceDate); } } // Если все дни уже загружены по плану — ищем день с минимальной нагрузкой var minLoad = currentDailyLoad.Values.Min(); var minDay = currentDailyLoad.First(x => x.Value == minLoad).Key; return CreateNextRun(minDay, referenceDate); } /// /// Получить план: дни, на которые должны быть назначены шаблоны /// /// /// /// /// private List GetPlan(int totalWorks, int totalDays, List workDays) { 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); } return plan; // return new HashSet(plan); } /// /// Рассчитать интервал между шаблонами /// /// Кол-во рабочих дней /// Кол-во шаблонов /// private double GetInterval(int totalDays, int totalWorks) { var interval = (double)totalDays / totalWorks; return interval; } /// /// Создать NextRun (из DateTimeOffset + refDate) /// /// Откуда взять дату /// Откуда взять время /// private DateTimeOffset CreateNextRun(DateTimeOffset date, DateTimeOffset refDate) { return CreateNextRun(DateOnly.FromDateTime(date.Date), refDate); } /// /// Создать NextRun (из DateOnly + refDate) /// /// /// /// private DateTimeOffset CreateNextRun(DateOnly date, DateTimeOffset refDate) { return new DateTimeOffset(date.Year, date.Month, date.Day, refDate.Hour, refDate.Minute, refDate.Second, TimeSpan.Zero); } /// /// Получить дату начала распределения относительно StartDate, ReferenceDate и текущей даты /// /// /// /// private DateOnly GetStartPeriodDate(DateOnly startDate, DateTimeOffset referenceDate) { // дата начала не может быть раньше referenceDate // и не может быть раньше чем сегодня 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); 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(); return weekends.ToHashSet(); } /// /// Возвращает список рабочих дней в диапазоне, исключая выходные и праздники. /// /// /// /// /// 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; } } }