using Nonemm.Core; namespace Nonemm.Contests.Tests; /// The `…BandChange…` settings: what counts as a change, and over what stretch /// of time the changes are counted. public class BandChangeRulesTests { private static readonly DateTime Start = new(2026, 5, 30, 12, 0, 0, DateTimeKind.Utc); private static Qso At(int minutes, double kilohertz, Mode? mode = null, int seconds = 0) => TestLog.Contact("OM3XYZ", kilohertz, mode: mode) with { TimestampUtc = Start.AddMinutes(minutes).AddSeconds(seconds), }; /// N1MM's fallback, which is the ten-minute rule: a multi-operator entry /// with one transmitter has to stay ten minutes on a band, and nothing caps /// how many times it may move. [Theory] [InlineData("MULTI-ONE", "ONE", 10)] [InlineData("MULTI-OP", "ONE", 10)] [InlineData("MULTI-TWO", "TWO", 10)] [InlineData("MULTI-OP", "TWO", 10)] [InlineData("MULTI-OP", "UNLIMITED", 0)] [InlineData("SINGLE-OP", "ONE", 0)] public void TheCategoryAloneSaysHowLongAStationStaysOnABand( string operatorCategory, string transmitters, int minutes) { BandChangeRules rules = BandChangeRules.ForCategory(new ContestEntry { OperatorCategory = operatorCategory, TransmitterCategory = transmitters, }); Assert.Equal(TimeSpan.FromMinutes(minutes), rules.MinimumStay); Assert.False(rules.IsCounted); } /// A contest that says nothing about band changes still gets the rule for /// the category, which is what N1MM does. [Fact] public void AContestThatSaysNothingStillGetsTheTenMinuteRule() { Contest contest = new Rules.CqWorldWide(ModeCategory.Cw); BandChangeRules rules = contest.BandChangesFor(new ContestEntry { OperatorCategory = "MULTI-ONE", TransmitterCategory = "ONE", }); Assert.True(rules.HasStayTimer); Assert.Equal(TimeSpan.FromMinutes(10), rules.MinimumStay); } [Fact] public void ASingleOperatorEntryHasNoBandChangeRule() { Contest contest = new Rules.CqWorldWide(ModeCategory.Cw); Assert.Equal( BandChangeRules.None, contest.BandChangesFor(new ContestEntry { OperatorCategory = "SINGLE-OP" })); } [Fact] public void EachMoveToAnotherBandIsOneChange() => Assert.Equal( 2, new BandChangeRules(8).CountedIn( [At(0, 14_025), At(5, 7_040), At(10, 7_045), At(15, 14_030)], Start.AddMinutes(20))); /// The count is per clock hour unless the file says otherwise, so a change /// made in the hour before does not count against this one. [Fact] public void ChangesMadeInAnEarlierHourAreNotCounted() => Assert.Equal( 1, new BandChangeRules(8).CountedIn( [At(0, 14_025), At(5, 7_040), At(65, 14_025)], Start.AddMinutes(70))); [Fact] public void CountingPerContestKeepsEveryChange() => Assert.Equal( 2, new BandChangeRules(8, PerContest: true).CountedIn( [At(0, 14_025), At(5, 7_040), At(65, 14_025)], Start.AddMinutes(70))); /// `CountBandChangesPerPeriod` counts the hour up to now rather than the /// clock hour. [Fact] public void CountingPerPeriodLooksBackAnHour() => Assert.Equal( 1, new BandChangeRules(8, PerPeriod: true).CountedIn( [At(0, 14_025), At(5, 7_040), At(65, 14_025)], Start.AddMinutes(70))); /// `CountBandOrModeChange` counts a move to another mode on the same band. [Fact] public void ModeChangesCountWhenTheContestSaysSo() => Assert.Equal( 1, new BandChangeRules(8, CountsModeChanges: true).CountedIn( [At(0, 14_025), At(5, 14_250, Modes.Usb)], Start.AddMinutes(10))); /// The stay runs from the minute after the contact that changed band, so a /// change at 12:05:40 with a ten-minute stay has five minutes left at /// 12:11. N1MM starts it the same way. [Fact] public void TheStayCountsDownFromTheMinuteAfterTheChange() { BandChangeRules rules = new(8, MinimumStay: TimeSpan.FromMinutes(10)); Assert.Equal( TimeSpan.FromMinutes(5), rules.StayLeft([At(0, 14_025), At(5, 7_040, seconds: 40)], Start.AddMinutes(11))); } [Fact] public void TheStayIsUpOnceTheTimeHasPassed() => Assert.Equal( TimeSpan.Zero, new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).StayLeft( [At(0, 14_025), At(5, 7_040)], Start.AddMinutes(30))); /// A log that has stayed on one band has nothing to count down. [Fact] public void NothingCountsDownUntilTheStationChangesBand() => Assert.Equal( TimeSpan.Zero, new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).StayLeft( [At(0, 14_025), At(5, 14_030)], Start.AddMinutes(6))); [Fact] public void AContestThatAsksForNoStayHasNoTimer() { Assert.False(new BandChangeRules(8).HasStayTimer); Assert.True(new BandChangeRules(8, MinimumStay: TimeSpan.FromMinutes(10)).HasStayTimer); } [Fact] public void AContestWithNoLimitCountsNothing() => Assert.False(BandChangeRules.None.IsCounted); }