Open a log N1MM wrote
Three things stopped it. A contest is keyed by ContestNR, not by ContestID. ContestID is only the ContestInstance table's primary key; DXLOG.ContestNR refers to ContestNR, which is what N1MM's SQLWhereString matches on. The two agree in a fresh log and drift apart in one that has been used for a while, so we were reading one contest's header with another contest's contacts. In a real log of 56284 contacts, joining on ContestID disagreed with the contact's own ContestName 52029 times; joining on ContestNR disagreed 109 times. N1MM names a contest per mode: CQWWCW, CQWWSSB, CQWWRTTY, never plain CQWW. Our contests now use those names, and N1mmContestNames turns one back into a contest and a mode when a log is opened. The name carries the mode, so it beats the ModeCategory column. Plain CQWW still opens, because older versions wrote it that way. The log columns were sized by counting characters, which was too narrow for the headers: those are drawn in the theme's font, not the grid's monospace. The grid sizes them now, with the character count as a floor so a column of short values does not collapse. Bandmap spots now age from when they arrived rather than from the time written in them. A node with a wrong clock, or one replaying its backlog on connect, emptied the bandmap as fast as it filled it. Checked by opening a real 56284-contact N1MM log under Xvfb and reading the contest and contacts back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ public sealed class ContestRegistry
|
||||
new("CQWW", "CQ World Wide DX", [ModeCategory.Cw, ModeCategory.Phone], m => new CqWorldWide(m)),
|
||||
new("CQWPX", "CQ WPX", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new CqWpx(m)),
|
||||
new("ARRLDX", "ARRL International DX", [ModeCategory.Cw, ModeCategory.Phone], m => new ArrlDx(m)),
|
||||
new("IARUHF", "IARU HF World Championship", [ModeCategory.Cw, ModeCategory.Phone], _ => new IaruHf()),
|
||||
new("IARU", "IARU HF World Championship", [ModeCategory.Cw, ModeCategory.Phone], _ => new IaruHf()),
|
||||
new("SS", "ARRL Sweepstakes", [ModeCategory.Cw, ModeCategory.Phone], m => new Sweepstakes(m)),
|
||||
new("ARRLRTTY", "ARRL RTTY Roundup", [ModeCategory.Digital], _ => new RttyRoundup()),
|
||||
new("NAQP", "North American QSO Party", [ModeCategory.Cw, ModeCategory.Phone, ModeCategory.Digital], m => new NorthAmericanQsoParty(m)),
|
||||
@@ -68,10 +68,33 @@ public sealed class ContestRegistry
|
||||
public IReadOnlyList<ContestChoice> Choices =>
|
||||
byName.Values.OrderBy(c => c.DisplayName, StringComparer.Ordinal).ToList();
|
||||
|
||||
public Contest Create(string name, ModeCategory mode) =>
|
||||
byName.TryGetValue(name, out ContestChoice? choice)
|
||||
? choice.Create(mode)
|
||||
: throw new KeyNotFoundException($"no contest named '{name}'");
|
||||
public Contest Create(string name, ModeCategory mode)
|
||||
{
|
||||
ContestChoice choice = Find(name, ref mode)
|
||||
?? throw new KeyNotFoundException($"no contest named '{name}'");
|
||||
return choice.Create(mode);
|
||||
}
|
||||
|
||||
public bool Has(string name) => byName.ContainsKey(name);
|
||||
public bool Has(string name)
|
||||
{
|
||||
ModeCategory ignored = ModeCategory.Cw;
|
||||
return Find(name, ref ignored) is not null;
|
||||
}
|
||||
|
||||
/// A name N1MM wrote carries the mode, and that is better than whatever the
|
||||
/// log's mode column says, so it wins when the name resolves that way.
|
||||
private ContestChoice? Find(string name, ref ModeCategory mode)
|
||||
{
|
||||
if (byName.TryGetValue(name, out ContestChoice? choice))
|
||||
{
|
||||
return choice;
|
||||
}
|
||||
if (N1mmContestNames.TryResolve(name, out string family, out ModeCategory named)
|
||||
&& byName.TryGetValue(family, out ContestChoice? resolved))
|
||||
{
|
||||
mode = named;
|
||||
return resolved;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
40
src/Nonemm.Contests/N1mmContestNames.cs
Normal file
40
src/Nonemm.Contests/N1mmContestNames.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Contests;
|
||||
|
||||
/// N1MM names a contest per mode: CQ WW is `CQWWCW`, `CQWWSSB` or `CQWWRTTY`,
|
||||
/// never plain `CQWW`. Our registry is keyed by the family, so a log written by
|
||||
/// N1MM has to have its contest name turned back into a family and a mode
|
||||
/// before the rules can be built.
|
||||
public static class N1mmContestNames
|
||||
{
|
||||
private static readonly Dictionary<string, (string Family, ModeCategory Mode)> Known =
|
||||
new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["CQWWCW"] = ("CQWW", ModeCategory.Cw),
|
||||
["CQWWSSB"] = ("CQWW", ModeCategory.Phone),
|
||||
["CQWWRTTY"] = ("CQWW", ModeCategory.Digital),
|
||||
["CQWPXCW"] = ("CQWPX", ModeCategory.Cw),
|
||||
["CQWPXSSB"] = ("CQWPX", ModeCategory.Phone),
|
||||
["CQWPXRTTY"] = ("CQWPX", ModeCategory.Digital),
|
||||
["ARRLDXCW"] = ("ARRLDX", ModeCategory.Cw),
|
||||
["ARRLDXSSB"] = ("ARRLDX", ModeCategory.Phone),
|
||||
["SSCW"] = ("SS", ModeCategory.Cw),
|
||||
["SSSSB"] = ("SS", ModeCategory.Phone),
|
||||
["NAQPCW"] = ("NAQP", ModeCategory.Cw),
|
||||
["NAQPSSB"] = ("NAQP", ModeCategory.Phone),
|
||||
["NAQPRTTY"] = ("NAQP", ModeCategory.Digital),
|
||||
};
|
||||
|
||||
public static bool TryResolve(string name, out string family, out ModeCategory mode)
|
||||
{
|
||||
if (Known.TryGetValue(name.Trim(), out (string Family, ModeCategory Mode) found))
|
||||
{
|
||||
(family, mode) = found;
|
||||
return true;
|
||||
}
|
||||
family = "";
|
||||
mode = ModeCategory.Cw;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ public sealed class ArrlDx : Contest
|
||||
|
||||
public ArrlDx(ModeCategory mode) => this.mode = mode;
|
||||
|
||||
public string Name => "ARRLDX";
|
||||
public string Name => mode == ModeCategory.Cw ? "ARRLDXCW" : "ARRLDXSSB";
|
||||
|
||||
public string DisplayName => $"ARRL International DX {ModeLabel()}";
|
||||
|
||||
|
||||
@@ -11,7 +11,12 @@ public sealed class CqWorldWide : Contest
|
||||
|
||||
public CqWorldWide(ModeCategory mode) => this.mode = mode;
|
||||
|
||||
public string Name => "CQWW";
|
||||
public string Name => mode switch
|
||||
{
|
||||
ModeCategory.Phone => "CQWWSSB",
|
||||
ModeCategory.Digital => "CQWWRTTY",
|
||||
_ => "CQWWCW",
|
||||
};
|
||||
|
||||
public string DisplayName => $"CQ World Wide DX {ModeLabel()}";
|
||||
|
||||
|
||||
@@ -15,7 +15,12 @@ public sealed class CqWpx : Contest
|
||||
|
||||
public CqWpx(ModeCategory mode) => this.mode = mode;
|
||||
|
||||
public string Name => "CQWPX";
|
||||
public string Name => mode switch
|
||||
{
|
||||
ModeCategory.Phone => "CQWPXSSB",
|
||||
ModeCategory.Digital => "CQWPXRTTY",
|
||||
_ => "CQWPXCW",
|
||||
};
|
||||
|
||||
public string DisplayName => $"CQ WPX {ModeLabel()}";
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Nonemm.Contests.Rules;
|
||||
/// headquarters station, its abbreviation; both count as multipliers per band.
|
||||
public sealed class IaruHf : Contest
|
||||
{
|
||||
public string Name => "IARUHF";
|
||||
public string Name => "IARU";
|
||||
|
||||
public string DisplayName => "IARU HF World Championship";
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ public sealed class Sweepstakes : Contest
|
||||
|
||||
public Sweepstakes(ModeCategory mode) => this.mode = mode;
|
||||
|
||||
public string Name => "SS";
|
||||
public string Name => mode == ModeCategory.Cw ? "SSCW" : "SSSSB";
|
||||
|
||||
public string DisplayName => $"ARRL Sweepstakes {ModeLabel()}";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user