Show the boxes N1MM shows for each contest

The entry window had a fixed set of boxes with a Name and Comment box beside
the exchange. N1MM lays out one row of boxes per contest and nothing else, so
this does the same: general logging keeps Name and Comment, and a contest gets
the boxes it exchanges.

Two new members on `Contest` carry the cases where the boxes and the log
columns are not the same list:

- `FromExchange1` spreads what was typed in one box across the log columns.
  Where the other station sends a serial or a district code depending on where
  it is, N1MM gives it one box and sorts the value into the number, section or
  zone column from there, so the score and the Cabrillo file read it from the
  same column N1MM does.
- `LogFieldsFor` gives the log window its own column list. Sweepstakes takes
  the whole exchange in one box and logs it in four columns; `SweepstakesExchange`
  parses `72 A 74 SCV` in any order, reading from the last word back as N1MM
  does.

The sent serial is now marked as not required and space steps over it along
with the two report boxes, because nobody types the number going out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
2026-09-09 20:12:37 +00:00
co-authored by Claude Opus 5
parent 6e24e00ec8
commit bae40bb04e
47 changed files with 702 additions and 277 deletions
+23 -12
View File
@@ -209,28 +209,37 @@ in again while you are running, and `EsmSendsCorrectedCall` sends the call again
in front of the last message when you have corrected it — copy `SM3AB`, fix it in front of the last message when you have corrected it — copy `SM3AB`, fix it
to `SM3ABC`, and the key sends `SM3ABC TU DL1ABC`. to `SM3ABC`, and the key sends `SM3ABC TU DL1ABC`.
### Name, comment, and the two lights ### The boxes a contest shows, and the two lights
Beside the exchange are a Name box and a Comment box, as N1MM has them. Neither The entry window shows the boxes N1MM's entry window shows for that contest and
is part of the exchange, so neither holds up a contact: they fill the log's Name no others. General logging has a Name box and a Comment box; a contest has the
and Comment columns, and a contest that exchanges a name uses that instead. boxes it exchanges. Two of them are for what this station sends — the report and
Neither is in the space walk, as neither is in N1MM's; tab reaches both. The name is filled from the call history file when there is one, the serial number — and both are filled in and stepped over, as they are in
and what the call history says in its UserText column appears under the entry N1MM.
boxes.
Where the other station sends a serial number or a district code depending on
where it is, there is one box for both, again as in N1MM: what is typed goes in
the exchange column, and the number, section or zone column as well, so the
score and the Cabrillo file read it from the same place N1MM does. Sweepstakes
takes its whole exchange in one box — `72 A 74 SCV`, in any order — and logs it
in four columns.
What the call history says in its UserText column appears under the entry boxes.
### Space and tab ### Space and tab
They are not the same key, and N1MM does not treat them as one. They are not the same key, and N1MM does not treat them as one.
Space moves between the callsign box and the exchange boxes and round again, Space moves between the callsign box and the exchange boxes and round again,
stepping over the report boxes, because nobody types 599: leaving the callsign stepping over the report boxes and the sent serial, because nobody types 599 or
box fills them with the report for the mode, 599 on CW and digital, 59 on phone. the number going out: leaving the callsign box fills the reports in for the
mode, 599 on CW and digital, 59 on phone.
It also steps over a box this station is not asked for, such as the state in It also steps over a box this station is not asked for, such as the state in
CQ WW RTTY for a station outside the US and Canada. So in CQ WW, space takes you CQ WW RTTY for a station outside the US and Canada. So in CQ WW, space takes you
from the callsign straight to the zone, and space again brings you back. from the callsign straight to the zone, and space again brings you back.
Tab walks every box in turn — callsign, the exchange including the reports, the Tab walks every box in turn — the callsign and the exchange, the reports
name and the comment — and round again; shift and tab go the other way. N1MM included — and round again; shift and tab go the other way. N1MM
does not touch the tab key at all, so tab there walks its boxes in the order does not touch the tab key at all, so tab there walks its boxes in the order
they are laid out, which is what this does. they are laid out, which is what this does.
@@ -693,7 +702,9 @@ them — `Exchange1` says `SQTC` or `RQTC`, the time, callsign and serial number
of the contact being reported go in the sent report, received report and sent of the contact being reported go in the sent report, received report and sent
number columns, and the series such as `9/10` goes in the misc text column. A number columns, and the series such as `9/10` goes in the misc text column. A
log written by either program therefore opens in the other with its QTCs log written by either program therefore opens in the other with its QTCs
intact, and a QTC row is written to the Cabrillo file as a `QTC:` record. intact, and a QTC row is written to the Cabrillo file as a `QTC:` record. The
log window shows both columns under N1MM's headers for them, `Series` and
`QTC?`, so a QTC row can be told from a contact at a glance.
**Ctrl+Z** opens the QTC window for the station being worked, which is the key **Ctrl+Z** opens the QTC window for the station being worked, which is the key
N1MM uses. The window is laid out the same way: the header, ten lines of time, N1MM uses. The window is laid out the same way: the header, ten lines of time,
+2 -1
View File
@@ -286,7 +286,8 @@ public sealed partial class EntryWindow
for (int at = 0; at < Logging.Entry.Exchange.Count; at++) for (int at = 0; at < Logging.Entry.Exchange.Count; at++)
{ {
ExchangeField field = Logging.Entry.Exchange[at]; ExchangeField field = Logging.Entry.Exchange[at];
if ((field.Kind != ExchangeFieldKind.Number && field.Slot != ExchangeSlot.Exchange1) || if (field.IsSent ||
(field.Kind != ExchangeFieldKind.Number && field.Slot != ExchangeSlot.Exchange1) ||
!int.TryParse(Logging.Entry[at + 1].Trim(), out int number) || number <= 0) !int.TryParse(Logging.Entry[at + 1].Trim(), out int number) || number <= 0)
{ {
continue; continue;
+7 -88
View File
@@ -51,8 +51,6 @@ public sealed partial class EntryWindow : Window
/// network status window. /// network status window.
private string sendingWhat = ""; private string sendingWhat = "";
private TextBlock? callMark; private TextBlock? callMark;
private TextBox? nameBox;
private TextBox? commentBox;
private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) }; private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) };
private readonly Dictionary<Type, Window> openWindows = []; private readonly Dictionary<Type, Window> openWindows = [];
private bool updating; private bool updating;
@@ -224,55 +222,11 @@ public sealed partial class EntryWindow : Window
ExchangeField field = Logging.Entry.Exchange[at]; ExchangeField field = Logging.Entry.Exchange[at];
AddBox(field.Label, field.Width, at + 1); AddBox(field.Label, field.Width, at + 1);
} }
AddNotes();
boxes[0].Focus(); boxes[0].Focus();
SyncBoxes();
Refresh(); Refresh();
} }
/// N1MM keeps a name and a comment beside the exchange, whatever the
/// contest is. Neither is part of the exchange: they fill the log columns
/// of the same names, and a contest that exchanges a name already has one.
private void AddNotes()
{
nameBox = Logging!.Entry.Exchange.Any(f => f.Slot == ExchangeSlot.Name)
? null
: AddNote("Name", 8, EntryGrid.ColumnDefinitions.Count);
commentBox = AddNote("Comment", 12, EntryGrid.ColumnDefinitions.Count);
}
private TextBox AddNote(string label, int width, int index)
{
EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
TextBlock caption = new() { Text = label };
caption.Classes.Add("label");
Grid.SetColumn(caption, index);
Grid.SetRow(caption, 0);
EntryGrid.Children.Add(caption);
TextBox box = new()
{
Width = (width * 13) + 20,
Margin = new Avalonia.Thickness(0, 0, 6, 0),
HorizontalAlignment = HorizontalAlignment.Left,
};
box.Classes.Add("entry");
box.TextChanged += (_, _) => OnNoteChanged();
Grid.SetColumn(box, index);
Grid.SetRow(box, 1);
EntryGrid.Children.Add(box);
return box;
}
private void OnNoteChanged()
{
if (updating || Logging is null)
{
return;
}
Logging.OtherName = nameBox?.Text ?? "";
Logging.Comment = commentBox?.Text ?? "";
}
private void AddBox(string label, int width, int index) private void AddBox(string label, int width, int index)
{ {
EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
@@ -398,7 +352,7 @@ public sealed partial class EntryWindow : Window
e.Handled = true; e.Handled = true;
session.SwapRadio(); session.SwapRadio();
break; break;
case Key.Tab when FocusedIsEntryBox() || FocusedIsNoteBox(): case Key.Tab when FocusedIsEntryBox():
e.Handled = true; e.Handled = true;
MoveToNextBox(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift)); MoveToNextBox(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift));
break; break;
@@ -648,7 +602,6 @@ public sealed partial class EntryWindow : Window
bool filled = TakeFramedCall(); bool filled = TakeFramedCall();
filled |= Logging.FillReports(); filled |= Logging.FillReports();
filled |= Logging.FillFromHistory(); filled |= Logging.FillFromHistory();
filled |= FillNameFromHistory();
if (filled) if (filled)
{ {
SyncBoxes(); SyncBoxes();
@@ -663,26 +616,19 @@ public sealed partial class EntryWindow : Window
FocusEntryBox(); FocusEntryBox();
} }
/// What tab does: every box in turn, reports, name and comment included, /// What tab does: every box in turn, the reports included, and round
/// and round again. N1MM does not touch the tab key, so tab there walks /// again. N1MM does not touch the tab key, so tab there walks the boxes in
/// the boxes in the order they are laid out, and this walks the same ones. /// the order they are laid out, and this walks the same ones.
private void MoveToNextBox(bool forward) private void MoveToNextBox(bool forward)
{ {
if (Logging is null) if (Logging is null)
{ {
return; return;
} }
List<TextBox> all = [.. boxes, .. new[] { nameBox, commentBox }.OfType<TextBox>()]; int at = boxes.FindIndex(box => box.IsFocused);
int at = all.FindIndex(box => box.IsFocused); int next = ((at < 0 ? 0 : at) + (forward ? 1 : -1) + boxes.Count) % boxes.Count;
int next = ((at < 0 ? 0 : at) + (forward ? 1 : -1) + all.Count) % all.Count;
if (next < boxes.Count)
{
Logging.Entry.FocusOn(next); Logging.Entry.FocusOn(next);
FocusEntryBox(); FocusEntryBox();
return;
}
all[next].Focus();
all[next].CaretIndex = all[next].Text?.Length ?? 0;
} }
private void FocusEntryBox() private void FocusEntryBox()
@@ -695,27 +641,8 @@ public sealed partial class EntryWindow : Window
boxes[Logging.Entry.Focus].CaretIndex = boxes[Logging.Entry.Focus].Text?.Length ?? 0; boxes[Logging.Entry.Focus].CaretIndex = boxes[Logging.Entry.Focus].Text?.Length ?? 0;
} }
/// The call history knows the names in a lot of contests, and the name box
/// is where N1MM shows it when the contest does not exchange one.
private bool FillNameFromHistory()
{
if (Logging is null || nameBox is null || Logging.OtherName.Trim().Length > 0)
{
return false;
}
string known = Logging.Session.History.Find(Logging.Entry.Call)?.Name ?? "";
if (known.Length == 0)
{
return false;
}
Logging.OtherName = known;
return true;
}
private bool FocusedIsEntryBox() => boxes.Any(b => b.IsFocused); private bool FocusedIsEntryBox() => boxes.Any(b => b.IsFocused);
private bool FocusedIsNoteBox() => nameBox?.IsFocused == true || commentBox?.IsFocused == true;
/// The buttons N1MM puts under its function keys. Each one does what its /// The buttons N1MM puts under its function keys. Each one does what its
/// key does, so there is one place deciding what happens. /// key does, so there is one place deciding what happens.
private void OnStopSending(object? sender, RoutedEventArgs e) private void OnStopSending(object? sender, RoutedEventArgs e)
@@ -798,14 +725,6 @@ public sealed partial class EntryWindow : Window
{ {
boxes[at].Text = Logging.Entry[at]; boxes[at].Text = Logging.Entry[at];
} }
if (nameBox is not null)
{
nameBox.Text = Logging.OtherName;
}
if (commentBox is not null)
{
commentBox.Text = Logging.Comment;
}
updating = false; updating = false;
} }
+14
View File
@@ -29,6 +29,20 @@ public interface Contest
/// contests skip nothing. /// contests skip nothing.
bool SkipsField(ExchangeField field, CountryLookup? their) => false; bool SkipsField(ExchangeField field, CountryLookup? their) => false;
/// Spreads what was typed in the one exchange box across the other log
/// columns. A lot of contests are a serial number from one station and a
/// district code from another, and N1MM gives them a single box and sorts
/// the value into the section, number or zone column from there. The
/// contact already holds the typed text in `Exchange1` and the country of
/// the call. Contests with a box per column change nothing.
Qso FromExchange1(Qso qso, string typed) => qso;
/// The exchange columns of the log window. N1MM keeps a column list per
/// contest that is not the entry box list: Sweepstakes types the whole
/// exchange in one box and logs it in four columns. The columns follow the
/// boxes where a contest does not say otherwise.
IReadOnlyList<ExchangeField> LogFieldsFor(StationInfo me) => ExchangeFieldsFor(me);
/// Up to three names, in the order the score summary shows them. /// Up to three names, in the order the score summary shows them.
IReadOnlyList<string> MultiplierNames { get; } IReadOnlyList<string> MultiplierNames { get; }
+5
View File
@@ -10,6 +10,10 @@ public sealed record ExchangeField(
/// How wide the box needs to be, in characters. /// How wide the box needs to be, in characters.
public int Width { get; init; } = DefaultWidth(Kind); public int Width { get; init; } = DefaultWidth(Kind);
/// True for a box holding what this station sends. N1MM shows those boxes,
/// fills them in and steps over them, so nothing here waits for one.
public bool IsSent => Slot is ExchangeSlot.SentReport or ExchangeSlot.SentNumber;
private static int DefaultWidth(ExchangeFieldKind kind) => kind switch private static int DefaultWidth(ExchangeFieldKind kind) => kind switch
{ {
ExchangeFieldKind.Report => 3, ExchangeFieldKind.Report => 3,
@@ -22,6 +26,7 @@ public sealed record ExchangeField(
ExchangeFieldKind.Precedence => 1, ExchangeFieldKind.Precedence => 1,
ExchangeFieldKind.Check => 2, ExchangeFieldKind.Check => 2,
ExchangeFieldKind.Power => 4, ExchangeFieldKind.Power => 4,
ExchangeFieldKind.SweepstakesExchange => 16,
_ => 8, _ => 8,
}; };
} }
+4
View File
@@ -15,4 +15,8 @@ public enum ExchangeFieldKind
Power, Power,
Precedence, Precedence,
Check, Check,
/// The whole Sweepstakes exchange in one box: number, precedence, check
/// and section, typed in any order. N1MM has one box for it too.
SweepstakesExchange,
} }
+2
View File
@@ -4,6 +4,8 @@ namespace Nonemm.Contests;
/// columns so an exchange lands in the same place N1MM would put it. /// columns so an exchange lands in the same place N1MM would put it.
public enum ExchangeSlot public enum ExchangeSlot
{ {
SentReport,
SentNumber,
ReceivedReport, ReceivedReport,
SerialNumber, SerialNumber,
Zone, Zone,
+2
View File
@@ -10,6 +10,8 @@ public static class QsoExchange
/// box it holds nothing for. /// box it holds nothing for.
public static string ValueOf(Qso qso, ExchangeSlot slot) => slot switch public static string ValueOf(Qso qso, ExchangeSlot slot) => slot switch
{ {
ExchangeSlot.SentReport => qso.SentReport,
ExchangeSlot.SentNumber => Digits(qso.SentNumber),
ExchangeSlot.ReceivedReport => qso.ReceivedReport, ExchangeSlot.ReceivedReport => qso.ReceivedReport,
ExchangeSlot.SerialNumber => Digits(qso.ReceivedNumber), ExchangeSlot.SerialNumber => Digits(qso.ReceivedNumber),
ExchangeSlot.Zone => Digits(qso.Zone), ExchangeSlot.Zone => Digits(qso.Zone),
+2 -1
View File
@@ -20,7 +20,8 @@ public sealed class AllAsian : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Age", ExchangeSlot.Exchange1, ExchangeFieldKind.Number) { Width = 3 }, new ExchangeField("Age", ExchangeSlot.Exchange1, ExchangeFieldKind.Number) { Width = 3 },
]; ];
+20 -10
View File
@@ -17,19 +17,29 @@ public sealed class Arrl10Metre : Contest
public string CabrilloName => "ARRL-10"; public string CabrilloName => "ARRL-10";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => /// One box takes the state, province or serial number the other station
/// sends, as N1MM has it. The sent serial box is there only where this
/// station sends a number rather than a state.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => SendsState(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch /// A number is a serial and anything else is a state or province.
{ public Qso FromExchange1(Qso qso, string typed) =>
ExchangeSlot.Section => their is not null && !SendsState(their.Entity.PrimaryPrefix), int.TryParse(typed, out int number)
ExchangeSlot.Exchange1 => their is not null && SendsState(their.Entity.PrimaryPrefix), ? qso with { ReceivedNumber = number, Section = "" }
_ => false, : qso with { ReceivedNumber = 0, Section = typed.Trim().ToUpperInvariant() };
};
public IReadOnlyList<string> MultiplierNames => ["States", "Countries"]; public IReadOnlyList<string> MultiplierNames => ["States", "Countries"];
+8 -4
View File
@@ -21,12 +21,16 @@ public sealed class ArrlDx : Contest
public string CabrilloName => mode == ModeCategory.Cw ? "ARRL-DX-CW" : "ARRL-DX-SSB"; public string CabrilloName => mode == ModeCategory.Cw ? "ARRL-DX-CW" : "ARRL-DX-SSB";
/// A W or VE operator copies the power the DX station sends, which goes in
/// the power column; a DX operator copies a state or province, which goes
/// in the section column. Both are where N1MM keeps them.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
IsNorthAmericanHome(me) IsNorthAmericanHome(me)
? new ExchangeField("Pwr", ExchangeSlot.Section, ExchangeFieldKind.Power) ? new ExchangeField("Pwr", ExchangeSlot.Power, ExchangeFieldKind.Power)
: new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince), : new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
]; ];
public IReadOnlyList<string> MultiplierNames => ["Mults"]; public IReadOnlyList<string> MultiplierNames => ["Mults"];
@@ -74,7 +78,7 @@ public sealed class ArrlDx : Contest
[ [
new CabrilloField(qso.Call.Text, 13), new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3), new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section, 6), new CabrilloField(IsNorthAmericanHome(me) ? qso.Power : qso.Section, 6),
]); ]);
private static bool IsNorthAmericanHome(StationInfo me) => IsNorthAmerican(me.CountryPrefix); private static bool IsNorthAmericanHome(StationInfo me) => IsNorthAmerican(me.CountryPrefix);
+4 -2
View File
@@ -25,8 +25,10 @@ public sealed class BartgSprint : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("RcvNr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
new ExchangeField("Time", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 }, new ExchangeField("Time", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 },
]; ];
+11 -9
View File
@@ -24,19 +24,21 @@ public sealed class CqOneSixty : Contest
public string CabrilloName => mode == ModeCategory.Cw ? "CQ-160-CW" : "CQ-160-SSB"; public string CabrilloName => mode == ModeCategory.Cw ? "CQ-160-CW" : "CQ-160-SSB";
/// One box takes the state, province or CQ zone the other station sends,
/// as N1MM has it.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Zone", ExchangeSlot.Exchange1, ExchangeFieldKind.CqZone), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch /// A number is a CQ zone and stays in the exchange column; anything else
{ /// is a state or province and goes in the section column as well.
ExchangeSlot.Section => their is not null && !IsUsOrCanadian(their.Entity.PrimaryPrefix), public Qso FromExchange1(Qso qso, string typed) =>
ExchangeSlot.Exchange1 => their is not null && IsUsOrCanadian(their.Entity.PrimaryPrefix), int.TryParse(typed, out _)
_ => false, ? qso with { Section = "" }
}; : qso with { Section = typed.Trim().ToUpperInvariant() };
public IReadOnlyList<string> MultiplierNames => ["States", "Countries"]; public IReadOnlyList<string> MultiplierNames => ["States", "Countries"];
+7 -5
View File
@@ -39,14 +39,16 @@ public sealed class CqWorldWide : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRtty public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRtty
? ?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince, IsRequired: false), new ExchangeField("CQ-Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone),
new ExchangeField("State/RAC", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince, IsRequired: false),
] ]
: :
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("CQ-Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone),
]; ];
/// On RTTY the exchange carries a state or province, but only from the US /// On RTTY the exchange carries a state or province, but only from the US
+4 -2
View File
@@ -33,8 +33,10 @@ public sealed class CqWpx : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("SntNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("RcvNR", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
]; ];
public IReadOnlyList<string> MultiplierNames => ["Prefixes"]; public IReadOnlyList<string> MultiplierNames => ["Prefixes"];
+11 -3
View File
@@ -31,10 +31,18 @@ public sealed class EuDxContest : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Code", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 5 },
]; ];
/// A station in the union sends a district code, which goes in the section
/// column; everyone else sends a CQ zone, which goes in the zone column.
public Qso FromExchange1(Qso qso, string typed) =>
IsUnion(qso.CountryPrefix)
? qso with { Section = typed.Trim().ToUpperInvariant() }
: qso with { Zone = int.TryParse(typed, out int zone) ? zone : 0 };
public IReadOnlyList<string> MultiplierNames => ["Codes", "Countries"]; public IReadOnlyList<string> MultiplierNames => ["Codes", "Countries"];
public DupeScope DupeScope => DupeScope.PerBandAndMode; public DupeScope DupeScope => DupeScope.PerBandAndMode;
@@ -90,7 +98,7 @@ public sealed class EuDxContest : Contest
[ [
new CabrilloField(qso.Call.Text, 13), new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.ReceivedReport, 3), new CabrilloField(qso.ReceivedReport, 3),
new CabrilloField(qso.Section, 5), new CabrilloField(qso.Section.Length > 0 ? qso.Section : qso.Exchange1, 5),
]); ]);
private static bool IsUnion(string countryPrefix) => Union.Contains(countryPrefix); private static bool IsUnion(string countryPrefix) => Union.Contains(countryPrefix);
@@ -15,7 +15,9 @@ public sealed class FieldDayRegionOne : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
]; ];
+27 -10
View File
@@ -27,19 +27,36 @@ public sealed class FrenchRef : Contest
public string CabrilloName => mode == ModeCategory.Cw ? "REF-CW" : "REF-SSB"; public string CabrilloName => mode == ModeCategory.Cw ? "REF-CW" : "REF-SSB";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => /// A French operator works both sides and gets one box for whichever the
/// other station sends; everyone else works French stations alone, so the
/// box is the department and this station sends a serial number.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsFrench(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Dept", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Department", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch public Qso FromExchange1(Qso qso, string typed) =>
{ qso.CountryPrefix == "F"
ExchangeSlot.Section => their is not null && their.Entity.PrimaryPrefix != "F", ? qso with { Section = typed.Trim().ToUpperInvariant() }
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "F", : qso with { ReceivedNumber = int.TryParse(typed, out int number) ? number : 0 };
_ => false,
}; /// N1MM's log for this contest keeps what the one box was sorted into in a
/// column of its own beside the exchange.
public IReadOnlyList<ExchangeField> LogFieldsFor(StationInfo me) =>
[
.. ExchangeFieldsFor(me),
new ExchangeField("Sect", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
];
public IReadOnlyList<string> MultiplierNames => ["Mults"]; public IReadOnlyList<string> MultiplierNames => ["Mults"];
+2 -1
View File
@@ -14,7 +14,8 @@ public sealed class GeneralLogging : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Name", ExchangeSlot.Name, ExchangeFieldKind.Text, IsRequired: false) { Width = 10 }, new ExchangeField("Name", ExchangeSlot.Name, ExchangeFieldKind.Text, IsRequired: false) { Width = 10 },
new ExchangeField("Comment", ExchangeSlot.Comment, ExchangeFieldKind.Text, IsRequired: false) { Width = 20 }, new ExchangeField("Comment", ExchangeSlot.Comment, ExchangeFieldKind.Text, IsRequired: false) { Width = 20 },
]; ];
+3 -2
View File
@@ -17,8 +17,9 @@ public sealed class IaruHf : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Zone/HQ", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("ITU-Zone", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 },
]; ];
public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"]; public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"];
+3 -1
View File
@@ -19,7 +19,9 @@ public sealed class Iota : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
new ExchangeField("IOTA", ExchangeSlot.Section, ExchangeFieldKind.Text, IsRequired: false) new ExchangeField("IOTA", ExchangeSlot.Section, ExchangeFieldKind.Text, IsRequired: false)
{ {
+16 -8
View File
@@ -15,18 +15,26 @@ public sealed class MexicoRtty : Contest
public string CabrilloName => "XE-RTTY"; public string CabrilloName => "XE-RTTY";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsMexican(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("SentNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch /// A Mexican station sends a state, which goes in the section column; the
/// serial everyone else sends stays in the exchange column.
public Qso FromExchange1(Qso qso, string typed) => qso with
{ {
ExchangeSlot.Section => their is not null && !IsMexican(their.Entity.PrimaryPrefix), Section = IsMexican(qso.CountryPrefix) ? typed.Trim().ToUpperInvariant() : "",
ExchangeSlot.Exchange1 => their is not null && IsMexican(their.Entity.PrimaryPrefix),
_ => false,
}; };
public IReadOnlyList<string> MultiplierNames => ["Countries", "States"]; public IReadOnlyList<string> MultiplierNames => ["Countries", "States"];
@@ -25,7 +25,7 @@ public sealed class NorthAmericanQsoParty : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("Name", ExchangeSlot.Name, ExchangeFieldKind.Text) { Width = 10 }, new ExchangeField("Name", ExchangeSlot.Name, ExchangeFieldKind.Text) { Width = 10 },
new ExchangeField("S/P/C", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 }, new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 },
]; ];
public IReadOnlyList<string> MultiplierNames => ["Mults"]; public IReadOnlyList<string> MultiplierNames => ["Mults"];
@@ -44,7 +44,7 @@ public sealed class NorthAmericanQsoParty : Contest
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso) public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
{ {
string band = qso.Band?.Name ?? ""; string band = qso.Band?.Name ?? "";
string exchange = qso.Qso.Exchange1.Trim().ToUpperInvariant(); string exchange = qso.Qso.Section.Trim().ToUpperInvariant();
if (StatesAndProvinces.IsStateOrProvince(exchange)) if (StatesAndProvinces.IsStateOrProvince(exchange))
{ {
return [new Multiplier(1, exchange, band)]; return [new Multiplier(1, exchange, band)];
@@ -66,7 +66,7 @@ public sealed class NorthAmericanQsoParty : Contest
[ [
new CabrilloField(qso.Call.Text, 13), new CabrilloField(qso.Call.Text, 13),
new CabrilloField(qso.Name, 10), new CabrilloField(qso.Name, 10),
new CabrilloField(qso.Exchange1, 6), new CabrilloField(qso.Section, 6),
]); ]);
private string ModeLabel() => mode switch private string ModeLabel() => mode switch
+4 -2
View File
@@ -20,8 +20,10 @@ public sealed class OceaniaDx : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("SntNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("RcvNR", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
]; ];
public IReadOnlyList<string> MultiplierNames => ["Prefixes"]; public IReadOnlyList<string> MultiplierNames => ["Prefixes"];
+19 -11
View File
@@ -22,21 +22,29 @@ public sealed class OkOmDx : Contest
public string CabrilloName => "OK-OM-DX"; public string CabrilloName => "OK-OM-DX";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => /// An OK or OM operator works everybody, so the one box takes a county or
/// a serial number; everyone else works OK and OM stations alone and sends
/// a serial number of their own.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsOkOm(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 4 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SntNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 },
]; ];
/// An OK or OM station sends a county and no serial; everyone else the /// A county goes in the section column as well, which is where the
/// other way round, so one of the two boxes is stepped over. /// multiplier is read from; a serial number stays in the exchange column.
public bool SkipsField(ExchangeField field, CountryLookup? their) => public Qso FromExchange1(Qso qso, string typed) => qso with
field.Slot switch
{ {
ExchangeSlot.Section => their is not null && !IsOkOm(their.Entity.PrimaryPrefix), Section = IsOkOm(qso.CountryPrefix) ? typed.Trim().ToUpperInvariant() : "",
ExchangeSlot.Exchange1 => their is not null && IsOkOm(their.Entity.PrimaryPrefix),
_ => false,
}; };
public IReadOnlyList<string> MultiplierNames => ["Mults"]; public IReadOnlyList<string> MultiplierNames => ["Mults"];
+22 -3
View File
@@ -13,12 +13,31 @@ public sealed class RttyRoundup : Contest
public string CabrilloName => "ARRL-RTTY"; public string CabrilloName => "ARRL-RTTY";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => /// A North American station sends a state or province and no serial;
/// everyone else sends a serial number, and N1MM shows the sent serial box
/// only for them.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => me.State.Length > 0
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("S/P/Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
]; ];
/// A number is a serial; anything else is a state or province, which goes
/// in the section column as well.
public Qso FromExchange1(Qso qso, string typed) =>
int.TryParse(typed, out int number)
? qso with { ReceivedNumber = number, Section = "" }
: qso with { ReceivedNumber = 0, Section = typed.Trim().ToUpperInvariant() };
public IReadOnlyList<string> MultiplierNames => ["Mults"]; public IReadOnlyList<string> MultiplierNames => ["Mults"];
public DupeScope DupeScope => DupeScope.PerBand; public DupeScope DupeScope => DupeScope.PerBand;
@@ -13,12 +13,28 @@ public sealed class RussianDistrictAward : Contest
public string CabrilloName => "RDAC"; public string CabrilloName => "RDAC";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRussian(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("RDA", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 5 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 5 },
]; ];
/// A district goes in the section column, where the multiplier is read
/// from; everyone else sends a serial number.
public Qso FromExchange1(Qso qso, string typed) =>
IsRussian(qso.CountryPrefix)
? qso with { Section = typed.Trim().ToUpperInvariant() }
: qso with { ReceivedNumber = int.TryParse(typed, out int number) ? number : 0 };
/// N1MM names a country multiplier for this contest and never counts one, /// N1MM names a country multiplier for this contest and never counts one,
/// and the score summary shows both, so the districts stay in the second /// and the score summary shows both, so the districts stay in the second
/// column here too. /// column here too.
@@ -59,4 +75,7 @@ public sealed class RussianDistrictAward : Contest
]); ]);
private static string District(Qso qso) => qso.Section.Trim().ToUpperInvariant(); private static string District(Qso qso) => qso.Section.Trim().ToUpperInvariant();
private static bool IsRussian(string countryPrefix) =>
countryPrefix is "UA" or "UA2" or "UA9";
} }
+19 -3
View File
@@ -15,12 +15,28 @@ public sealed class RussianDxRtty : Contest
public string CabrilloName => "RADIO-WW-RTTY"; public string CabrilloName => "RADIO-WW-RTTY";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRussian(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Oblast/Zone", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]; ];
/// A Russian station sends an oblast, which goes in the section column
/// where the multiplier is read from; everyone else sends a CQ zone.
public Qso FromExchange1(Qso qso, string typed) => qso with
{
Section = typed.Trim().ToUpperInvariant(),
};
public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"]; public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"];
public DupeScope DupeScope => DupeScope.PerBand; public DupeScope DupeScope => DupeScope.PerBand;
+4 -2
View File
@@ -15,8 +15,10 @@ public sealed class SartgRtty : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("SntNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("RcvNr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
]; ];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Areas"]; public IReadOnlyList<string> MultiplierNames => ["Countries", "Areas"];
+27 -3
View File
@@ -17,12 +17,36 @@ public sealed class Sweepstakes : Contest
public string CabrilloName => mode == ModeCategory.Cw ? "ARRL-SS-CW" : "ARRL-SS-SSB"; public string CabrilloName => mode == ModeCategory.Cw ? "ARRL-SS-CW" : "ARRL-SS-SSB";
/// The whole exchange goes in one box, as it does in N1MM: the number,
/// precedence, check and section are read back out of what was typed.
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Snt Nr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField(
"Nr Prec CK Section",
ExchangeSlot.Exchange1,
ExchangeFieldKind.SweepstakesExchange),
];
public Qso FromExchange1(Qso qso, string typed)
{
SweepstakesExchange exchange = SweepstakesExchange.Parse(typed);
return qso with
{
ReceivedNumber = exchange.Number,
Precedence = exchange.Precedence,
Check = exchange.Check,
Section = exchange.Section,
};
}
/// The log keeps the four parts in four columns, as N1MM's does.
public IReadOnlyList<ExchangeField> LogFieldsFor(StationInfo me) =>
[
new ExchangeField("NR", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
new ExchangeField("Prec", ExchangeSlot.Precedence, ExchangeFieldKind.Precedence), new ExchangeField("Prec", ExchangeSlot.Precedence, ExchangeFieldKind.Precedence),
new ExchangeField("Ck", ExchangeSlot.Check, ExchangeFieldKind.Check), new ExchangeField("CK", ExchangeSlot.Check, ExchangeFieldKind.Check),
new ExchangeField("Sec", ExchangeSlot.Section, ExchangeFieldKind.ArrlSection), new ExchangeField("Sect", ExchangeSlot.Section, ExchangeFieldKind.ArrlSection),
]; ];
public IReadOnlyList<string> MultiplierNames => ["Sections"]; public IReadOnlyList<string> MultiplierNames => ["Sections"];
@@ -0,0 +1,109 @@
using Nonemm.Contests.Multipliers;
namespace Nonemm.Contests.Rules;
/// The four parts of a Sweepstakes exchange, read out of the one box the
/// operator types them into: `72 A 74 SCV`, or `72A 74SCV`, or any other order.
/// N1MM reads the same box the same way, from the last word back, so a part
/// typed again replaces nothing that was already found.
public sealed record SweepstakesExchange(int Number, string Precedence, int Check, string Section)
{
private const int HighestNumber = 10000;
private const int HighestCheck = 99;
private static readonly IReadOnlySet<string> Precedences =
new HashSet<string> { "A", "B", "Q", "U", "M", "S" };
public static SweepstakesExchange Parse(string typed)
{
string rest = typed.Trim().ToUpperInvariant();
string number = "";
string check = "";
string section = "";
string precedence = "";
bool foundSection = false;
bool foundNumber = false;
while (rest.Trim().Length > 0)
{
string word = LastWord(rest);
rest = AllButLastWord(rest);
string before = LastWord(rest);
// "72 A" and "74 SCV" are one part typed with a space in it
if ((word.Length == 1 && char.IsAsciiLetter(word[0]) && IsNumber(before))
|| (word.Length is >= 2 and < 4 && word.All(char.IsAsciiLetter) && IsNumber(before)))
{
word = before + word;
rest = AllButLastWord(rest);
}
if (word.Length > 1 && !IsNumber(word[^1..]) && IsNumber(word[..^1]))
{
if (!foundNumber)
{
number = word[..^1];
foundNumber = true;
}
precedence = precedence.Length == 0 ? PrecedenceFrom(word[^1..]) : precedence;
}
else if (word.Length > 3 && !IsNumber(word) && IsNumber(word[..2]) && word[2..].All(char.IsAsciiLetter))
{
check = check.Length == 0 ? word[..2] : check;
(section, foundSection) = SectionFrom(word[2..], section, foundSection);
}
else if (word.Length > 2 && !IsNumber(word) && IsNumber(word[..1]) && word[1..].All(char.IsAsciiLetter))
{
check = check.Length == 0 ? word[..1] : check;
(section, foundSection) = SectionFrom(word[1..], section, foundSection);
}
else if (IsNumber(word) && ((word.Length == 1 && word != "0") || word.Length > 2))
{
number = foundNumber ? number : word;
foundNumber = true;
}
else if (IsNumber(word) && check.Length > 0 && !foundNumber)
{
number = number.Length == 0 ? word : number;
}
else if (IsNumber(word))
{
check = check.Length == 0 ? word : check;
}
else if (word.Length > 1)
{
(section, foundSection) = SectionFrom(word, section, foundSection);
}
else if (precedence.Length == 0)
{
precedence = PrecedenceFrom(word);
}
}
return new SweepstakesExchange(
Math.Min(Value(number), HighestNumber),
precedence,
Math.Min(Value(check), HighestCheck),
section);
}
private static (string Section, bool Found) SectionFrom(string word, string section, bool found) =>
found || !ArrlSections.IsSection(word) ? (section, found) : (word, true);
private static string PrecedenceFrom(string word) => Precedences.Contains(word) ? word : "";
private static string LastWord(string text)
{
string trimmed = text.Trim();
int space = trimmed.LastIndexOf(' ');
return space < 0 ? trimmed : trimmed[(space + 1)..];
}
private static string AllButLastWord(string text)
{
string trimmed = text.Trim();
int space = trimmed.LastIndexOf(' ');
return space < 0 ? "" : trimmed[..space];
}
private static bool IsNumber(string text) => text.Length > 0 && text.All(char.IsAsciiDigit);
private static int Value(string text) => int.TryParse(text, out int number) ? number : 0;
}
+26 -10
View File
@@ -15,19 +15,35 @@ public sealed class UkrainianDx : Contest
public string CabrilloName => "UKRAINIAN-DX"; public string CabrilloName => "UKRAINIAN-DX";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsUkrainian(me.CountryPrefix)
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Oblast", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SntNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch /// A number is the serial everyone outside Ukraine sends; anything else is
{ /// an oblast, which goes in the section column as well.
ExchangeSlot.Section => their is not null && !IsUkrainian(their.Entity.PrimaryPrefix), public Qso FromExchange1(Qso qso, string typed) =>
ExchangeSlot.Exchange1 => their is not null && IsUkrainian(their.Entity.PrimaryPrefix), int.TryParse(typed, out int number)
_ => false, ? qso with { ReceivedNumber = number, Section = "" }
}; : qso with { ReceivedNumber = 0, Section = typed.Trim().ToUpperInvariant() };
/// N1MM's log for this contest keeps what the one box was sorted into in a
/// column of its own beside the exchange.
public IReadOnlyList<ExchangeField> LogFieldsFor(StationInfo me) =>
[
.. ExchangeFieldsFor(me),
new ExchangeField("Sect", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"]; public IReadOnlyList<string> MultiplierNames => ["Countries", "Oblasts"];
+3 -1
View File
@@ -49,7 +49,9 @@ public sealed class Wae : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("SentNR", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
]; ];
+24 -8
View File
@@ -15,20 +15,36 @@ public sealed class YoDxHf : Contest
public string CabrilloName => "YO-DX-HF"; public string CabrilloName => "YO-DX-HF";
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => me.CountryPrefix == "YO"
?
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 }, new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number), new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]
:
[
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Nr", ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Exch", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
]; ];
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch /// A YO station sends a county, which goes in the section column where the
/// multiplier is read from; everyone else sends a serial number.
public Qso FromExchange1(Qso qso, string typed) => qso with
{ {
ExchangeSlot.Section => their is not null && their.Entity.PrimaryPrefix != "YO", Section = qso.CountryPrefix == "YO" ? typed.Trim().ToUpperInvariant() : "",
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "YO",
_ => false,
}; };
/// N1MM's log for this contest keeps what the one box was sorted into in a
/// column of its own beside the exchange.
public IReadOnlyList<ExchangeField> LogFieldsFor(StationInfo me) =>
[
.. ExchangeFieldsFor(me),
new ExchangeField("Sect", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
];
public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"]; public IReadOnlyList<string> MultiplierNames => ["Countries", "Counties"];
public DupeScope DupeScope => DupeScope.PerBandAndMode; public DupeScope DupeScope => DupeScope.PerBandAndMode;
+2 -1
View File
@@ -18,7 +18,8 @@ public sealed class Yota : Contest
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
[ [
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
new ExchangeField("Age", ExchangeSlot.Exchange1, ExchangeFieldKind.Number) { Width = 3 }, new ExchangeField("Age", ExchangeSlot.Exchange1, ExchangeFieldKind.Number) { Width = 3 },
]; ];
@@ -281,10 +281,11 @@ public sealed class UserDefinedContest : Contest
/// The names are N1MM's `LogItem.LogItemNameType` members, which is what a /// The names are N1MM's `LogItem.LogItemNameType` members, which is what a
/// published `.udc` file holds. `SectText` is not one of them but files in /// published `.udc` file holds. `SectText` is not one of them but files in
/// the wild use it. The sent-side boxes — `SNTText`, `SntNrText` — are not /// the wild use it.
/// exchange boxes here and fall through.
private static ExchangeField? BoxToField(string box, string label) => box.ToUpperInvariant() switch private static ExchangeField? BoxToField(string box, string label) => box.ToUpperInvariant() switch
{ {
"SNTTEXT" => new ExchangeField(label, ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
"SNTNRTEXT" => new ExchangeField(label, ExchangeSlot.SentNumber, ExchangeFieldKind.Number, IsRequired: false),
"RCVTEXT" => new ExchangeField(label, ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report), "RCVTEXT" => new ExchangeField(label, ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
"RCVNRTEXT" => new ExchangeField(label, ExchangeSlot.SerialNumber, ExchangeFieldKind.Number), "RCVNRTEXT" => new ExchangeField(label, ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
"EXCHANGE1TEXT" => new ExchangeField(label, ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 }, "EXCHANGE1TEXT" => new ExchangeField(label, ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
+2 -1
View File
@@ -75,6 +75,7 @@ public sealed class AvailableStations
return ExchangeSlots.ApplyHistory( return ExchangeSlots.ApplyHistory(
CountryFields.Apply(qso, position.Session.Countries), CountryFields.Apply(qso, position.Session.Countries),
position.Session.History, position.Session.History,
position.Entry.Exchange); position.Contest,
position.Me);
} }
} }
+7 -4
View File
@@ -65,9 +65,9 @@ public sealed class EntryFields
public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1); public void FocusOn(int field) => Focus = Math.Clamp(field, 0, FieldCount - 1);
/// What space and tab do: move to the next box the operator types in, and /// What space and tab do: move to the next box the operator types in, and
/// round to the callsign again from the last one. The report boxes are /// round to the callsign again from the last one. The report boxes and the
/// stepped over, because nobody types 599 — N1MM steps over them too, and /// sent serial are stepped over, because nobody types 599 or the number
/// fills them in instead. /// they are sending — N1MM steps over the same three and fills them in.
public void Advance() => Focus = Next(1); public void Advance() => Focus = Next(1);
public void Retreat() => Focus = Next(-1); public void Retreat() => Focus = Next(-1);
@@ -78,7 +78,7 @@ public sealed class EntryFields
for (int step = 0; step < FieldCount; step++) for (int step = 0; step < FieldCount; step++)
{ {
at = (at + by + FieldCount) % FieldCount; at = (at + by + FieldCount) % FieldCount;
if (at == 0 || Exchange[at - 1].Kind != ExchangeFieldKind.Report) if (at == 0 || !IsFilledIn(Exchange[at - 1]))
{ {
return at; return at;
} }
@@ -86,6 +86,9 @@ public sealed class EntryFields
return Focus; return Focus;
} }
private static bool IsFilledIn(ExchangeField field) =>
field.IsSent || field.Kind == ExchangeFieldKind.Report;
/// True when everything the contest asks for has been typed. /// True when everything the contest asks for has been typed.
public bool IsComplete => public bool IsComplete =>
Call.Trim().Length > 0 && Call.Trim().Length > 0 &&
+27 -6
View File
@@ -10,6 +10,9 @@ public static class ExchangeSlots
{ {
public static Qso Apply(Qso qso, ExchangeSlot slot, string value) => slot switch public static Qso Apply(Qso qso, ExchangeSlot slot, string value) => slot switch
{ {
// an empty sent box leaves what the contest filled in
ExchangeSlot.SentReport => value.Length == 0 ? qso : qso with { SentReport = value },
ExchangeSlot.SentNumber => Number(value) == 0 ? qso : qso with { SentNumber = Number(value) },
ExchangeSlot.ReceivedReport => qso with { ReceivedReport = value }, ExchangeSlot.ReceivedReport => qso with { ReceivedReport = value },
ExchangeSlot.SerialNumber => qso with { ReceivedNumber = Number(value) }, ExchangeSlot.SerialNumber => qso with { ReceivedNumber = Number(value) },
ExchangeSlot.Zone => qso with { Zone = Number(value) }, ExchangeSlot.Zone => qso with { Zone = Number(value) },
@@ -28,7 +31,12 @@ public static class ExchangeSlots
/// What the call history file says this station sends in one exchange box, /// What the call history file says this station sends in one exchange box,
/// or an empty string when the file holds nothing for it. /// or an empty string when the file holds nothing for it.
public static string ValueFrom(CallHistoryEntry known, ExchangeField field) => field.Slot switch public static string ValueFrom(CallHistoryEntry known, ExchangeField field) =>
field.Kind == ExchangeFieldKind.SweepstakesExchange
? SweepstakesText(known)
: FromSlot(known, field);
private static string FromSlot(CallHistoryEntry known, ExchangeField field) => field.Slot switch
{ {
ExchangeSlot.Name => known.Name, ExchangeSlot.Name => known.Name,
// a file for a section contest often carries the state instead // a file for a section contest often carries the state instead
@@ -45,25 +53,38 @@ public static class ExchangeSlots
_ => "", _ => "",
}; };
/// What the file holds for a Sweepstakes station, put together the way
/// N1MM puts it: one box takes the whole exchange, and a file carries the
/// check and the section of it. The number and the precedence are left for
/// the operator to copy.
private static string SweepstakesText(CallHistoryEntry known)
{
string section = known.Section.Length > 0 ? known.Section : known.State;
return section.Length == 0 ? "" : $"{known.Check:00}{section}";
}
/// Fills the exchange the call history file published for this station into /// Fills the exchange the call history file published for this station into
/// a contact. Used where nobody has copied the exchange yet: a spotted /// a contact. Used where nobody has copied the exchange yet: a spotted
/// station the operator has not worked. /// station the operator has not worked.
public static Qso ApplyHistory(Qso qso, CallHistory history, IReadOnlyList<ExchangeField> exchange) public static Qso ApplyHistory(Qso qso, CallHistory history, Contest contest, StationInfo me)
{ {
CallHistoryEntry? known = history.Find(qso.Call.Text); CallHistoryEntry? known = history.Find(qso.Call.Text);
if (known is null) if (known is null)
{ {
return qso; return qso;
} }
foreach (ExchangeField field in exchange) string exchange1 = "";
foreach (ExchangeField field in contest.ExchangeFieldsFor(me))
{ {
string value = ValueFrom(known, field); string value = ValueFrom(known, field);
if (value.Length > 0) if (value.Length == 0)
{ {
continue;
}
qso = Apply(qso, field.Slot, value); qso = Apply(qso, field.Slot, value);
exchange1 = field.Slot == ExchangeSlot.Exchange1 ? value : exchange1;
} }
} return exchange1.Length > 0 ? contest.FromExchange1(qso, exchange1) : qso;
return qso;
} }
private static string Digits(int value) => value > 0 ? value.ToString() : ""; private static string Digits(int value) => value > 0 ? value.ToString() : "";
+31 -3
View File
@@ -16,6 +16,11 @@ public sealed class OperatingPosition
Session = session; Session = session;
RadioNumber = radioNumber; RadioNumber = radioNumber;
Entry = new EntryFields(session.Contest.ExchangeFieldsFor(session.Me)); Entry = new EntryFields(session.Contest.ExchangeFieldsFor(session.Me));
FillSentExchange();
// N1MM fills the sent serial in again on every entry window when a
// contact is logged, so the other radio is not left showing the number
// that has just gone
Session.Logged += (_, _) => FillSentExchange();
} }
public ContestSession Session { get; } public ContestSession Session { get; }
@@ -38,8 +43,9 @@ public sealed class OperatingPosition
public bool IsRunning { get; set; } public bool IsRunning { get; set; }
/// The other operator's name and a note about the contact. Neither is part /// The other operator's name and a note about the contact. Neither is part
/// of any exchange — N1MM keeps both beside the exchange boxes and writes /// of any exchange: the name is what a contact being quick edited already
/// them into the log — so neither counts towards a complete exchange. /// held, and the note is what Ctrl+N asks for. Neither counts towards a
/// complete exchange.
public string OtherName { get; set; } = ""; public string OtherName { get; set; } = "";
public string Comment { get; set; } = ""; public string Comment { get; set; } = "";
@@ -62,8 +68,13 @@ public sealed class OperatingPosition
public void Tune(Frequency frequency, Mode? mode = null, Frequency? transmitFrequency = null) public void Tune(Frequency frequency, Mode? mode = null, Frequency? transmitFrequency = null)
{ {
bool modeChanged = mode is not null && mode.Category != Mode.Category;
Frequency = frequency; Frequency = frequency;
Mode = mode ?? Mode; Mode = mode ?? Mode;
if (modeChanged)
{
FillSentExchange();
}
TransmitFrequency = transmitFrequency ?? TransmitFrequency; TransmitFrequency = transmitFrequency ?? TransmitFrequency;
Session.NotifyChanged(); Session.NotifyChanged();
} }
@@ -209,6 +220,7 @@ public sealed class OperatingPosition
StampSentTime(); StampSentTime();
Qso stored = Session.Add(Noted(BuildQso(Entry.Call.Trim()))); Qso stored = Session.Add(Noted(BuildQso(Entry.Call.Trim())));
Entry.Clear(); Entry.Clear();
FillSentExchange();
OtherName = ""; OtherName = "";
Comment = ""; Comment = "";
SentTime = ""; SentTime = "";
@@ -228,6 +240,19 @@ public sealed class OperatingPosition
} }
} }
/// Puts what this station sends into the boxes that show it: the report
/// and the serial number. N1MM keeps both filled in and steps over them,
/// so the number going out is on the screen without being typed.
public void FillSentExchange()
{
foreach (ExchangeField field in Entry.Exchange.Where(f => f.IsSent))
{
Entry.Set(
field.Slot,
field.Slot == ExchangeSlot.SentReport ? DefaultReport() : SentNumber.ToString());
}
}
/// Puts the report the operator would send anyway into the report boxes /// Puts the report the operator would send anyway into the report boxes
/// that are empty: 599 on CW and digital modes, 59 on phone. N1MM does the /// that are empty: 599 on CW and digital modes, 59 on phone. N1MM does the
/// same when the callsign box is left, which is why neither program stops /// same when the callsign box is left, which is why neither program stops
@@ -406,6 +431,7 @@ public sealed class OperatingPosition
Editing = null; Editing = null;
held = null; held = null;
Entry.Clear(); Entry.Clear();
FillSentExchange();
OtherName = ""; OtherName = "";
Comment = ""; Comment = "";
SentTime = ""; SentTime = "";
@@ -458,6 +484,8 @@ public sealed class OperatingPosition
{ {
qso = ExchangeSlots.Apply(qso, field.Slot, Entry.ValueOf(field.Slot).Trim()); qso = ExchangeSlots.Apply(qso, field.Slot, Entry.ValueOf(field.Slot).Trim());
} }
return qso; return Entry.Exchange.Any(f => f.Slot == ExchangeSlot.Exchange1)
? Contest.FromExchange1(qso, Entry.ValueOf(ExchangeSlot.Exchange1).Trim())
: qso;
} }
} }
+5 -3
View File
@@ -237,7 +237,7 @@ public sealed class QsoEditor
{ {
columns.Add(new QsoColumn("SntNr", QsoField.SentNumber, 5)); columns.Add(new QsoColumn("SntNr", QsoField.SentNumber, 5));
} }
foreach (ExchangeField field in contest.ExchangeFieldsFor(me)) foreach (ExchangeField field in contest.LogFieldsFor(me).Where(f => !f.IsSent))
{ {
QsoField mapped = FieldFor(field.Slot); QsoField mapped = FieldFor(field.Slot);
kinds[mapped] = field.Kind; kinds[mapped] = field.Kind;
@@ -245,8 +245,10 @@ public sealed class QsoEditor
} }
if (contest.HasQtcTraffic) if (contest.HasQtcTraffic)
{ {
// N1MM's header for the misc column in WAE, where it holds the // N1MM's two headers for WAE: the exchange column says whether the
// series a QTC row belongs to // row is a contact or QTC traffic — `QSO`, `SQTC` or `RQTC` — and
// the misc column holds the series a QTC row belongs to
columns.Add(new QsoColumn("Series", QsoField.Exchange1, 6));
columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6)); columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6));
} }
columns.Add(new QsoColumn("Op", QsoField.Operator, 8)); columns.Add(new QsoColumn("Op", QsoField.Operator, 8));
@@ -86,9 +86,9 @@ public class CqWorldWideRttyTests
{ {
IReadOnlyList<ExchangeField> fields = Rtty.ExchangeFieldsFor(TestLog.Germany); IReadOnlyList<ExchangeField> fields = Rtty.ExchangeFieldsFor(TestLog.Germany);
Assert.Equal(3, fields.Count); Assert.Equal(4, fields.Count);
Assert.Equal(ExchangeSlot.Section, fields[2].Slot); Assert.Equal(ExchangeSlot.Section, fields[3].Slot);
Assert.False(fields[2].IsRequired); Assert.False(fields[3].IsRequired);
} }
/// The Canadian areas this contest counts are its own list, which splits /// The Canadian areas this contest counts are its own list, which splits
+15 -8
View File
@@ -61,17 +61,24 @@ public class OkOmDxTests
/// The entry window walks the county box for an OK or OM station and the /// The entry window walks the county box for an OK or OM station and the
/// serial box for everyone else. /// serial box for everyone else.
[Fact] [Fact]
public void OnlyOneOfTheTwoBoxesIsWalked() public void TheOneExchangeBoxHoldsACountyOrASerialNumber()
{ {
IReadOnlyList<ExchangeField> fields = Cw.ExchangeFieldsFor(TestLog.Slovakia); IReadOnlyList<ExchangeField> fields = Cw.ExchangeFieldsFor(TestLog.Slovakia);
CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ");
CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ");
Assert.Equal( Assert.Equal(
[ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1], [ExchangeSlot.SentReport, ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
fields.Where(f => !Cw.SkipsField(f, dx)).Select(f => f.Slot).ToList()); fields.Select(f => f.Slot).ToList());
Assert.Equal( }
[ExchangeSlot.ReceivedReport, ExchangeSlot.Section],
fields.Where(f => !Cw.SkipsField(f, okom)).Select(f => f.Slot).ToList()); /// A county goes in the section column as well, where the multiplier is
/// read from; a serial number from anywhere else does not.
[Fact]
public void ACountyReachesTheSectionColumn()
{
Qso okom = TestLog.Contact("OM3XYZ") with { CountryPrefix = "OM" };
Qso dx = TestLog.Contact("IK2XYZ") with { CountryPrefix = "I" };
Assert.Equal("BAA", Cw.FromExchange1(okom with { Exchange1 = "BAA" }, "BAA").Section);
Assert.Equal("", Cw.FromExchange1(dx with { Exchange1 = "123" }, "123").Section);
} }
} }
@@ -32,4 +32,51 @@ public class SweepstakesTests
[Fact] [Fact]
public void SomethingThatIsNotASectionCountsForNothing() => public void SomethingThatIsNotASectionCountsForNothing() =>
Assert.Empty(Log().Judge(TestLog.Contact("K1ABC", section: "ZZ")).NewMultipliers); Assert.Empty(Log().Judge(TestLog.Contact("K1ABC", section: "ZZ")).NewMultipliers);
/// The whole exchange goes in one box, in any order, and comes back out in
/// four columns. These are the shapes N1MM's box takes.
[Theory]
[InlineData("72 A 74 SCV")]
[InlineData("72A 74SCV")]
[InlineData("72 A 74SCV")]
public void TheOneBoxIsReadBackAsFourFields(string typed)
{
SweepstakesExchange exchange = SweepstakesExchange.Parse(typed);
Assert.Equal(72, exchange.Number);
Assert.Equal("A", exchange.Precedence);
Assert.Equal(74, exchange.Check);
Assert.Equal("SCV", exchange.Section);
}
[Fact]
public void TheOneBoxFillsTheContact()
{
Qso logged = new Sweepstakes(ModeCategory.Cw)
.FromExchange1(TestLog.Contact("K1ABC"), "72A 74SCV");
Assert.Equal(72, logged.ReceivedNumber);
Assert.Equal("A", logged.Precedence);
Assert.Equal(74, logged.Check);
Assert.Equal("SCV", logged.Section);
}
/// The exchange is one box and four log columns, as it is in N1MM.
[Fact]
public void TheLogKeepsTheFourPartsApart()
{
Sweepstakes contest = new(ModeCategory.Cw);
Assert.Equal(
[ExchangeSlot.SentNumber, ExchangeSlot.Exchange1],
contest.ExchangeFieldsFor(TestLog.UnitedStates).Select(f => f.Slot).ToList());
Assert.Equal(
[
ExchangeSlot.SerialNumber,
ExchangeSlot.Precedence,
ExchangeSlot.Check,
ExchangeSlot.Section,
],
contest.LogFieldsFor(TestLog.UnitedStates).Select(f => f.Slot).ToList());
}
} }
@@ -93,7 +93,7 @@ public class UserDefinedContestTests
/// The sent-side boxes are in the same setting and are not exchange boxes. /// The sent-side boxes are in the same setting and are not exchange boxes.
[Fact] [Fact]
public void TheSentBoxesAreLeftOutOfTheExchange() public void TheSentBoxesAreBoxesOfTheirOwn()
{ {
UserDefinedContest contest = new(UdcFile.Parse(""" UserDefinedContest contest = new(UdcFile.Parse("""
[Contest] [Contest]
@@ -103,7 +103,12 @@ public class UserDefinedContestTests
""")); """));
Assert.Equal( Assert.Equal(
[ExchangeSlot.ReceivedReport, ExchangeSlot.SerialNumber], [
ExchangeSlot.SentReport,
ExchangeSlot.SentNumber,
ExchangeSlot.ReceivedReport,
ExchangeSlot.SerialNumber,
],
contest.ExchangeFieldsFor(TestLog.Germany).Select(f => f.Slot).ToList()); contest.ExchangeFieldsFor(TestLog.Germany).Select(f => f.Slot).ToList());
} }
} }
@@ -50,6 +50,34 @@ public class OperatingPositionTests
session.Entry.Set(ExchangeSlot.Zone, zone); session.Entry.Set(ExchangeSlot.Zone, zone);
} }
/// N1MM shows what this station sends beside what it copies, keeps both
/// filled in and steps over them.
[Fact]
public void TheSentBoxesAreFilledInAndSteppedOver()
{
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
Assert.Equal("599", session.Entry.ValueOf(ExchangeSlot.SentReport));
Assert.Equal("1", session.Entry.ValueOf(ExchangeSlot.SentNumber));
// the boxes are the call, the two reports, the sent serial and the
// serial being copied
session.Entry.Advance();
Assert.Equal(4, session.Entry.Focus);
}
[Fact]
public void TheSentSerialGoesUpWhenAContactIsLogged()
{
OperatingPosition session = Session(new CqWpx(ModeCategory.Cw));
session.Entry.Call = "JA1XYZ";
session.Entry.Set(ExchangeSlot.ReceivedReport, "599");
session.Entry.Set(ExchangeSlot.SerialNumber, "12");
Assert.Equal(1, session.LogContact().SentNumber);
Assert.Equal("2", session.Entry.ValueOf(ExchangeSlot.SentNumber));
}
/// The BARTG contest sends the time of the exchange, and logs the time it /// The BARTG contest sends the time of the exchange, and logs the time it
/// sent rather than the time the contact was logged. /// sent rather than the time the contact was logged.
[Fact] [Fact]
@@ -152,12 +180,12 @@ public class OperatingPositionTests
[Fact] [Fact]
public void SpaceStepsOverTheReportAndComesBackToTheCall() public void SpaceStepsOverTheReportAndComesBackToTheCall()
{ {
// CQ WW's boxes are the call, the report and the zone // CQ WW's boxes are the call, the two reports and the zone
OperatingPosition session = Session(); OperatingPosition session = Session();
Assert.Equal(0, session.Entry.Focus); Assert.Equal(0, session.Entry.Focus);
session.Entry.Advance(); session.Entry.Advance();
Assert.Equal(2, session.Entry.Focus); Assert.Equal(3, session.Entry.Focus);
session.Entry.Advance(); session.Entry.Advance();
Assert.Equal(0, session.Entry.Focus); Assert.Equal(0, session.Entry.Focus);
} }
@@ -166,7 +194,7 @@ public class OperatingPositionTests
public void GoingBackStepsOverTheReportToo() public void GoingBackStepsOverTheReportToo()
{ {
OperatingPosition session = Session(); OperatingPosition session = Session();
session.Entry.FocusOn(2); session.Entry.FocusOn(3);
session.Entry.Retreat(); session.Entry.Retreat();
@@ -388,8 +416,7 @@ public class OperatingPositionTests
session.Entry.Call = "W3LPL"; session.Entry.Call = "W3LPL";
Assert.True(session.FillFromHistory()); Assert.True(session.FillFromHistory());
Assert.Equal("MDC", session.Entry.ValueOf(ExchangeSlot.Section)); Assert.Equal("56MDC", session.Entry.ValueOf(ExchangeSlot.Exchange1));
Assert.Equal("56", session.Entry.ValueOf(ExchangeSlot.Check));
} }
/// What the other station actually sends beats what was published, so a box /// What the other station actually sends beats what was published, so a box
@@ -401,11 +428,11 @@ public class OperatingPositionTests
new Sweepstakes(ModeCategory.Cw), new Sweepstakes(ModeCategory.Cw),
"!!Order!!,Call,Sect\nW3LPL,MDC"); "!!Order!!,Call,Sect\nW3LPL,MDC");
session.Entry.Call = "W3LPL"; session.Entry.Call = "W3LPL";
session.Entry.Set(ExchangeSlot.Section, "EPA"); session.Entry.Set(ExchangeSlot.Exchange1, "72A 74EPA");
session.FillFromHistory(); session.FillFromHistory();
Assert.Equal("EPA", session.Entry.ValueOf(ExchangeSlot.Section)); Assert.Equal("72A 74EPA", session.Entry.ValueOf(ExchangeSlot.Exchange1));
} }
[Fact] [Fact]
@@ -417,7 +444,7 @@ public class OperatingPositionTests
session.Entry.Call = "JA1XYZ"; session.Entry.Call = "JA1XYZ";
Assert.False(session.FillFromHistory()); Assert.False(session.FillFromHistory());
Assert.Equal("", session.Entry.ValueOf(ExchangeSlot.Section)); Assert.Equal("", session.Entry.ValueOf(ExchangeSlot.Exchange1));
} }
/// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use. /// CQ WW exchanges the CQ zone, so the CQ zone column is the one to use.
@@ -445,7 +472,7 @@ public class OperatingPositionTests
session.FillFromHistory(); session.FillFromHistory();
Assert.Equal("CT", session.Entry.ValueOf(ExchangeSlot.Section)); Assert.Equal("00CT", session.Entry.ValueOf(ExchangeSlot.Exchange1));
} }
[Fact] [Fact]
@@ -485,9 +512,9 @@ public class OperatingPositionTests
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital)); OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "JA1XYZ"; session.Entry.Call = "JA1XYZ";
// the boxes are the call, the report, the zone and the state // the boxes are the call, the two reports, the zone and the state
session.MoveFocus(forward: true); session.MoveFocus(forward: true);
Assert.Equal(2, session.Entry.Focus); Assert.Equal(3, session.Entry.Focus);
session.MoveFocus(forward: true); session.MoveFocus(forward: true);
Assert.Equal(0, session.Entry.Focus); Assert.Equal(0, session.Entry.Focus);
} }
@@ -498,10 +525,10 @@ public class OperatingPositionTests
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital)); OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
session.Entry.Call = "K1ABC"; session.Entry.Call = "K1ABC";
session.MoveFocus(forward: true);
Assert.Equal(2, session.Entry.Focus);
session.MoveFocus(forward: true); session.MoveFocus(forward: true);
Assert.Equal(3, session.Entry.Focus); Assert.Equal(3, session.Entry.Focus);
session.MoveFocus(forward: true);
Assert.Equal(4, session.Entry.Focus);
} }
[Fact] [Fact]
@@ -58,6 +58,35 @@ public class QsoEditorTests
public void AContestWithSerialNumbersGetsASentNumberColumn() => public void AContestWithSerialNumbersGetsASentNumberColumn() =>
Assert.Contains(QsoField.SentNumber, Sweeps().Columns.Select(c => c.Field)); Assert.Contains(QsoField.SentNumber, Sweeps().Columns.Select(c => c.Field));
/// Sweepstakes types the whole exchange in one box and logs it in four
/// columns, as N1MM does.
[Fact]
public void TheSweepstakesExchangeGetsAColumnPerPart()
{
IReadOnlyList<QsoField> fields = [.. Sweeps().Columns.Select(c => c.Field)];
Assert.Contains(QsoField.ReceivedNumber, fields);
Assert.Contains(QsoField.Precedence, fields);
Assert.Contains(QsoField.Check, fields);
Assert.Contains(QsoField.Section, fields);
Assert.DoesNotContain(QsoField.Exchange1, fields);
}
/// A WAE row says whether it is a contact or QTC traffic in the exchange
/// column — `QSO`, `SQTC` or `RQTC` — and N1MM's log has that column.
[Fact]
public void TheWaeLogHasAColumnForTheQtcDesignator()
{
QsoEditor editor = new(new Wae(ModeCategory.Cw), Me, CountryFile.Parse(Countries));
Assert.Equal(
["Series", "QTC?"],
editor.Columns
.Where(c => c.Field is QsoField.Exchange1 or QsoField.MiscText)
.Select(c => c.Label)
.ToList());
}
[Fact] [Fact]
public void ChangingTheCallLooksTheCountryUpAgain() public void ChangingTheCallLooksTheCountryUpAgain()
{ {