Files
TeleWave/backend/src/TeleWave.Application/Programming/Templates/PlanningRules.cs
T
Leonid PershinandClaude Opus 5 0442056367 Normalize line endings to LF via .gitattributes
Репозиторий хранил фронтенд в CRLF, а часть бэкенда — вперемешку, хотя CI и Docker-сборка
работают под Linux. Прибиваем LF атрибутом `* text=auto eol=lf` и разово нормализуем дерево,
чтобы форматтеры не переписывали файлы целиком на каждом прогоне.

Коммит чисто механический: изменений содержимого нет, только концы строк.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 01:37:33 +03:00

76 lines
3.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json;
using System.Text.Json.Serialization;
using TeleWave.Domain.Library;
namespace TeleWave.Application.Programming.Templates;
/// <summary>
/// Окно детского времени: с <paramref name="From"/> до <paramref name="To"/> во времени канала
/// в эфир идёт только контент не строже <paramref name="MaxAudience"/>. Окно может переходить
/// через полночь (23:00–06:00) — тогда границы сравниваются в обратную сторону.
/// </summary>
public sealed record AudienceWindow(TimeOnly From, TimeOnly To, ShowAudience MaxAudience)
{
public bool Contains(TimeOnly moment) =>
From <= To ? moment >= From && moment < To : moment >= From || moment < To;
}
/// <summary>
/// Правила отбора кандидатов на уровне канала (см. 3.8). Это жёсткие фильтры: они отсекают
/// недопустимое до жребия, поэтому не требуют пересборки и не ломают воспроизводимость.
///
/// Область действия — весь канал: дейпарты сюда не заведены намеренно, окна детского времени
/// и так задаются временем, а потолок повторов осмыслен только целиком по каналу.
/// </summary>
public sealed record PlanningRules(
IReadOnlyList<AudienceWindow>? MaxAudienceByTime = null,
/// <summary>Не чаще <c>Max</c> раз за <c>WindowDays</c> суток (null — без ограничения).</summary>
RepeatLimitRule? MaxRepeatsInWindow = null,
// ── Пост-проверки: считаются по готовой ленте, дают предупреждения и ничего не переигрывают. ──
/// <summary>Потолок врезок в часе, минуты (null — не проверять).</summary>
int? MaxBreakMinutesPerHour = null,
/// <summary>Потолок доли одного жанра за вещательные сутки, проценты (null — не проверять).</summary>
int? MaxGenreSharePercent = null,
/// <summary>Потолок доли эфира, отданной фону, проценты (null — не проверять).</summary>
int? MaxFallbackSharePercent = null
)
{
private static readonly JsonSerializerOptions Options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
Converters = { new JsonStringEnumConverter() },
};
/// <summary>Возрастной потолок в указанный момент времени канала (null — без ограничения).</summary>
public ShowAudience? AudienceAt(TimeOnly moment)
{
if (MaxAudienceByTime is not { Count: > 0 } windows)
return null;
// Пересекающиеся окна разрешаются в пользу строгого: детское время не должно
// отменяться более широким окном, случайно наложенным сверху.
var applicable = windows.Where(w => w.Contains(moment)).Select(w => w.MaxAudience).ToList();
return applicable.Count > 0 ? applicable.Min() : null;
}
public string ToJson() => JsonSerializer.Serialize(this, Options);
public static PlanningRules? FromJson(string? json)
{
if (string.IsNullOrWhiteSpace(json))
return null;
try
{
return JsonSerializer.Deserialize<PlanningRules>(json, Options);
}
catch (JsonException)
{
return null;
}
}
}
public sealed record RepeatLimitRule(int WindowDays, int Max);