CQ WW lists LOCATION as required, and the export never wrote it. It now carries the ARRL section, state or province, or DX. CQ WW M/2 has no stay on a band: it may make eight band changes in a clock hour. It was getting the ten-minute stay that M/S has. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
169 lines
5.8 KiB
C#
169 lines
5.8 KiB
C#
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 CqWwMultiTwoMayMakeEightBandChangesAnHourAndHasNoStay()
|
|
{
|
|
Contest contest = new Rules.CqWorldWide(ModeCategory.Phone);
|
|
|
|
BandChangeRules rules = contest.BandChangesFor(new ContestEntry
|
|
{
|
|
OperatorCategory = "MULTI-OP",
|
|
TransmitterCategory = "TWO",
|
|
});
|
|
|
|
Assert.Equal(8, rules.Max);
|
|
Assert.False(rules.HasStayTimer);
|
|
}
|
|
|
|
[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);
|
|
}
|