diff --git a/README.md b/README.md index adffd7b..af198f0 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ scorer: red for a dupe, green for a new multiplier, blue for points. | | | |---|---| -| Contests | CQ WW (CW, SSB, RTTY), CQ WPX (CW, SSB, RTTY), WAE (CW, SSB, RTTY), ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP, OK/OM DX, YOTA, IOTA, EU DX Contest, CQ 160, ARRL 10M, REF, Ukrainian DX, Russian DX RTTY, RDAC, YO DX HF, Oceania DX, All Asian, SA 10, SARTG RTTY, BARTG Sprint, Mexico RTTY, IARU Region 1 Field Day, general logging, and user-defined `.udc` contests | +| Contests | CQ WW (CW, SSB, RTTY), CQ WPX (CW, SSB, RTTY), WAE (CW, SSB, RTTY), ARRL DX, IARU HF, Sweepstakes, RTTY Roundup, NAQP, OK/OM DX (CW), YOTA, IOTA, EU DX Contest, CQ 160, ARRL 10M, REF, Ukrainian DX, Russian DX RTTY, RDAC, YO DX HF, Oceania DX, All Asian, SARTG RTTY, BARTG Sprint, Mexico RTTY, IARU Region 1 Field Day, general logging, and user-defined `.udc` contests | | Log | N1MM `.s3db`, Cabrillo 3.0 out, ADIF in and out | | While typing | dupe check, multiplier check, points, country and zone from the country file, exchange filled from a call history file | | Windows | entry, log, check, bandmap, available mults and Qs, score summary, telnet | diff --git a/docs/unfinished.md b/docs/unfinished.md index e0fdd7f..38c9927 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -92,7 +92,7 @@ which is too much work per spot for the little it would add. the contest rules score it, but there is no digital window: no decoding, no transmitting, no interface to fldigi, MMTTY or similar. -**Contest coverage.** Twenty-six families are built in, plus whatever `.udc` files +**Contest coverage.** Twenty-five families are built in, plus whatever `.udc` files are in the user-defined folder. N1MM ships well over a hundred. Opening a log from a contest that is in neither place fails with the contest name, which is the right answer but it is still a wall. @@ -136,10 +136,16 @@ What still differs, and why: multiplier, which is not what N1MM's own class works out, so the log looks like it was never scored. The rules here follow the class: 5 points for a portable station, 2 for one in Region 1 at home and 3 from outside it. -- **All Asian and SA 10** have no class in N1MM — it logs them from a `.udc` - file — so the points and multipliers are written from the published rules and - from the station's log. Their Cabrillo names have not been checked against a - sponsor's robot. +- **SA 10 and OK/OM DX on SSB** are not written in code at all: both are + published as `.udc` files, and the files score the station's logs to the + contact. N1MM's OK/OM DX class is the CW running only and says so; the SSB + running has different rules and its own pair of files, one for each side of + the contest. +- **A `.udc` section multiplier** is counted for any exchange that is not all + digits. N1MM checks it against the section list the file names in + `MultWindowType`, and those lists are not held here, so a US state sent into + an OK/OM DX log counts here and not in N1MM. Three contacts across the two + logs. - **The BARTG Sprint** stores the time of the contact in the exchange column, which N1MM fills in by itself. Nothing here fills it in yet, so a log written here carries the serial number alone. diff --git a/src/Nonemm.Contests/ContestRegistry.cs b/src/Nonemm.Contests/ContestRegistry.cs index d8891f3..9853e2e 100644 --- a/src/Nonemm.Contests/ContestRegistry.cs +++ b/src/Nonemm.Contests/ContestRegistry.cs @@ -18,7 +18,7 @@ public sealed class ContestRegistry new("WAE", "Worked All Europe DX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new Wae(m)), new("ARRLRTTY", "ARRL RTTY Roundup", [ModeCategory.Digital], _ => new RttyRoundup()), new("NAQP", "North American QSO Party", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new NorthAmericanQsoParty(m)), - new("OKOMDX", "Czech and Slovak Republics DX", [ModeCategory.Cw, ModeCategory.Phone], m => new OkOmDx(m)), + new("OKOMDX", "Czech and Slovak Republics DX", [ModeCategory.Cw], _ => new OkOmDx()), new("YOTA", "YOTA Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new Yota()), new("IOTA", "RSGB Islands On The Air", [ModeCategory.Cw, ModeCategory.Phone], _ => new Iota()), new("EUDXC", "EU DX Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new EuDxContest()), @@ -35,7 +35,6 @@ public sealed class ContestRegistry new("FDREG1", "IARU Region 1 Field Day", [ModeCategory.Cw, ModeCategory.Phone], _ => new FieldDayRegionOne()), new("SARTGRTTY", "SARTG World Wide RTTY", [ModeCategory.Digital], _ => new SartgRtty()), new("ALLASIA", "All Asian DX", [ModeCategory.Cw, ModeCategory.Phone], m => new AllAsian(m)), - new("SA10", "SA 10 Metre Contest", [ModeCategory.Cw, ModeCategory.Phone], _ => new SouthAmerica10()), new("DX", "General logging", [], _ => new GeneralLogging()), ]; @@ -60,15 +59,18 @@ public sealed class ContestRegistry return new ContestRegistry(byName); } - /// Reads every `.udc` file in the folder. A file that will not parse is - /// reported rather than silently left out. + /// Reads every `.udc` file in the folder. Published files come with the + /// extension in either case, and a file that will not parse is reported + /// rather than silently left out. public static ContestRegistry FromFolder(string folder, out IReadOnlyList problems) { List files = []; List failures = []; if (Directory.Exists(folder)) { - foreach (string path in Directory.EnumerateFiles(folder, "*.udc")) + IEnumerable found = Directory.EnumerateFiles(folder) + .Where(f => Path.GetExtension(f).Equals(".udc", StringComparison.OrdinalIgnoreCase)); + foreach (string path in found) { try { diff --git a/src/Nonemm.Contests/N1mmContestNames.cs b/src/Nonemm.Contests/N1mmContestNames.cs index 151f5be..6b1f1ea 100644 --- a/src/Nonemm.Contests/N1mmContestNames.cs +++ b/src/Nonemm.Contests/N1mmContestNames.cs @@ -6,11 +6,7 @@ namespace Nonemm.Contests; /// never plain `CQWW`. Our registry is keyed by the family, so a log written by /// N1MM has to have its contest name turned back into a family and a mode /// before the rules can be built. -/// -/// The `.udc` files people use for OK/OM DX name it per mode as well, and one -/// file per side of the contest — `OKOMDXS_DX` is the one a station outside OK -/// and OM runs. Both sides are the same rules here, so both names resolve to -/// the same contest. + public static class N1mmContestNames { private static readonly Dictionary Known = @@ -32,10 +28,6 @@ public static class N1mmContestNames ["NAQPCW"] = ("NAQP", ModeCategory.Cw), ["NAQPSSB"] = ("NAQP", ModeCategory.Phone), ["NAQPRTTY"] = ("NAQP", ModeCategory.Digital), - ["OKOMDXC"] = ("OKOMDX", ModeCategory.Cw), - ["OKOMDXS"] = ("OKOMDX", ModeCategory.Phone), - ["OKOMDXC_DX"] = ("OKOMDX", ModeCategory.Cw), - ["OKOMDXS_DX"] = ("OKOMDX", ModeCategory.Phone), ["CQ160CW"] = ("CQ160", ModeCategory.Cw), ["CQ160SSB"] = ("CQ160", ModeCategory.Phone), ["EU_DXC"] = ("EUDXC", ModeCategory.Cw), @@ -46,7 +38,6 @@ public static class N1mmContestNames ["BARTGSRTTY"] = ("BARTGRTTYS", ModeCategory.Digital), ["ALLASIACW"] = ("ALLASIA", ModeCategory.Cw), ["ALLASIASSB"] = ("ALLASIA", ModeCategory.Phone), - ["SA10_DX"] = ("SA10", ModeCategory.Phone), }; public static bool TryResolve(string name, out string family, out ModeCategory mode) diff --git a/src/Nonemm.Contests/Rules/AllAsian.cs b/src/Nonemm.Contests/Rules/AllAsian.cs index 0fa4f08..62977e7 100644 --- a/src/Nonemm.Contests/Rules/AllAsian.cs +++ b/src/Nonemm.Contests/Rules/AllAsian.cs @@ -4,7 +4,8 @@ namespace Nonemm.Contests.Rules; /// The JARL All Asian DX contest. Only contacts with Asia count for a station /// outside it. The exchange is the operator's age, the points go by band, and -/// each prefix worked counts once per band. +/// each prefix worked counts once per band. An Asian station scores three +/// times what a station outside Asia scores for the same contact. public sealed class AllAsian : Contest { private readonly ModeCategory mode; @@ -15,7 +16,7 @@ public sealed class AllAsian : Contest public string DisplayName => $"All Asian DX {ModeLabel()}"; - public string CabrilloName => mode == ModeCategory.Cw ? "AA-CW" : "AA-SSB"; + public string CabrilloName => mode == ModeCategory.Cw ? "AADX-CW" : "AADX-SSB"; public IReadOnlyList ExchangeFieldsFor(StationInfo me) => [ @@ -33,7 +34,14 @@ public sealed class AllAsian : Contest public string SentExchangeFor(StationInfo me) => ""; - public int PointsFor(QsoContext qso) => IsWorkable(qso) ? PointsOn(qso.Band) : 0; + public int PointsFor(QsoContext qso) + { + if (!IsWorkable(qso)) + { + return 0; + } + return PointsOn(qso.Band) * (qso.Me.Continent == "AS" ? 3 : 1); + } public IReadOnlyList MultipliersFor(QsoContext qso) => IsWorkable(qso) && qso.Qso.Call.WpxPrefix() is { } prefix @@ -60,10 +68,11 @@ public sealed class AllAsian : Contest private static bool IsWorkable(QsoContext qso) => qso.Continent == "AS" || qso.Me.Continent == "AS"; + /// 160 metres is worth 3, 80 and 10 metres 2, and every other band 1. private static int PointsOn(Band? band) => band?.Name switch { - "160M" or "80M" => 3, - "40M" => 2, + "160M" => 3, + "80M" or "10M" => 2, null => 0, _ => 1, }; diff --git a/src/Nonemm.Contests/Rules/OkOmDx.cs b/src/Nonemm.Contests/Rules/OkOmDx.cs index e6f29ef..c1d8da3 100644 --- a/src/Nonemm.Contests/Rules/OkOmDx.cs +++ b/src/Nonemm.Contests/Rules/OkOmDx.cs @@ -3,19 +3,19 @@ using Nonemm.Core.Country; namespace Nonemm.Contests.Rules; -/// OK/OM DX. An OK or OM station sends a county code and everyone else a +/// OK/OM DX, CW. An OK or OM station sends a county code and everyone else a /// serial number, and which side the operator is on decides what a contact -/// scores. +/// scores and what it counts for. +/// +/// The SSB running of this contest has its own rules and its own pair of `.udc` +/// files, `OKOMDXS` and `OKOMDXS_DX`, one for each side. N1MM says not to use +/// this contest for it, and neither should we. /// /// The received exchange lands in the section column for an OK or OM station /// and in the exchange column for everyone else, which is where N1MM keeps /// each of them. public sealed class OkOmDx : Contest { - private readonly ModeCategory mode; - - public OkOmDx(ModeCategory mode) => this.mode = mode; - public string Name => "OKOMDX"; public string DisplayName => "Czech and Slovak Republics DX"; @@ -39,52 +39,52 @@ public sealed class OkOmDx : Contest _ => false, }; - public IReadOnlyList MultiplierNames => ["Countries", "Counties"]; + public IReadOnlyList MultiplierNames => ["Mults"]; public DupeScope DupeScope => DupeScope.PerBand; public bool HasSerialNumbers => true; - public IReadOnlyList Modes => [mode]; + public IReadOnlyList Modes => [ModeCategory.Cw]; public string SentExchangeFor(StationInfo me) => IsOkOm(me.CountryPrefix) ? me.County : "001"; /// A maritime mobile is 5 points to either side. An OK or OM operator gets - /// 2 points at home, 3 elsewhere on the continent and 5 from another — - /// Czechia and Slovakia are two countries, so they are 3 points to each - /// other. Everyone else gets 10 points for an OK or OM station, 1 at home, - /// 3 on their own continent and 5 from another. + /// 2 points for another OK or OM station, 3 for the rest of Europe and 5 + /// for another continent. Everyone else gets 10 points for an OK or OM + /// station, 1 at home, 3 on their own continent and 5 from another. public int PointsFor(QsoContext qso) { if (qso.Qso.Call.IsMaritimeMobile) { return 5; } - if (!IsOkOm(qso.Me.CountryPrefix) && IsOkOm(qso.CountryPrefix)) + bool theirs = IsOkOm(qso.CountryPrefix); + if (IsOkOm(qso.Me.CountryPrefix)) + { + return theirs ? 2 : qso.Continent == "EU" ? 3 : 5; + } + if (theirs) { return 10; } - int home = IsOkOm(qso.Me.CountryPrefix) ? 2 : 1; - return qso.IsSameCountry ? home : qso.IsSameContinent ? 3 : 5; + return qso.IsSameCountry ? 1 : qso.IsSameContinent ? 3 : 5; } - /// Both sides count the same two, once per band each: every country, and - /// every OK or OM county. N1MM's own class counts prefixes for an OK or OM - /// operator, which was the rule before the counties came in. + /// Once per band: an OK or OM operator counts the prefixes of the stations + /// it works, and everyone else counts the OK and OM counties. public IReadOnlyList MultipliersFor(QsoContext qso) { string band = qso.Band?.Name ?? ""; - List found = []; - if (qso.CountryPrefix.Length > 0) + bool theirs = IsOkOm(qso.CountryPrefix); + if (IsOkOm(qso.Me.CountryPrefix)) { - found.Add(new Multiplier(1, qso.CountryPrefix, band)); + return theirs || qso.Qso.Call.WpxPrefix() is not { } prefix + ? [] + : [new Multiplier(1, prefix, band)]; } string county = County(qso.Qso); - if (IsOkOm(qso.CountryPrefix) && county.Length > 0) - { - found.Add(new Multiplier(2, county, band)); - } - return found; + return theirs && county.Length > 0 ? [new Multiplier(1, county, band)] : []; } public int TotalScore(ScoreTally tally, ContestEntry entry) => diff --git a/src/Nonemm.Contests/Rules/SouthAmerica10.cs b/src/Nonemm.Contests/Rules/SouthAmerica10.cs deleted file mode 100644 index e3beab1..0000000 --- a/src/Nonemm.Contests/Rules/SouthAmerica10.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Nonemm.Core; - -namespace Nonemm.Contests.Rules; - -/// The SA 10 metre contest. A South American station is worth 4 points and -/// everyone else 2. The exchange is a CQ zone, and both the prefixes and the -/// zones are multipliers. One band, so nothing is counted per band. -/// -/// N1MM logs this one through a `.udc` file, so the Cabrillo name here is the -/// contest name and has not been checked against the sponsor's robot. -public sealed class SouthAmerica10 : Contest -{ - public string Name => "SA10"; - - public string DisplayName => "SA 10 Metre Contest"; - - public string CabrilloName => "SA10"; - - public IReadOnlyList ExchangeFieldsFor(StationInfo me) => - [ - new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), - new ExchangeField("Zone", ExchangeSlot.Section, ExchangeFieldKind.CqZone), - ]; - - public IReadOnlyList MultiplierNames => ["Prefixes", "Zones"]; - - public DupeScope DupeScope => DupeScope.PerMode; - - public bool HasSerialNumbers => false; - - public IReadOnlyList Modes => [ModeCategory.Cw, ModeCategory.Phone]; - - public string SentExchangeFor(StationInfo me) => me.CqZone.ToString(); - - public int PointsFor(QsoContext qso) => qso.Continent == "SA" ? 4 : 2; - - public IReadOnlyList MultipliersFor(QsoContext qso) - { - List found = []; - if (qso.Qso.Call.WpxPrefix() is { } prefix) - { - found.Add(new Multiplier(1, prefix, "")); - } - string zone = qso.Qso.Section.Trim(); - if (zone.Length > 0) - { - found.Add(new Multiplier(2, zone, "")); - } - return found; - } - - public int TotalScore(ScoreTally tally, ContestEntry entry) => - tally.Points * tally.TotalMultipliers; - - public CabrilloExchange CabrilloExchange(Qso qso, StationInfo me, ContestEntry entry) => - new( - [ - new CabrilloField(me.Callsign, 13), - new CabrilloField(qso.SentReport, 3), - new CabrilloField(me.CqZone.ToString(), 3), - ], - [ - new CabrilloField(qso.Call.Text, 13), - new CabrilloField(qso.ReceivedReport, 3), - new CabrilloField(qso.Section, 3), - ]); -} diff --git a/src/Nonemm.Contests/Udc/UdcCondition.cs b/src/Nonemm.Contests/Udc/UdcCondition.cs index 6b75620..478bb7b 100644 --- a/src/Nonemm.Contests/Udc/UdcCondition.cs +++ b/src/Nonemm.Contests/Udc/UdcCondition.cs @@ -17,6 +17,7 @@ public static class UdcCondition { "" => false, "MYCOUNTRY" => qso.IsSameCountry, + "OTHERCOUNTRY" => qso.CountryPrefix.Length > 0 && !qso.IsSameCountry, "MYEXCHANGE" => Same(qso.Qso.Section, mySentExchange), "MYGRID" => SameGridField(qso.Qso.GridSquare, qso.Me.GridSquare), "MYCQZONE" => qso.CqZone > 0 && qso.CqZone == qso.Me.CqZone, diff --git a/src/Nonemm.Contests/Udc/UdcMultipliers.cs b/src/Nonemm.Contests/Udc/UdcMultipliers.cs index da4065c..93dc5e9 100644 --- a/src/Nonemm.Contests/Udc/UdcMultipliers.cs +++ b/src/Nonemm.Contests/Udc/UdcMultipliers.cs @@ -86,13 +86,13 @@ public sealed class UdcMultiplier private string? ValueFor(QsoContext qso) => source switch { - "COUNTRYPREFIX" => qso.CountryPrefix, + "COUNTRYPREFIX" or "COUNTRY" => qso.CountryPrefix, "EU_COUNTRY" or "AS_COUNTRY" or "NA_COUNTRY" or "SA_COUNTRY" or "AF_COUNTRY" or "OC_COUNTRY" => CountryOnContinent(qso, source[..2]), "WPXPREFIX" => qso.Qso.Call.WpxPrefix(), "2LPREFIX" => Truncate(qso.Qso.Call.Text, 2), "LASTLETTER" => LastLetter(qso.Qso.Call.Text), - "SECTION" or "SECT" => Upper(qso.Qso.Section), + "SECTION" or "SECT" => SectionName(qso.Qso.Section), "EXCHANGE" or "EXCH" => Upper(qso.Qso.Exchange1), "MISC" or "MISCTEXT" => Upper(qso.Qso.MiscText), "COMMENT" => Upper(qso.Qso.Comment), @@ -127,6 +127,18 @@ public sealed class UdcMultiplier private static string Upper(string text) => text.Trim().ToUpperInvariant(); + /// A section multiplier ignores a value that is all digits. N1MM checks the + /// received exchange against the section list the contest names, and a file + /// whose section column also carries the serial numbers the other side of + /// the contest sends — OK/OM DX SSB is one — would otherwise count every + /// serial number as a multiplier. The section lists themselves are not held + /// here. + private static string? SectionName(string text) + { + string section = Upper(text); + return section.Length > 0 && section.All(char.IsAsciiDigit) ? null : section; + } + private static string? LastLetter(string call) { string trimmed = call.Trim().ToUpperInvariant(); diff --git a/tests/Nonemm.Contests.Tests/N1mmContestNamesTests.cs b/tests/Nonemm.Contests.Tests/N1mmContestNamesTests.cs index 329ea36..d11b767 100644 --- a/tests/Nonemm.Contests.Tests/N1mmContestNamesTests.cs +++ b/tests/Nonemm.Contests.Tests/N1mmContestNamesTests.cs @@ -52,4 +52,21 @@ public class N1mmContestNamesTests [Fact] public void HasFindsAContestByTheNameN1mmWrote() => Assert.True(Registry().Has("CQWPXRTTY")); + + /// Published `.udc` files come with the extension in either case, and a + /// case-sensitive file system will not match one glob for both. + [Fact] + public void AUdcFileIsReadWhateverTheCaseOfItsExtension() + { + string folder = Directory.CreateTempSubdirectory().FullName; + File.WriteAllText(Path.Combine(folder, "Loud.UDC"), "[Contest]\nName=LOUD\n"); + File.WriteAllText(Path.Combine(folder, "quiet.udc"), "[Contest]\nName=QUIET\n"); + + ContestRegistry registry = ContestRegistry.FromFolder(folder, out IReadOnlyList problems); + + Assert.Empty(problems); + Assert.True(registry.Has("LOUD")); + Assert.True(registry.Has("QUIET")); + Directory.Delete(folder, recursive: true); + } } diff --git a/tests/Nonemm.Contests.Tests/OkOmDxTests.cs b/tests/Nonemm.Contests.Tests/OkOmDxTests.cs index 8c55a24..eee0225 100644 --- a/tests/Nonemm.Contests.Tests/OkOmDxTests.cs +++ b/tests/Nonemm.Contests.Tests/OkOmDxTests.cs @@ -6,20 +6,23 @@ namespace Nonemm.Contests.Tests; public class OkOmDxTests { - private static readonly Contest Ssb = new OkOmDx(ModeCategory.Phone); + private static readonly Contest Cw = new OkOmDx(); - private static ContestLog LogFor(StationInfo me) => new(Ssb, me, TestLog.CountryFile); + private static ContestLog LogFor(StationInfo me) => new(Cw, me, TestLog.CountryFile); private static Qso FromOkOm(string call, string county, double kilohertz = 14_025) => - TestLog.Contact(call, kilohertz, mode: Modes.Usb, section: county); + TestLog.Contact(call, kilohertz, section: county); private static Qso FromDx(string call, string number, double kilohertz = 14_025) => - TestLog.Contact(call, kilohertz, mode: Modes.Usb, exchange: number); + TestLog.Contact(call, kilohertz, exchange: number); + /// The CW running counts Czechia and Slovakia as one side of the contest: + /// two points either way. The SSB running splits them, and it has its own + /// pair of .udc files. [Theory] [InlineData("OM3XYZ", 2)] - [InlineData("OK1XYZ", 3)] - public void AtHomeCzechiaAndSlovakiaAreTwoCountries(string call, int expected) => + [InlineData("OK1XYZ", 2)] + public void EitherSideOfTheContestIsTwoPointsAtHome(string call, int expected) => Assert.Equal(expected, LogFor(TestLog.Slovakia).Judge(FromOkOm(call, "NOV")).Points); [Theory] @@ -36,40 +39,39 @@ public class OkOmDxTests public void AStationOutsideScoresTenForAnOkOmContact(string call, int expected) => Assert.Equal(expected, LogFor(TestLog.Germany).Judge(FromOkOm(call, "NOV")).Points); + /// An OK or OM operator counts the prefixes it works and nothing for the + /// other side of the contest. [Fact] - public void CountriesAndCountiesBothCountOncePerBand() + public void AnOkOmOperatorCountsPrefixes() { ContestLog log = LogFor(TestLog.Slovakia); - Assert.Equal( - [1, 2], - log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers.Select(m => m.Index).ToList()); - log.Add(FromOkOm("OM3XYZ", "NOV")); - Assert.Empty(log.Judge(FromOkOm("OM4XYZ", "NOV")).NewMultipliers); - Assert.Single(log.Judge(FromOkOm("OM4XYZ", "PAR")).NewMultipliers); + Assert.Equal("IK2", log.Judge(FromDx("IK2XYZ", "014")).NewMultipliers.Single().Value); + Assert.Empty(log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers); } + /// A station outside counts the counties instead. [Fact] - public void AStationOutsideOkOmBringsItsCountryAndNoCounty() => - Assert.Equal( - [1], - LogFor(TestLog.Slovakia).Judge(FromDx("IK2XYZ", "014")).NewMultipliers - .Select(m => m.Index) - .ToList()); + public void EveryoneElseCountsCounties() + { + ContestLog log = LogFor(TestLog.Germany); + Assert.Equal("NOV", log.Judge(FromOkOm("OM3XYZ", "NOV")).NewMultipliers.Single().Value); + Assert.Empty(log.Judge(FromDx("IK2XYZ", "014")).NewMultipliers); + } /// The entry window walks the county box for an OK or OM station and the /// serial box for everyone else. [Fact] public void OnlyOneOfTheTwoBoxesIsWalked() { - IReadOnlyList fields = Ssb.ExchangeFieldsFor(TestLog.Slovakia); + IReadOnlyList fields = Cw.ExchangeFieldsFor(TestLog.Slovakia); CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ"); CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ"); Assert.Equal( [ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1], - fields.Where(f => !Ssb.SkipsField(f, dx)).Select(f => f.Slot).ToList()); + fields.Where(f => !Cw.SkipsField(f, dx)).Select(f => f.Slot).ToList()); Assert.Equal( [ExchangeSlot.ReceivedReport, ExchangeSlot.Section], - fields.Where(f => !Ssb.SkipsField(f, okom)).Select(f => f.Slot).ToList()); + fields.Where(f => !Cw.SkipsField(f, okom)).Select(f => f.Slot).ToList()); } } diff --git a/tests/Nonemm.Contests.Tests/SmallContestTests.cs b/tests/Nonemm.Contests.Tests/SmallContestTests.cs index fb53666..8992c46 100644 --- a/tests/Nonemm.Contests.Tests/SmallContestTests.cs +++ b/tests/Nonemm.Contests.Tests/SmallContestTests.cs @@ -28,10 +28,12 @@ public class SmallContestTests .Judge(TestLog.Contact("K1ABC", mode: Modes.Rtty, section: "K1")) .NewMultipliers.Select(m => m.Index).ToList()); + /// 160 metres pays 3, 80 and 10 metres 2, and the rest 1. [Theory] [InlineData(14_150, 1)] - [InlineData(7_050, 2)] - [InlineData(3_650, 3)] + [InlineData(7_050, 1)] + [InlineData(3_650, 2)] + [InlineData(1_830, 3)] public void AllAsianScoresByBand(double kilohertz, int expected) => Assert.Equal( expected, @@ -47,16 +49,6 @@ public class SmallContestTests .Judge(TestLog.Contact("IK2XYZ", mode: Modes.Usb, exchange: "46")) .Points); - [Theory] - [InlineData("PY2XYZ", 4)] - [InlineData("IK2XYZ", 2)] - public void SouthAmericaTenPaysMoreForSouthAmerica(string call, int expected) => - Assert.Equal( - expected, - LogFor(new SouthAmerica10()) - .Judge(TestLog.Contact(call, 28_400, mode: Modes.Usb, section: "11")) - .Points); - [Theory] [InlineData("YO3XYZ", 8)] [InlineData("DL9XYZ", 1)] diff --git a/tests/Nonemm.Contests.Tests/UdcScoringTests.cs b/tests/Nonemm.Contests.Tests/UdcScoringTests.cs index eda388c..b7742b0 100644 --- a/tests/Nonemm.Contests.Tests/UdcScoringTests.cs +++ b/tests/Nonemm.Contests.Tests/UdcScoringTests.cs @@ -261,4 +261,35 @@ public class UdcScoringTests Assert.True(Contest("ZoneType=IARU").UsesItuZones); Assert.False(Contest("ZoneType=CQ").UsesItuZones); } + + /// The received exchange of a contest where one side sends a section and + /// the other a serial number lands in the same column, and only the + /// section counts. N1MM checks it against the contest's section list; the + /// lists are not held here, so an exchange that is all digits is taken for + /// a serial number. + [Fact] + public void ASerialNumberIsNoSectionMultiplier() + { + UserDefinedContest contest = Contest("MultSqlString=Section", "IsMultPer=1"); + Assert.Empty(LogFor(contest).Judge(TestLog.Contact("IK2XYZ", section: "058")).NewMultipliers); + Assert.Single(LogFor(contest).Judge(TestLog.Contact("OM3XYZ", section: "NOV")).NewMultipliers); + } + + /// `Country` is what a published file writes where the editor writes + /// `CountryPrefix`. + [Fact] + public void CountryIsTheSameSourceAsCountryPrefix() => + Assert.Equal( + "I", + LogFor(Contest("MultSqlString=Country", "IsMultPer=4")) + .Judge(TestLog.Contact("IK2XYZ")) + .NewMultipliers.Single().Value); + + [Fact] + public void OtherCountryIsEveryCountryButMine() + { + UserDefinedContest contest = Contest("PointsPerContact=MyCountry, 0, OtherCountry, 2"); + Assert.Equal(0, Points(contest, TestLog.Contact("DL9XYZ"))); + Assert.Equal(2, Points(contest, TestLog.Contact("JA1XYZ"))); + } }