diff --git a/docs/unfinished.md b/docs/unfinished.md index b96186b..7adbf83 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -110,10 +110,15 @@ with the CW and SSB running of the contest. `.udc` settings that are still passed over. `CallHist` as a multiplier source, which takes the value from the call history file. `BonusPoints2`, which reads -the bonus callsigns from a file. `MultiplierBands`, `MultWindowType` and -`QsoErrorString`, which are about the windows rather than the score. -`GenericPrintString`, which is the layout of a printed log rather than a -Cabrillo one. +the bonus callsigns from a file. `MultiplierBands` and `QsoErrorString`, which +are about the windows rather than the score. `GenericPrintString`, which is the +layout of a printed log rather than a Cabrillo one. + +`MultWindowType` is read as far as the score goes: it names the list a section +multiplier is checked against. Four of N1MM's lists are held here — the ARRL +sections, the US states, the Canadian provinces and the OK/OM districts — and a +file naming any of the hundred-odd others falls back to counting any exchange +that is not all digits. `CabrilloString` and `CabrilloVersion` are read: a contest whose sponsor asks for its own columns gets them, in N1MM's shape — the operator's callsign in the @@ -140,8 +145,8 @@ a county from OK and OM stations and a serial number from everyone else — and of REF, ARRL 10M, the Ukrainian DX contest and the Russian DX contest, which are not written yet. -Every contest in the station's own logs can now be opened, and all but five of -its 78 contest instances score contact for contact as N1MM did. +Every contest in the station's own logs can now be opened, and its 78 contest +instances score contact for contact as N1MM did apart from the cases below. What still differs, and why: @@ -158,11 +163,10 @@ What still differs, and why: folder the station's logs score 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 given in the file's - `MultWindowType` setting, and those lists are not held here, so a US state - sent into an OK/OM DX log counts as a multiplier here and not in N1MM. Three - contacts across the two logs. +- **Two OK/OM DX contacts** whose district code was stored with a trailing + space. N1MM checks the code against its list without trimming it, finds + nothing and counts no multiplier; the code is trimmed here and counts. Both + contacts are the same station in the 2026 log. ## Known rough edges diff --git a/src/Nonemm.Contests/Multipliers/OkOmDistricts.cs b/src/Nonemm.Contests/Multipliers/OkOmDistricts.cs new file mode 100644 index 0000000..1f43ef3 --- /dev/null +++ b/src/Nonemm.Contests/Multipliers/OkOmDistricts.cs @@ -0,0 +1,26 @@ +namespace Nonemm.Contests.Multipliers; + +/// The 165 district codes the OK/OM DX contest exchanges. N1MM keeps the same +/// list in its admin database under the name `OKOMDX`, which is the name a +/// `.udc` file gives in `MultWindowType`. +public static class OkOmDistricts +{ + public static readonly IReadOnlySet All = new HashSet(StringComparer.OrdinalIgnoreCase) + { + "APA", "APB", "APC", "APD", "APE", "APF", "APG", "APH", "API", "APJ", "BBE", + "BBN", "BKD", "BKH", "BKO", "BMB", "BME", "BNY", "BPB", "BPV", "BPZ", "BRA", + "CBU", "CCK", "CJH", "CPE", "CPI", "CPR", "CST", "CTA", "DCH", "DDO", "DKL", + "DKV", "DPJ", "DPM", "DPS", "DRO", "DSO", "DTA", "ECH", "ECL", "EDE", "EJA", + "ELI", "ELO", "ELT", "EMO", "ETE", "EUL", "FCR", "FHB", "FHK", "FJI", "FNA", + "FPA", "FRK", "FSE", "FSV", "FTR", "FUO", "GBL", "GBM", "GBR", "GBV", "GHO", + "GJI", "GKR", "GPR", "GTR", "GUH", "GVY", "GZL", "GZN", "GZS", "HBR", "HFM", + "HJE", "HKA", "HNJ", "HOL", "HOP", "HOS", "HPR", "HSU", "HVS", "BAA", "BAB", + "BAC", "BAD", "BAE", "MAL", "PEZ", "SEN", "DST", "GAL", "HLO", "PIE", "SEA", + "SKA", "TRN", "BAN", "ILA", "MYJ", "NMV", "PAR", "PBY", "PRI", "PUC", "TNC", + "KOM", "LVC", "NIT", "NZA", "SAL", "TOP", "ZMO", "BYT", "CAD", "DKU", "KNM", + "LMI", "MAR", "NAM", "RUZ", "TTE", "TVR", "ZIL", "BBY", "BRE", "BST", "DET", + "KRU", "LUC", "POL", "REV", "RSO", "VKR", "ZAR", "ZIH", "ZVO", "GEL", "KEA", + "KEB", "KEC", "KED", "KEO", "MIC", "ROZ", "SNV", "SOB", "TRE", "BAR", "HUM", + "KEZ", "LEV", "MED", "POP", "PRE", "SAB", "SLU", "SNI", "STR", "SVI", "VRT", + }; +} diff --git a/src/Nonemm.Contests/Multipliers/SectionLists.cs b/src/Nonemm.Contests/Multipliers/SectionLists.cs new file mode 100644 index 0000000..636d30c --- /dev/null +++ b/src/Nonemm.Contests/Multipliers/SectionLists.cs @@ -0,0 +1,21 @@ +namespace Nonemm.Contests.Multipliers; + +/// The lists N1MM checks a received section against, by the name a contest's +/// `MultWindowType` gives. N1MM holds well over a hundred of them in its admin +/// database; the ones here are the lists this program already has, plus the +/// OK/OM districts. +/// +/// A name with no list gives null, and the caller then counts whatever the +/// operator typed. +public static class SectionLists +{ + public static IReadOnlySet? For(string multWindowType) => + multWindowType.Trim().ToUpperInvariant() switch + { + "SECTION" => ArrlSections.All, + "STATE" => StatesAndProvinces.AllStates, + "PROVINCE" => StatesAndProvinces.Provinces, + "OKOMDX" => OkOmDistricts.All, + _ => null, + }; +} diff --git a/src/Nonemm.Contests/Udc/UdcMultipliers.cs b/src/Nonemm.Contests/Udc/UdcMultipliers.cs index 93dc5e9..a94ac30 100644 --- a/src/Nonemm.Contests/Udc/UdcMultipliers.cs +++ b/src/Nonemm.Contests/Udc/UdcMultipliers.cs @@ -1,3 +1,4 @@ +using Nonemm.Contests.Multipliers; using Nonemm.Core; namespace Nonemm.Contests.Udc; @@ -17,6 +18,7 @@ public sealed class UdcMultiplier private readonly IReadOnlyList onlyFor; private readonly IReadOnlyList notFor; private readonly IReadOnlyList onlyForSections; + private readonly IReadOnlySet? sections; public UdcMultiplier(int index, UdcFile file, UdcMultiplierScope shared) { @@ -32,6 +34,7 @@ public sealed class UdcMultiplier onlyFor = file.List($"CountMultOnlyFor{suffix}"); notFor = file.List($"DoNotCountMultOnlyFor{suffix}"); onlyForSections = file.List($"CountMultOnlyForSec{suffix}"); + sections = SectionLists.For(file.Text("MultWindowType")); } public bool IsDefined => source.Length > 0; @@ -127,16 +130,24 @@ 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) + /// A section multiplier counts only a value on the list `MultWindowType` + /// names, which is what N1MM checks it against. Without a list — this + /// program holds four of N1MM's, and N1MM has over a hundred — anything + /// that is not all digits counts, so a file whose section column also + /// carries the serial numbers the other side of the contest sends — OK/OM + /// DX SSB is one — does not count every serial number as a multiplier. + private string? SectionName(string text) { string section = Upper(text); - return section.Length > 0 && section.All(char.IsAsciiDigit) ? null : section; + if (section.Length == 0) + { + return null; + } + if (sections is not null) + { + return sections.Contains(section) ? section : null; + } + return section.All(char.IsAsciiDigit) ? null : section; } private static string? LastLetter(string call) diff --git a/tests/Nonemm.Contests.Tests/UdcScoringTests.cs b/tests/Nonemm.Contests.Tests/UdcScoringTests.cs index 70d60c6..d10aa06 100644 --- a/tests/Nonemm.Contests.Tests/UdcScoringTests.cs +++ b/tests/Nonemm.Contests.Tests/UdcScoringTests.cs @@ -264,9 +264,8 @@ public class UdcScoringTests /// 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. + /// section counts. A file that names no list this program holds falls back + /// to taking an exchange of all digits for a serial number. [Fact] public void ASerialNumberIsNoSectionMultiplier() { @@ -275,6 +274,23 @@ public class UdcScoringTests Assert.Single(LogFor(contest).Judge(TestLog.Contact("OM3XYZ", section: "NOV")).NewMultipliers); } + /// `MultWindowType` names the list N1MM checks the section against. `GZS` + /// is an OK/OM district and counts; `NOV` and the US state `MA` are not on + /// that list and count for neither program. + [Theory] + [InlineData("GZS", 1)] + [InlineData("NOV", 0)] + [InlineData("MA", 0)] + public void ASectionOffTheContestsListIsNoMultiplier(string section, int expected) => + Assert.Equal( + expected, + LogFor(Contest( + "MultSqlString=Section", + "IsMultPer=1", + "MultWindowType=OKOMDX")) + .Judge(TestLog.Contact("OM3XYZ", section: section)) + .NewMultipliers.Count); + /// Published files hold `Country` in this key; the UDC editor puts /// `CountryPrefix` there. [Fact]