Read the rest of what a .udc file says, and score what N1MM scores

Two pieces of work on the scoring engine.

The .udc reader now takes N1MM's published scoring vocabulary: the full
PointsPerContact condition set, the PointsMultBy family, PowerMult, BonusPoints,
the multiplier sources and the settings that say which stations bring a
multiplier in. MultMult is a weight rather than a switch, as in N1MM's
ComputeScore. The entry categories and the sent exchange reach the rules in a
new ContestEntry, so a contest can score by the power category it is entered in
or by what this station sends.

Then every contest in a real N1MM log was scored again from these rules and
compared with the points and multiplier flags N1MM wrote. That found five bugs:
ARRL DX and IARU read the exchange from the wrong column, a stored " " was read
as a value rather than as empty, CQ WW RTTY's own Canadian area names were not
counted, a maritime mobile scored nothing, and a contact in a mode the contest
does not run scored as if it were in the contest.

docs/n1mm-interop.md has the check and what still differs.

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 08:33:40 +00:00
co-authored by Claude Opus 5
parent 9424c488ea
commit 04568d0ca3
28 changed files with 1088 additions and 142 deletions
@@ -21,11 +21,50 @@ public static class StatesAndProvinces
"NS", "QC", "ON", "MB", "SK", "AB", "BC", "NT", "NB", "NL", "PE", "YT", "NU",
};
/// What ARRL DX counts for a DX station: the contiguous states, the District
/// of Columbia, and the Canadian provinces and territories.
/// The Canadian areas as the contests that split them name them:
/// Newfoundland apart from Labrador, and the territories spelled out.
public static readonly IReadOnlyList<string> SplitCanadianAreas =
["NWT", "NF", "LB", "PEI"];
/// What ARRL DX counts for a DX station: the contiguous states, the
/// District of Columbia, and the Canadian provinces and territories.
public static readonly IReadOnlySet<string> ArrlDxMultipliers =
new HashSet<string>(ContiguousStates.Concat(["DC"]).Concat(Provinces), StringComparer.OrdinalIgnoreCase);
/// The same province under another name. Canadian stations send the older
/// abbreviations as often as the postal codes, and ARRL DX counts the
/// province, not the spelling, so `NF` and `LB` are both Newfoundland and
/// Labrador and count once between them. N1MM counts them as two.
private static readonly IReadOnlyDictionary<string, string> OtherSpellings =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["NF"] = "NL",
["LB"] = "NL",
["NFL"] = "NL",
["QB"] = "QC",
["PQ"] = "QC",
["PEI"] = "PE",
["NWT"] = "NT",
};
/// The province or state a received ARRL DX exchange names, or the exchange
/// as it was sent when it names none.
public static string ArrlDxArea(string received)
{
string text = received.Trim();
return OtherSpellings.TryGetValue(text, out string? province) ? province : text.ToUpperInvariant();
}
/// What CQ WW RTTY counts: the 48 contiguous states, DC, and the Canadian
/// areas as that contest names them, which is not the postal list — it
/// splits Newfoundland from Labrador and spells out `NWT` and `PEI`.
public static readonly IReadOnlySet<string> CqWwRttyAreas =
new HashSet<string>(
ContiguousStates
.Concat(["DC", "NB", "NS", "QC", "ON", "MB", "SK", "AB", "BC", "NU", "YT"])
.Concat(SplitCanadianAreas),
StringComparer.OrdinalIgnoreCase);
public static bool IsStateOrProvince(string text) =>
AllStates.Contains(text.Trim()) || Provinces.Contains(text.Trim()) ||
text.Trim().Equals("DC", StringComparison.OrdinalIgnoreCase);