using Nonemm.Core; namespace Nonemm.Contests.Tests; /// Contacts an hour, as the network status window and the score window show /// them. public class RatesTests { private static readonly DateTime Now = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc); private static readonly TimeSpan Off = TimeSpan.FromMinutes(30); private static Qso MinutesAgo(int minutes) => TestLog.Contact("OM3XYZ") with { TimestampUtc = Now.AddMinutes(-minutes) }; /// Ten contacts spread over the last ten minutes: the oldest of them was /// ten minutes ago, so the rate is ten in a sixth of an hour. [Fact] public void TheRateIsTheContactsOverTheTimeTheyTook() { List log = [.. Enumerable.Range(1, 10).Select(MinutesAgo)]; Assert.Equal(60, Rates.PerHour(log, 10, Now, Off)); } /// Only the last `count` contacts count, whatever else is in the log. [Fact] public void OlderContactsThanTheWindowAreLeftOut() { List log = [.. Enumerable.Range(1, 40).Select(MinutesAgo)]; Assert.Equal(60, Rates.PerHour(log, 10, Now, Off)); } /// A gap as long as the contest's minimum off time ends the count. [Fact] public void CountingStopsAtATimeOffGap() { List log = [MinutesAgo(1), MinutesAgo(2), MinutesAgo(90), MinutesAgo(91)]; // two contacts in the last two minutes, floored at a minute Assert.Equal(60, Rates.PerHour(log, 10, Now, Off)); } /// Under ten contacts the span counts as at least a minute, so a burst does /// not read as thousands an hour. [Fact] public void AShortBurstIsFlooredAtAMinute() { List log = [MinutesAgo(0), MinutesAgo(0), MinutesAgo(0)]; Assert.Equal(180, Rates.PerHour(log, 10, Now, Off)); } [Fact] public void AnEmptyLogIsNoRate() => Assert.Equal(0, Rates.PerHour([], 10, Now, Off)); /// A station that has worked nobody since the gap has no rate, even with a /// full log behind it. [Fact] public void NothingSinceTheGapIsNoRate() { List log = [.. Enumerable.Range(60, 20).Select(MinutesAgo)]; Assert.Equal(0, Rates.PerHour(log, 10, Now, Off)); } }