Check a .udc section multiplier against the list the file names

MultWindowType names the list N1MM validates a received section against,
and every exchange that was not all digits counted as a multiplier here.
Four of N1MM's lists are held now -- the ARRL sections, the US states,
the Canadian provinces and the 165 OK/OM districts, which came out of
N1MM's own exe -- and a file naming one of them counts only the values
on it. A file naming any of the hundred-odd others keeps the old rule.

The 2023 OK/OM DX log now scores contact for contact as N1MM did. Two
contacts in the 2026 log still differ: the district code is stored with
a trailing space, which N1MM matches against its list untrimmed, so it
counts no multiplier where this counts one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RoGtneMQaz4M9w7Kk49AVD
This commit is contained in:
2026-08-31 13:30:27 +00:00
parent 921033cd93
commit 1d3167cd04
5 changed files with 100 additions and 22 deletions

View File

@@ -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, `.udc` settings that are still passed over. `CallHist` as a multiplier source,
which takes the value from the call history file. `BonusPoints2`, which reads which takes the value from the call history file. `BonusPoints2`, which reads
the bonus callsigns from a file. `MultiplierBands`, `MultWindowType` and the bonus callsigns from a file. `MultiplierBands` and `QsoErrorString`, which
`QsoErrorString`, which are about the windows rather than the score. are about the windows rather than the score. `GenericPrintString`, which is the
`GenericPrintString`, which is the layout of a printed log rather than a layout of a printed log rather than a Cabrillo one.
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 `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 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 of REF, ARRL 10M, the Ukrainian DX contest and the Russian DX contest, which
are not written yet. are not written yet.
Every contest in the station's own logs can now be opened, and all but five of Every contest in the station's own logs can now be opened, and its 78 contest
its 78 contest instances score contact for contact as N1MM did. instances score contact for contact as N1MM did apart from the cases below.
What still differs, and why: 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 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 running has different rules and its own pair of files, one for each side of
the contest. the contest.
- **A `.udc` section multiplier** is counted for any exchange that is not all - **Two OK/OM DX contacts** whose district code was stored with a trailing
digits. N1MM checks it against the section list given in the file's space. N1MM checks the code against its list without trimming it, finds
`MultWindowType` setting, and those lists are not held here, so a US state nothing and counts no multiplier; the code is trimmed here and counts. Both
sent into an OK/OM DX log counts as a multiplier here and not in N1MM. Three contacts are the same station in the 2026 log.
contacts across the two logs.
## Known rough edges ## Known rough edges

View File

@@ -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<string> All = new HashSet<string>(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",
};
}

View File

@@ -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<string>? For(string multWindowType) =>
multWindowType.Trim().ToUpperInvariant() switch
{
"SECTION" => ArrlSections.All,
"STATE" => StatesAndProvinces.AllStates,
"PROVINCE" => StatesAndProvinces.Provinces,
"OKOMDX" => OkOmDistricts.All,
_ => null,
};
}

View File

@@ -1,3 +1,4 @@
using Nonemm.Contests.Multipliers;
using Nonemm.Core; using Nonemm.Core;
namespace Nonemm.Contests.Udc; namespace Nonemm.Contests.Udc;
@@ -17,6 +18,7 @@ public sealed class UdcMultiplier
private readonly IReadOnlyList<string> onlyFor; private readonly IReadOnlyList<string> onlyFor;
private readonly IReadOnlyList<string> notFor; private readonly IReadOnlyList<string> notFor;
private readonly IReadOnlyList<string> onlyForSections; private readonly IReadOnlyList<string> onlyForSections;
private readonly IReadOnlySet<string>? sections;
public UdcMultiplier(int index, UdcFile file, UdcMultiplierScope shared) public UdcMultiplier(int index, UdcFile file, UdcMultiplierScope shared)
{ {
@@ -32,6 +34,7 @@ public sealed class UdcMultiplier
onlyFor = file.List($"CountMultOnlyFor{suffix}"); onlyFor = file.List($"CountMultOnlyFor{suffix}");
notFor = file.List($"DoNotCountMultOnlyFor{suffix}"); notFor = file.List($"DoNotCountMultOnlyFor{suffix}");
onlyForSections = file.List($"CountMultOnlyForSec{suffix}"); onlyForSections = file.List($"CountMultOnlyForSec{suffix}");
sections = SectionLists.For(file.Text("MultWindowType"));
} }
public bool IsDefined => source.Length > 0; public bool IsDefined => source.Length > 0;
@@ -127,16 +130,24 @@ public sealed class UdcMultiplier
private static string Upper(string text) => text.Trim().ToUpperInvariant(); private static string Upper(string text) => text.Trim().ToUpperInvariant();
/// A section multiplier ignores a value that is all digits. N1MM checks the /// A section multiplier counts only a value on the list `MultWindowType`
/// received exchange against the section list the contest names, and a file /// names, which is what N1MM checks it against. Without a list — this
/// whose section column also carries the serial numbers the other side of /// program holds four of N1MM's, and N1MM has over a hundred — anything
/// the contest sends — OK/OM DX SSB is one — would otherwise count every /// that is not all digits counts, so a file whose section column also
/// serial number as a multiplier. The section lists themselves are not held /// carries the serial numbers the other side of the contest sends — OK/OM
/// here. /// DX SSB is one — does not count every serial number as a multiplier.
private static string? SectionName(string text) private string? SectionName(string text)
{ {
string section = Upper(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) private static string? LastLetter(string call)

View File

@@ -264,9 +264,8 @@ public class UdcScoringTests
/// The received exchange of a contest where one side sends a section and /// 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 /// 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 /// section counts. A file that names no list this program holds falls back
/// lists are not held here, so an exchange that is all digits is taken for /// to taking an exchange of all digits for a serial number.
/// a serial number.
[Fact] [Fact]
public void ASerialNumberIsNoSectionMultiplier() public void ASerialNumberIsNoSectionMultiplier()
{ {
@@ -275,6 +274,23 @@ public class UdcScoringTests
Assert.Single(LogFor(contest).Judge(TestLog.Contact("OM3XYZ", section: "NOV")).NewMultipliers); 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 /// Published files hold `Country` in this key; the UDC editor puts
/// `CountryPrefix` there. /// `CountryPrefix` there.
[Fact] [Fact]