Enhance GridPlanner with SlotFactory for improved slot composition and management
ci / build-backend (push) Successful in 1m35s
ci / build-frontend (push) Successful in 1m2s
ci / tests (push) Successful in 3m54s
ci / sonar (push) Successful in 4m22s

Refactored the GridPlanner class to introduce a SlotFactory for better organization and clarity in slot composition. Added detailed summaries for methods to improve code documentation and understanding. Enhanced the logic for handling content and repeat slots, ensuring more efficient management of grid slots during generation. These changes contribute to improved readability and maintainability of the code.
This commit is contained in:
Leonid Pershin
2026-07-27 19:53:01 +03:00
parent 986cf02b0d
commit e55161f8f9
2 changed files with 68 additions and 46 deletions
@@ -306,6 +306,13 @@ public sealed class GridPlanner(
} }
} }
/// <summary>
/// Чем закрыть точку сетки. Решение здесь, сборка — в <see cref="SlotFactory"/>: «что ставим»
/// и «как это собрать» читаются по отдельности.
///
/// Порядок предпочтений: свежий контент → повтор вечернего блока → контент по второму кругу →
/// ничего (место закроет фон).
/// </summary>
private static PlannedSlot? Compose( private static PlannedSlot? Compose(
GridBand band, GridBand band,
SlotTarget target, SlotTarget target,
@@ -315,73 +322,79 @@ public sealed class GridPlanner(
PlanRun run PlanRun run
) )
{ {
var factory = new SlotFactory(band, target, offset, anchor, run);
if (band.SlotKind == SlotKind.SignOff) if (band.SlotKind == SlotKind.SignOff)
return Build(band.Title, null, available, SlotBlockMode.FillSlot, 1, null, null); return factory.SignOff(available);
if (band.SlotKind == SlotKind.Repeat) if (band.SlotKind == SlotKind.Repeat)
return RepeatSlot(band.Title, band.RepeatFrom, available); return factory.Repeat(band.RepeatFrom, available);
var group = Pick(band, target, run, available); var group = Pick(band, target, run, available);
if (group is not null && !run.Exhausted(group))
return factory.Content(group, available);
// Ставить нечего — либо подходящей группы нет вовсе, либо свежий контент на неделю кончился. // Свежего контента нет — либо подходящей группы не нашлось вовсе, либо ёмкость на неделю
// И то и другое закрывается повтором вечернего блока, как на настоящем ТВ: крутить по // исчерпана. И то и другое закрывается повтором, как на настоящем ТВ: крутить по третьему
// третьему кругу одно и то же хуже — зритель видит то же самое, но без подписи «повтор». // кругу одно и то же хуже — зритель видит то же самое, но без честной подписи «повтор».
if (group is null || run.Exhausted(group)) if (CanRepeat(band, run))
{ {
if (!CanRepeat(band, run)) run.Note(RepeatReason(band, group));
{ return factory.Repeat(run.PrimeStart!.Value, Math.Min(available, RepeatMinutes(band)));
run.Note(Unfillable(band, group), isError: group is null);
return group is null ? null : ContentSlot(group);
}
run.Note(
group is null
? $"Для полосы «{band.Title}» подходящей группы нет — её закрывает повтор "
+ "вечернего блока."
: $"Полосу «{band.Title}» закрывает повтор вечернего блока: свежего контента "
+ "на неделю не хватает."
);
return RepeatSlot(
band.Title,
run.PrimeStart!.Value,
Math.Min(available, RepeatMinutes(band))
);
} }
return ContentSlot(group); run.Note(Unfillable(band, group), isError: group is null);
return group is null ? null : factory.Content(group, available);
}
PlannedSlot ContentSlot(GroupCandidate picked) /// <summary>
/// Сборка слота в известной точке сетки: полоса, день, слой и смещение уже определены, меняется
/// только наполнение. Отдельный тип, а не набор замыканий внутри <see cref="Compose"/>: там
/// принимается решение, здесь — оформляется.
/// </summary>
private readonly record struct SlotFactory(
GridBand Band,
SlotTarget Target,
int Offset,
bool Anchor,
PlanRun Run
)
{
public PlannedSlot Content(GroupCandidate group, int available)
{ {
var (duration, units) = Measure(band, picked, available); var (duration, units) = Measure(Band, group, available);
run.Reserve(picked, units, duration, target.AiringsPerWeek); Run.Reserve(group, units, duration, Target.AiringsPerWeek);
return Build( return Build(
picked.Name, group.Name,
picked.Id, group.Id,
duration, duration,
band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot, Band.UnitsPerBlock > 0 ? SlotBlockMode.Count : SlotBlockMode.FillSlot,
Math.Max(1, units), Math.Max(1, units),
new SlotStrategy( new SlotStrategy(
band.Strategy, Band.Strategy,
RestartOnEnd: true, RestartOnEnd: true,
CooldownDays: band.CooldownDays CooldownDays: Band.CooldownDays
), ),
null null
); );
} }
PlannedSlot RepeatSlot(string title, TimeOnly repeatFrom, int minutes) => public PlannedSlot Repeat(TimeOnly from, int minutes) =>
Build( Build(
title, Band.Title,
null, null,
minutes, minutes,
SlotBlockMode.FillSlot, SlotBlockMode.FillSlot,
1, 1,
null, null,
new RepeatSource(band.RepeatDaysAgo, repeatFrom, minutes) new RepeatSource(Band.RepeatDaysAgo, from, minutes)
); );
PlannedSlot Build( public PlannedSlot SignOff(int minutes) =>
Build(Band.Title, null, minutes, SlotBlockMode.FillSlot, 1, null, null);
private PlannedSlot Build(
string title, string title,
Guid? groupId, Guid? groupId,
int duration, int duration,
@@ -391,23 +404,30 @@ public sealed class GridPlanner(
RepeatSource? repeat RepeatSource? repeat
) => ) =>
new( new(
target.Layer, Target.Layer,
target.Weekday, Target.Weekday,
GridCoverage.AtOffset(offset, run.DayStart), GridCoverage.AtOffset(Offset, Run.DayStart),
duration, duration,
title, title,
band.Daypart, Band.Daypart,
repeat is null ? band.SlotKind : SlotKind.Repeat, repeat is null ? Band.SlotKind : SlotKind.Repeat,
groupId, groupId,
groupId is null ? null : title, groupId is null ? null : title,
blockMode, blockMode,
blockValue, blockValue,
strategy, strategy,
repeat, repeat,
anchor Anchor
); );
} }
/// <summary>Почему полосу закрыл повтор — текст для предпросмотра.</summary>
private static string RepeatReason(GridBand band, GroupCandidate? group) =>
group is null
? $"Для полосы «{band.Title}» подходящей группы нет — её закрывает повтор вечернего блока."
: $"Полосу «{band.Title}» закрывает повтор вечернего блока: свежего контента на неделю "
+ "не хватает.";
/// <summary>Почему полоса осталась без свежего контента — текст для предпросмотра.</summary> /// <summary>Почему полоса осталась без свежего контента — текст для предпросмотра.</summary>
private static string Unfillable(GridBand band, GroupCandidate? group) => private static string Unfillable(GridBand band, GroupCandidate? group) =>
group is null group is null
@@ -77,15 +77,17 @@ export function parseEpisodeName(name: string, opts?: ParseOptions): ParsedEpiso
// и движок перебирает варианты впустую (у Sonar это «super-linear backtracking»). // и движок перебирает варианты впустую (у Sonar это «super-linear backtracking»).
const SEASON_RANGES = [ const SEASON_RANGES = [
/(?:s|season|сезоны?)[\s._-]*\d{1,2}[\s._]*[-][\s._]*(?:s|season)?\d{1,2}/i, /(?:s|season|сезоны?)[\s._-]*\d{1,2}[\s._]*[-][\s._]*(?:s|season)?\d{1,2}/i,
/\d{1,2}[\s._]*[-][\s._]*\d{1,2}[\s._-]*сезон/i, /\d{1,2}[\s._]*[-][\s._]*\d{1,2}[\s._-]*(?:season|сезон)/i,
] ]
/** Сезон в имени одного сегмента пути: «S02», «Season 2», «Сезон 2», «2 сезон». */ /** Сезон в имени одного сегмента пути: «S02», «Season 2», «Сезон 2», «2 сезон», «3 Season». */
const SEASON_PATTERNS = [ const SEASON_PATTERNS = [
// S02 — не часть более длинного числа (год «S2007» сезоном не считаем). // S02 — не часть более длинного числа (год «S2007» сезоном не считаем).
/(?:^|[^a-z0-9])s[\s._-]*(\d{1,2})(?!\d)/i, /(?:^|[^a-z0-9])s[\s._-]*(\d{1,2})(?!\d)/i,
/(?:season|сезон)[\s._-]*(\d{1,2})(?!\d)/i, /(?:season|сезон)[\s._-]*(\d{1,2})(?!\d)/i,
/(\d{1,2})[\s._-]*сезон/i, // Номер перед словом — «3 Season», «2 сезон». Ограничение слева не даёт принять за сезон
// хвост года: в «2007 Season» это не третий сезон, а сезон 2007 года.
/(?:^|[^\d])(\d{1,2})[\s._-]*(?:season|сезон)/i,
] ]
/** /**