using System.Globalization; namespace SVSim.EmulatedEntrypoint.Services; /// /// JST-anchored period bucketing for ViewerEventCounters. Day/week/month boundaries are /// 02:00 JST (matching the real-game daily reset). Pure functions, no dependencies. /// public static class JstPeriod { private static readonly TimeSpan Jst = TimeSpan.FromHours(9); private const string DayPrefix = "day:"; private const string WeekPrefix = "week:"; private const string MonthPrefix = "month:"; public const string AllTime = "all-time"; /// /// Converts the given instant to a JST-anchored "effective date" by: /// 1. Shifting to JST (+09:00) /// 2. Subtracting 2 hours so anything before 02:00 JST belongs to the previous day /// private static DateTime EffectiveJstDate(DateTimeOffset utcOrAny) { var jst = utcOrAny.ToOffset(Jst); return jst.AddHours(-2).Date; } public static string DayKey(DateTimeOffset when) { var d = EffectiveJstDate(when); return $"{DayPrefix}{d:yyyy-MM-dd}"; } public static string WeekKey(DateTimeOffset when) { var d = EffectiveJstDate(when); var iso = ISOWeek.GetWeekOfYear(d); var year = ISOWeek.GetYear(d); return $"{WeekPrefix}{year:D4}-W{iso:D2}"; } public static string MonthKey(DateTimeOffset when) { var d = EffectiveJstDate(when); return $"{MonthPrefix}{d:yyyy-MM}"; } /// Returns [day, week, month, all-time] keys for the given instant. public static IReadOnlyList AllPeriods(DateTimeOffset when) => new[] { DayKey(when), WeekKey(when), MonthKey(when), AllTime, }; }