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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
26
src/Nonemm.Contests/Multipliers/OkOmDistricts.cs
Normal file
26
src/Nonemm.Contests/Multipliers/OkOmDistricts.cs
Normal 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
21
src/Nonemm.Contests/Multipliers/SectionLists.cs
Normal file
21
src/Nonemm.Contests/Multipliers/SectionLists.cs
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user