Score the last three cases the way N1MM scores them

The three differences left after replaying the log were ours on purpose. A log
has to score the same in both programs, so they now follow N1MM:

- ARRL DX and CQ WW RTTY count the 14 Canadian areas N1MM lists as `48SDC14P`,
  which keeps Newfoundland apart from Labrador. A station sending the postal
  code is counted as the area the contest names.
- A maritime mobile is scored from the call it signs. CQ WW says it counts for
  the zone alone; N1MM gives it the country, and so do we.
- A callsign the country file cannot place counts as a multiplier with an empty
  value, so every unplaceable call worked on a band shares one multiplier.

29 of the 37 contest instances in the log now agree with N1MM on every contact
and on the totals. What is left is the country file of the day and rows whose
cached points N1MM never worked out again after a call was corrected.

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:36:44 +00:00
parent 04568d0ca3
commit 5c0ca558c1
8 changed files with 69 additions and 60 deletions

View File

@@ -100,29 +100,38 @@ from 2019 to 2026, 57,000 contacts, 28 contest names. Replaying it is how the
scoring rules are checked — every contact is scored again from our rules and
compared with the points and multiplier flags N1MM wrote.
26 of the 37 instances whose contest we have rules for now agree on every
contact and on the totals. What the check found and fixed:
29 of the 37 instances whose contest we have rules for agree on every contact
and on the totals. What the check found and fixed:
| What | Was |
|---|---|
| ARRL DX and IARU read the wrong column | no multipliers at all, and IARU scored every contact 1 point |
| a stored `" "` was read as a value | 181 contacts in one log scored as another continent |
| the CQ WW RTTY area list | `NF` and `LB` are that contest's own names for the Canadian areas, and were not counted |
| ARRL DX province spellings | `QB`, `NF` and `LB` count as Quebec and as Newfoundland and Labrador |
| a maritime mobile scored nothing | it counts for no country and still scores by continent |
| the Canadian area lists | ARRL DX and CQ WW RTTY count the 14 areas N1MM calls `48SDC14P`, Newfoundland apart from Labrador, and the postal codes map onto them |
| a maritime mobile scored nothing | it is scored from the call it signs, which is the country N1MM logs for it |
| a call the country file cannot place brought no multiplier | it counts, with an empty value, and all such calls share the one multiplier per band |
| a contact in a mode the contest does not run | scored as if it were in the contest; N1MM scores it zero and keeps its multiplier |
What still differs, and why:
What still differs, and why. None of it is a rule:
- **The country file of the day.** A 2019 log holds calls that today's
`wl_cty.dat` places elsewhere, and the multiplier moves to another contact
with it. This is most of the difference in the 2019 CQ WW SSB log.
- **N1MM counts a call it cannot place as a multiplier with an empty value**,
and we do not. Same as the WAE difference already written down.
- **N1MM counts `NF` and `LB` as two ARRL DX multipliers.** They are one
province, and we count one.
- **A maritime mobile brings no country multiplier here**, which is the
published CQ WW rule; N1MM counts one for it.
`wl_cty.dat` places elsewhere`TO7O` was Mayotte then and is France now,
China was not split by call area yet — and the multiplier moves to another
contact with it. This is nearly all of the difference in the 2019 CQ WW SSB
log and all of it in the 2021 ones.
- **Stale cached points.** N1MM caches points in the row and does not always
work them out again after a callsign is corrected, so a handful of rows hold
a number that its own rules do not produce.
a number that its own rules do not produce. Two IARU rows and about six CQ WW
rows across 57,000 contacts.
- **A one-contact log** whose contact also sits in another instance of the same
contest.
### What N1MM does that the published rules do not say
Three of the fixes above follow N1MM rather than the rule as published, because
a log has to score the same in both programs:
- ARRL DX counts `NF` and `LB` as two multipliers. They are one province.
- A maritime mobile counts for the country of the call it signs. CQ WW says it
counts for the zone alone.
- A callsign that places nowhere counts as a multiplier with an empty value.

View File

@@ -55,9 +55,7 @@ any other message.
disagreements are the country file of the day, not the rules: 4U1A counted as
`4U1V` then and as `OE` in today's `wl_cty.dat`, `KP2BH` counted as `KP2` then
and is listed as a US call now, and China was not split by call area yet. The
2022 to 2025 logs agree to the contact. One difference is ours on purpose: N1MM
counts a callsign it cannot place — `D1M` — as a multiplier with an empty
value, and we do not.
2022 to 2025 logs agree to the contact.
**Telnet window: what N1MM has and this does not.** Left out: the special-calls
list, and saving received spots to a database — N1MM keeps those spots in its

View File

@@ -22,37 +22,41 @@ public static class StatesAndProvinces
};
/// The Canadian areas as the contests that split them name them:
/// Newfoundland apart from Labrador, and the territories spelled out.
/// Newfoundland apart from Labrador, and the territories spelled out. N1MM
/// calls this list `48SDC14P` — 48 states, DC and 14 provinces — and both
/// ARRL DX and CQ WW RTTY count them this way.
public static readonly IReadOnlyList<string> SplitCanadianAreas =
["NWT", "NF", "LB", "PEI"];
["NB", "NS", "QC", "ON", "MB", "SK", "AB", "BC", "NWT", "NF", "LB", "NU", "YT", "PEI"];
/// What ARRL DX counts for a DX station: the contiguous states, the
/// District of Columbia, and the Canadian provinces and territories.
/// What ARRL DX counts for a DX station: the 48 contiguous states, the
/// District of Columbia and those 14 areas. Alaska and Hawaii are DX for
/// this contest and count for none of it.
public static readonly IReadOnlySet<string> ArrlDxMultipliers =
new HashSet<string>(ContiguousStates.Concat(["DC"]).Concat(Provinces), StringComparer.OrdinalIgnoreCase);
new HashSet<string>(
ContiguousStates.Concat(["DC"]).Concat(SplitCanadianAreas),
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.
/// The same area under another name. A Canadian station sends the postal
/// code as often as the abbreviation the contest lists, and `NL` is read as
/// Newfoundland, which is the half of that province a station signing the
/// postal code is nearly always in; Labrador signs `LB`.
private static readonly IReadOnlyDictionary<string, string> OtherSpellings =
new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["NF"] = "NL",
["LB"] = "NL",
["NFL"] = "NL",
["NL"] = "NF",
["NFL"] = "NF",
["QB"] = "QC",
["PQ"] = "QC",
["PEI"] = "PE",
["NWT"] = "NT",
["PE"] = "PEI",
["NT"] = "NWT",
};
/// The province or state a received ARRL DX exchange names, or the exchange
/// as it was sent when it names none.
/// The area 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();
return OtherSpellings.TryGetValue(text, out string? area) ? area : text.ToUpperInvariant();
}
/// What CQ WW RTTY counts: the 48 contiguous states, DC, and the Canadian
@@ -60,9 +64,7 @@ public static class StatesAndProvinces
/// 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),
ContiguousStates.Concat(["DC"]).Concat(SplitCanadianAreas),
StringComparer.OrdinalIgnoreCase);
public static bool IsStateOrProvince(string text) =>

View File

@@ -102,10 +102,7 @@ public sealed class CqWorldWide : Contest
{
found.Add(new Multiplier(1, qso.Qso.Zone.ToString(), band));
}
if (qso.CountryPrefix.Length > 0 && qso.Qso.Call.CountsForEntity)
{
found.Add(new Multiplier(2, qso.CountryPrefix, band));
}
found.Add(new Multiplier(2, qso.CountryPrefix, band));
string home = qso.Qso.Section.Trim().ToUpperInvariant();
if (IsRtty && StatesAndProvinces.CqWwRttyAreas.Contains(home))
{

View File

@@ -84,14 +84,11 @@ public sealed class Wae : Contest
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{
int weight = WeightFor(qso.Band);
if (weight == 0 || !IsWorkable(qso) || !qso.Qso.Call.CountsForEntity)
if (weight == 0 || !IsWorkable(qso))
{
return [];
}
string value = MultiplierValue(qso);
return value.Length == 0
? []
: [new Multiplier(1, value, qso.Band?.Name ?? "", weight)];
return [new Multiplier(1, MultiplierValue(qso), qso.Band?.Name ?? "", weight)];
}
public int TotalScore(ScoreTally tally, ContestEntry entry) => tally.Points * tally.TotalMultipliers;

View File

@@ -64,12 +64,15 @@ public class ArrlDxTests
Assert.Empty(log.Judge(TestLog.Contact("VO2XYZ", section: "LB")).NewMultipliers);
}
[Fact]
public void TheOlderQuebecAbbreviationIsQuebec()
[Theory]
[InlineData("QB", "QC")]
[InlineData("PE", "PEI")]
[InlineData("NT", "NWT")]
public void AnotherSpellingIsTheSameArea(string sent, string counted)
{
ContestLog log = new(new ArrlDx(ModeCategory.Phone), TestLog.Germany, TestLog.CountryFile);
Assert.Equal(
"QC",
log.Judge(TestLog.Contact("VA2XYZ", section: "QB")).NewMultipliers.Single().Value);
counted,
log.Judge(TestLog.Contact("VA2XYZ", section: sent)).NewMultipliers.Single().Value);
}
}

View File

@@ -101,13 +101,14 @@ public class CqWorldWideRttyTests
LogFor(TestLog.Germany).Judge(Contact("VO1XYZ", 5, area)).NewMultipliers,
m => m.Index == 3 && m.Value == area);
/// A maritime mobile station belongs to no country. CQ WW counts it for the
/// zone alone, and the contact still scores by continent.
/// A maritime mobile station is at sea and belongs to no country. It is
/// scored from the call it signs, which is the country N1MM logs for it,
/// and it brings no WPX prefix.
[Fact]
public void MaritimeMobileScoresButBringsNoCountry()
public void MaritimeMobileIsScoredFromTheCallItSigns()
{
Verdict verdict = LogFor(TestLog.Germany).Judge(Contact("IK2XYZ/MM", 15));
Assert.Equal(2, verdict.Points);
Assert.DoesNotContain(verdict.NewMultipliers, m => m.Index == 2);
Assert.Contains(verdict.NewMultipliers, m => m.Index == 2 && m.Value == "I");
}
}

View File

@@ -103,14 +103,16 @@ public class WaeTests
Assert.Equal("UA0", multiplier.Value);
}
/// N1MM counts a callsign it cannot place as a multiplier with an empty
/// value, which inflates the score. A call with no country is no
/// multiplier here.
/// A callsign the country file cannot place still counts, with an empty
/// value, so the log scores the same here as in N1MM. All the unplaceable
/// calls worked on a band share the one multiplier between them.
[Fact]
public void ACallWithNoCountryIsNoMultiplier()
public void ACallWithNoCountryCountsOncePerBand()
{
ContestLog log = LogFor(Cw, TestLog.Germany);
Assert.Empty(log.Judge(On("D1M", 14_025)).NewMultipliers);
Assert.Equal("", Assert.Single(log.Judge(On("D1M", 14_025)).NewMultipliers).Value);
log.Add(On("D1M", 14_025));
Assert.Empty(log.Judge(On("D2X", 14_025)).NewMultipliers);
}
[Fact]