Implement interval intersection and subtraction methods in GridCoverage, update GridPlan and related DTOs for PlanNote integration
ci / build-backend (push) Successful in 2m45s
ci / build-frontend (push) Successful in 1m12s
ci / tests (push) Successful in 2m52s
ci / sonar (push) Successful in 8m13s

Added new methods for intersecting and subtracting time intervals in the GridCoverage class to enhance scheduling capabilities. Updated the GridPlan and GridPlanDto classes to replace string notes with a structured PlanNote type, improving note management. Enhanced the GridPlanner class to utilize these new features, ensuring better handling of scheduling conflicts and notes during grid generation. Adjusted related tests to validate the new functionality.
This commit is contained in:
Leonid Pershin
2026-07-27 11:36:06 +03:00
parent 062f4aa200
commit 96ff2cc7ab
12 changed files with 742 additions and 176 deletions
@@ -147,8 +147,20 @@ public class GenerateGridTests
// Пустая сетка свободна всю неделю, и профиль обязан закрыть её целиком.
Assert.Equal(7 * GridCoverage.MinutesInDay, plan.FreeMinutes);
Assert.Equal(plan.FreeMinutes, plan.CoveredMinutes);
// Свободно во все семь дней — значит слоты идут «каждый день», а не семью копиями.
Assert.All(plan.Slots, slot => Assert.Null(slot.Weekday));
// Полосы без ротации свободны во все семь дней — они идут «каждый день», а не семью копиями.
Assert.All(
plan.Slots.Where(s => s.Start < new TimeOnly(20, 0)),
slot => Assert.Null(slot.Weekday)
);
// Прайм (с 20:00) чередуется по дням недели, поэтому у него дни проставлены явно.
Assert.All(
plan.Slots.Where(s => s.Start == new TimeOnly(20, 0)),
slot => Assert.NotNull(slot.Weekday)
);
Assert.Equal(
5,
plan.Slots.Count(s => s.Start == new TimeOnly(20, 0) && s.Weekday is >= 1 and <= 5)
);
}
[Fact]
@@ -215,8 +227,13 @@ public class GenerateGridTests
var plan = await PlanAsync(fixture, channelId);
Assert.Contains(plan.Notes, note => note.Contains("Ночной блок 18+"));
Assert.True(plan.CoveredMinutes < plan.FreeMinutes);
// Взрослого контента нет — ночной блок закрывается повтором вечернего, а не остаётся дырой.
Assert.Contains(plan.Notes, note => note.Text.Contains("Ночной блок 18+"));
Assert.Contains(
plan.Slots,
s => s.SlotKind == SlotKind.Repeat && s.Title == "Ночной блок 18+"
);
Assert.Equal(plan.FreeMinutes, plan.CoveredMinutes);
}
[Fact]
@@ -304,6 +321,109 @@ public class GenerateGridTests
Assert.Contains(plan.Slots, s => s.Layer == GridPlanLayer.Main && s.Weekday is null);
}
[Fact]
public async Task SlotLength_FollowsContent_NotProfileConstant()
{
// Серии по 25 минут: двухчасовая полоса должна разложиться на блоки, кратные серии,
// а не отдать десятки минут фону.
var fixture = new TestDb();
var series = Playable("Сериал", ShowKind.Series, 60, 25);
var (channelId, _) = await SeedChannelAsync(
fixture,
[series.Show],
series.Assets,
[GroupOf("Сериалы", series.Show.Id)]
);
var plan = await PlanAsync(fixture, channelId);
var morning = plan.Slots.Where(s => s.Daypart == Daypart.Morning && s.GroupId is not null);
Assert.All(
morning,
slot =>
{
// Слот вмещает ровно столько серий, сколько в нём заявлено, и с точностью до пяти минут.
Assert.Equal(slot.DurationMinutes, slot.BlockValue * 25);
Assert.Equal(SlotBlockMode.Count, slot.BlockMode);
}
);
}
[Fact]
public async Task ExhaustedGroup_IsReplacedByRepeatOfPrime()
{
// Контента на неделю заведомо мало: сетка должна закрыть день повторами, а не крутить
// одно и то же по третьему кругу молча.
var fixture = new TestDb();
var tiny = Playable("Мало", ShowKind.Series, 3, 25);
var (channelId, _) = await SeedChannelAsync(
fixture,
[tiny.Show],
tiny.Assets,
[GroupOf("Мало", tiny.Show.Id)]
);
var plan = await PlanAsync(fixture, channelId);
Assert.Contains(plan.Slots, s => s.SlotKind == SlotKind.Repeat);
Assert.Contains(plan.Notes, n => n.Text.Contains("повтор вечернего блока"));
// Повтор ссылается на вечерний блок прошлых суток — с начала прайма профиля.
var repeat = plan.Slots.First(s => s.RepeatSource is not null);
Assert.Equal(1, repeat.RepeatSource!.DaysAgo);
Assert.Equal(new TimeOnly(18, 0), repeat.RepeatSource.Time);
}
[Fact]
public async Task PrimeRotatesAcrossWeekdays()
{
var fixture = new TestDb();
var first = Films(6, 100);
var second = Playable("Сериал", ShowKind.Series, 40, 25);
var (channelId, _) = await SeedChannelAsync(
fixture,
[second.Show, .. first.Shows],
[.. second.Assets, .. first.Assets],
[
GroupOf("Кино А", first.Shows[0].Id, first.Shows[1].Id, first.Shows[2].Id),
GroupOf("Кино Б", first.Shows[3].Id, first.Shows[4].Id, first.Shows[5].Id),
GroupOf("Сериалы", second.Show.Id),
]
);
var plan = await PlanAsync(
fixture,
channelId,
GridProfileKind.Mixed,
GridGenerationMode.Rebuild
);
var prime = plan
.Slots.Where(s => s.Start == new TimeOnly(20, 0) && s.Weekday is >= 1 and <= 5)
.ToList();
Assert.Equal(5, prime.Count);
// Всю неделю один и тот же фильм в прайме — это не сетка, а один повторяющийся день.
Assert.True(prime.Select(s => s.GroupName).Distinct().Count() > 1);
}
[Fact]
public async Task WeeklyShortage_IsReportedWithNumbers()
{
var fixture = new TestDb();
var series = Playable("Сериал", ShowKind.Series, 4, 25);
var (channelId, _) = await SeedChannelAsync(
fixture,
[series.Show],
series.Assets,
[GroupOf("Сериалы", series.Show.Id)]
);
var plan = await PlanAsync(fixture, channelId);
// Проверки считаются по тому же бюджету, по которому раскладывалось, — до нажатия кнопки.
Assert.Contains(plan.Notes, n => n.Text.Contains("выходах в неделю"));
}
[Fact]
public async Task Fails_WhenNothingToPlanFrom()
{