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:
@@ -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
|
||||
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
|
||||
is part of the exchange, so neither holds up a contact: they fill the log's Name
|
||||
and Comment columns, and a contest that exchanges a name uses that instead.
|
||||
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,
|
||||
and what the call history says in its UserText column appears under the entry
|
||||
boxes.
|
||||
The entry window shows the boxes N1MM's entry window shows for that contest and
|
||||
no others. General logging has a Name box and a Comment box; a contest has the
|
||||
boxes it exchanges. Two of them are for what this station sends — the report and
|
||||
the serial number — and both are filled in and stepped over, as they are in
|
||||
N1MM.
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
stepping over the report boxes, because nobody types 599: leaving the callsign
|
||||
box fills them with the report for the mode, 599 on CW and digital, 59 on phone.
|
||||
stepping over the report boxes and the sent serial, because nobody types 599 or
|
||||
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
|
||||
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.
|
||||
|
||||
Tab walks every box in turn — callsign, the exchange including the reports, the
|
||||
name and the comment — and round again; shift and tab go the other way. N1MM
|
||||
Tab walks every box in turn — the callsign and the exchange, the reports
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
N1MM uses. The window is laid out the same way: the header, ten lines of time,
|
||||
|
||||
@@ -286,7 +286,8 @@ public sealed partial class EntryWindow
|
||||
for (int at = 0; at < Logging.Entry.Exchange.Count; 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)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -51,8 +51,6 @@ public sealed partial class EntryWindow : Window
|
||||
/// network status window.
|
||||
private string sendingWhat = "";
|
||||
private TextBlock? callMark;
|
||||
private TextBox? nameBox;
|
||||
private TextBox? commentBox;
|
||||
private readonly DispatcherTimer clock = new() { Interval = TimeSpan.FromSeconds(1) };
|
||||
private readonly Dictionary<Type, Window> openWindows = [];
|
||||
private bool updating;
|
||||
@@ -224,55 +222,11 @@ public sealed partial class EntryWindow : Window
|
||||
ExchangeField field = Logging.Entry.Exchange[at];
|
||||
AddBox(field.Label, field.Width, at + 1);
|
||||
}
|
||||
AddNotes();
|
||||
boxes[0].Focus();
|
||||
SyncBoxes();
|
||||
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)
|
||||
{
|
||||
EntryGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
|
||||
@@ -398,7 +352,7 @@ public sealed partial class EntryWindow : Window
|
||||
e.Handled = true;
|
||||
session.SwapRadio();
|
||||
break;
|
||||
case Key.Tab when FocusedIsEntryBox() || FocusedIsNoteBox():
|
||||
case Key.Tab when FocusedIsEntryBox():
|
||||
e.Handled = true;
|
||||
MoveToNextBox(forward: !e.KeyModifiers.HasFlag(KeyModifiers.Shift));
|
||||
break;
|
||||
@@ -648,7 +602,6 @@ public sealed partial class EntryWindow : Window
|
||||
bool filled = TakeFramedCall();
|
||||
filled |= Logging.FillReports();
|
||||
filled |= Logging.FillFromHistory();
|
||||
filled |= FillNameFromHistory();
|
||||
if (filled)
|
||||
{
|
||||
SyncBoxes();
|
||||
@@ -663,26 +616,19 @@ public sealed partial class EntryWindow : Window
|
||||
FocusEntryBox();
|
||||
}
|
||||
|
||||
/// What tab does: every box in turn, reports, name and comment included,
|
||||
/// and round again. N1MM does not touch the tab key, so tab there walks
|
||||
/// the boxes in the order they are laid out, and this walks the same ones.
|
||||
/// What tab does: every box in turn, the reports included, and round
|
||||
/// again. N1MM does not touch the tab key, so tab there walks the boxes in
|
||||
/// the order they are laid out, and this walks the same ones.
|
||||
private void MoveToNextBox(bool forward)
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
List<TextBox> all = [.. boxes, .. new[] { nameBox, commentBox }.OfType<TextBox>()];
|
||||
int at = all.FindIndex(box => box.IsFocused);
|
||||
int next = ((at < 0 ? 0 : at) + (forward ? 1 : -1) + all.Count) % all.Count;
|
||||
if (next < boxes.Count)
|
||||
{
|
||||
Logging.Entry.FocusOn(next);
|
||||
FocusEntryBox();
|
||||
return;
|
||||
}
|
||||
all[next].Focus();
|
||||
all[next].CaretIndex = all[next].Text?.Length ?? 0;
|
||||
int at = boxes.FindIndex(box => box.IsFocused);
|
||||
int next = ((at < 0 ? 0 : at) + (forward ? 1 : -1) + boxes.Count) % boxes.Count;
|
||||
Logging.Entry.FocusOn(next);
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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 FocusedIsNoteBox() => nameBox?.IsFocused == true || commentBox?.IsFocused == true;
|
||||
|
||||
/// The buttons N1MM puts under its function keys. Each one does what its
|
||||
/// key does, so there is one place deciding what happens.
|
||||
private void OnStopSending(object? sender, RoutedEventArgs e)
|
||||
@@ -798,14 +725,6 @@ public sealed partial class EntryWindow : Window
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,6 +29,20 @@ public interface Contest
|
||||
/// contests skip nothing.
|
||||
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.
|
||||
IReadOnlyList<string> MultiplierNames { get; }
|
||||
|
||||
|
||||
@@ -10,6 +10,10 @@ public sealed record ExchangeField(
|
||||
/// How wide the box needs to be, in characters.
|
||||
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
|
||||
{
|
||||
ExchangeFieldKind.Report => 3,
|
||||
@@ -22,6 +26,7 @@ public sealed record ExchangeField(
|
||||
ExchangeFieldKind.Precedence => 1,
|
||||
ExchangeFieldKind.Check => 2,
|
||||
ExchangeFieldKind.Power => 4,
|
||||
ExchangeFieldKind.SweepstakesExchange => 16,
|
||||
_ => 8,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,4 +15,8 @@ public enum ExchangeFieldKind
|
||||
Power,
|
||||
Precedence,
|
||||
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,
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ namespace Nonemm.Contests;
|
||||
/// columns so an exchange lands in the same place N1MM would put it.
|
||||
public enum ExchangeSlot
|
||||
{
|
||||
SentReport,
|
||||
SentNumber,
|
||||
ReceivedReport,
|
||||
SerialNumber,
|
||||
Zone,
|
||||
|
||||
@@ -10,6 +10,8 @@ public static class QsoExchange
|
||||
/// box it holds nothing for.
|
||||
public static string ValueOf(Qso qso, ExchangeSlot slot) => slot switch
|
||||
{
|
||||
ExchangeSlot.SentReport => qso.SentReport,
|
||||
ExchangeSlot.SentNumber => Digits(qso.SentNumber),
|
||||
ExchangeSlot.ReceivedReport => qso.ReceivedReport,
|
||||
ExchangeSlot.SerialNumber => Digits(qso.ReceivedNumber),
|
||||
ExchangeSlot.Zone => Digits(qso.Zone),
|
||||
|
||||
@@ -20,7 +20,8 @@ public sealed class AllAsian : Contest
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -17,19 +17,29 @@ public sealed class Arrl10Metre : Contest
|
||||
|
||||
public string CabrilloName => "ARRL-10";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
/// 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("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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 },
|
||||
];
|
||||
|
||||
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
|
||||
{
|
||||
ExchangeSlot.Section => their is not null && !SendsState(their.Entity.PrimaryPrefix),
|
||||
ExchangeSlot.Exchange1 => their is not null && SendsState(their.Entity.PrimaryPrefix),
|
||||
_ => false,
|
||||
};
|
||||
/// A number is a serial and anything else is a state or province.
|
||||
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 => ["States", "Countries"];
|
||||
|
||||
|
||||
@@ -21,12 +21,16 @@ public sealed class ArrlDx : Contest
|
||||
|
||||
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) =>
|
||||
[
|
||||
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)
|
||||
? new ExchangeField("Pwr", ExchangeSlot.Section, ExchangeFieldKind.Power)
|
||||
: new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
|
||||
? new ExchangeField("Pwr", ExchangeSlot.Power, ExchangeFieldKind.Power)
|
||||
: new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
|
||||
];
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Mults"];
|
||||
@@ -74,7 +78,7 @@ public sealed class ArrlDx : Contest
|
||||
[
|
||||
new CabrilloField(qso.Call.Text, 13),
|
||||
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);
|
||||
|
||||
@@ -25,8 +25,10 @@ public sealed class BartgSprint : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
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("RcvNr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
new ExchangeField("Time", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 4 },
|
||||
];
|
||||
|
||||
|
||||
@@ -24,19 +24,21 @@ public sealed class CqOneSixty : Contest
|
||||
|
||||
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) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince),
|
||||
new ExchangeField("Zone", ExchangeSlot.Exchange1, ExchangeFieldKind.CqZone),
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, 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
|
||||
{
|
||||
ExchangeSlot.Section => their is not null && !IsUsOrCanadian(their.Entity.PrimaryPrefix),
|
||||
ExchangeSlot.Exchange1 => their is not null && IsUsOrCanadian(their.Entity.PrimaryPrefix),
|
||||
_ => false,
|
||||
};
|
||||
/// 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.
|
||||
public Qso FromExchange1(Qso qso, string typed) =>
|
||||
int.TryParse(typed, out _)
|
||||
? qso with { Section = "" }
|
||||
: qso with { Section = typed.Trim().ToUpperInvariant() };
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["States", "Countries"];
|
||||
|
||||
|
||||
@@ -39,14 +39,16 @@ public sealed class CqWorldWide : Contest
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRtty
|
||||
?
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone),
|
||||
new ExchangeField("S/P", ExchangeSlot.Section, ExchangeFieldKind.UsStateOrCanadianProvince, IsRequired: false),
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
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("Zone", ExchangeSlot.Zone, ExchangeFieldKind.CqZone),
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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
|
||||
|
||||
@@ -33,8 +33,10 @@ public sealed class CqWpx : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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"];
|
||||
|
||||
@@ -31,10 +31,18 @@ public sealed class EuDxContest : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Code", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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 DupeScope DupeScope => DupeScope.PerBandAndMode;
|
||||
@@ -90,7 +98,7 @@ public sealed class EuDxContest : Contest
|
||||
[
|
||||
new CabrilloField(qso.Call.Text, 13),
|
||||
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);
|
||||
|
||||
@@ -15,7 +15,9 @@ public sealed class FieldDayRegionOne : Contest
|
||||
|
||||
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),
|
||||
];
|
||||
|
||||
|
||||
@@ -27,19 +27,36 @@ public sealed class FrenchRef : Contest
|
||||
|
||||
public string CabrilloName => mode == ModeCategory.Cw ? "REF-CW" : "REF-SSB";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Dept", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
/// 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("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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("Department", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 3 },
|
||||
];
|
||||
|
||||
public bool SkipsField(ExchangeField field, CountryLookup? their) => field.Slot switch
|
||||
{
|
||||
ExchangeSlot.Section => their is not null && their.Entity.PrimaryPrefix != "F",
|
||||
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "F",
|
||||
_ => false,
|
||||
};
|
||||
public Qso FromExchange1(Qso qso, string typed) =>
|
||||
qso.CountryPrefix == "F"
|
||||
? qso with { Section = typed.Trim().ToUpperInvariant() }
|
||||
: qso with { ReceivedNumber = int.TryParse(typed, out int number) ? number : 0 };
|
||||
|
||||
/// 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"];
|
||||
|
||||
|
||||
@@ -14,7 +14,8 @@ public sealed class GeneralLogging : Contest
|
||||
|
||||
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("Comment", ExchangeSlot.Comment, ExchangeFieldKind.Text, IsRequired: false) { Width = 20 },
|
||||
];
|
||||
|
||||
@@ -17,8 +17,9 @@ public sealed class IaruHf : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Zone/HQ", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 },
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("ITU-Zone", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 6 },
|
||||
];
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Zones", "HQ"];
|
||||
|
||||
@@ -19,7 +19,9 @@ public sealed class Iota : Contest
|
||||
|
||||
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("IOTA", ExchangeSlot.Section, ExchangeFieldKind.Text, IsRequired: false)
|
||||
{
|
||||
|
||||
@@ -15,18 +15,26 @@ public sealed class MexicoRtty : Contest
|
||||
|
||||
public string CabrilloName => "XE-RTTY";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("State", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsMexican(me.CountryPrefix)
|
||||
?
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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("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),
|
||||
ExchangeSlot.Exchange1 => their is not null && IsMexican(their.Entity.PrimaryPrefix),
|
||||
_ => false,
|
||||
Section = IsMexican(qso.CountryPrefix) ? typed.Trim().ToUpperInvariant() : "",
|
||||
};
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Countries", "States"];
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class NorthAmericanQsoParty : Contest
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
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"];
|
||||
@@ -44,7 +44,7 @@ public sealed class NorthAmericanQsoParty : Contest
|
||||
public IReadOnlyList<Multiplier> MultipliersFor(QsoContext qso)
|
||||
{
|
||||
string band = qso.Band?.Name ?? "";
|
||||
string exchange = qso.Qso.Exchange1.Trim().ToUpperInvariant();
|
||||
string exchange = qso.Qso.Section.Trim().ToUpperInvariant();
|
||||
if (StatesAndProvinces.IsStateOrProvince(exchange))
|
||||
{
|
||||
return [new Multiplier(1, exchange, band)];
|
||||
@@ -66,7 +66,7 @@ public sealed class NorthAmericanQsoParty : Contest
|
||||
[
|
||||
new CabrilloField(qso.Call.Text, 13),
|
||||
new CabrilloField(qso.Name, 10),
|
||||
new CabrilloField(qso.Exchange1, 6),
|
||||
new CabrilloField(qso.Section, 6),
|
||||
]);
|
||||
|
||||
private string ModeLabel() => mode switch
|
||||
|
||||
@@ -20,8 +20,10 @@ public sealed class OceaniaDx : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
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("RcvNR", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
];
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Prefixes"];
|
||||
|
||||
@@ -22,22 +22,30 @@ public sealed class OkOmDx : Contest
|
||||
|
||||
public string CabrilloName => "OK-OM-DX";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 4 },
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
/// 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("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
new ExchangeField("Rcv", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
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
|
||||
/// other way round, so one of the two boxes is stepped over.
|
||||
public bool SkipsField(ExchangeField field, CountryLookup? their) =>
|
||||
field.Slot switch
|
||||
{
|
||||
ExchangeSlot.Section => their is not null && !IsOkOm(their.Entity.PrimaryPrefix),
|
||||
ExchangeSlot.Exchange1 => their is not null && IsOkOm(their.Entity.PrimaryPrefix),
|
||||
_ => false,
|
||||
};
|
||||
/// A county goes in the section column as well, which is where the
|
||||
/// multiplier is read from; a serial number stays in the exchange column.
|
||||
public Qso FromExchange1(Qso qso, string typed) => qso with
|
||||
{
|
||||
Section = IsOkOm(qso.CountryPrefix) ? typed.Trim().ToUpperInvariant() : "",
|
||||
};
|
||||
|
||||
public IReadOnlyList<string> MultiplierNames => ["Mults"];
|
||||
|
||||
|
||||
@@ -13,11 +13,30 @@ public sealed class RttyRoundup : Contest
|
||||
|
||||
public string CabrilloName => "ARRL-RTTY";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("S/P/Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
|
||||
];
|
||||
/// 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("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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"];
|
||||
|
||||
|
||||
@@ -13,11 +13,27 @@ public sealed class RussianDistrictAward : Contest
|
||||
|
||||
public string CabrilloName => "RDAC";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("RDA", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 5 },
|
||||
];
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRussian(me.CountryPrefix)
|
||||
?
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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,
|
||||
/// and the score summary shows both, so the districts stay in the second
|
||||
@@ -59,4 +75,7 @@ public sealed class RussianDistrictAward : Contest
|
||||
]);
|
||||
|
||||
private static string District(Qso qso) => qso.Section.Trim().ToUpperInvariant();
|
||||
|
||||
private static bool IsRussian(string countryPrefix) =>
|
||||
countryPrefix is "UA" or "UA2" or "UA9";
|
||||
}
|
||||
|
||||
@@ -15,11 +15,27 @@ public sealed class RussianDxRtty : Contest
|
||||
|
||||
public string CabrilloName => "RADIO-WW-RTTY";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Oblast/Zone", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
|
||||
];
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsRussian(me.CountryPrefix)
|
||||
?
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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"];
|
||||
|
||||
|
||||
@@ -15,8 +15,10 @@ public sealed class SartgRtty : Contest
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Nr", ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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"];
|
||||
|
||||
@@ -17,12 +17,36 @@ public sealed class Sweepstakes : Contest
|
||||
|
||||
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) =>
|
||||
[
|
||||
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("Ck", ExchangeSlot.Check, ExchangeFieldKind.Check),
|
||||
new ExchangeField("Sec", ExchangeSlot.Section, ExchangeFieldKind.ArrlSection),
|
||||
new ExchangeField("CK", ExchangeSlot.Check, ExchangeFieldKind.Check),
|
||||
new ExchangeField("Sect", ExchangeSlot.Section, ExchangeFieldKind.ArrlSection),
|
||||
];
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -15,19 +15,35 @@ public sealed class UkrainianDx : Contest
|
||||
|
||||
public string CabrilloName => "UKRAINIAN-DX";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("Oblast", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => IsUkrainian(me.CountryPrefix)
|
||||
?
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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("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
|
||||
{
|
||||
ExchangeSlot.Section => their is not null && !IsUkrainian(their.Entity.PrimaryPrefix),
|
||||
ExchangeSlot.Exchange1 => their is not null && IsUkrainian(their.Entity.PrimaryPrefix),
|
||||
_ => false,
|
||||
};
|
||||
/// A number is the serial everyone outside Ukraine sends; anything else is
|
||||
/// an oblast, 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() };
|
||||
|
||||
/// 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"];
|
||||
|
||||
|
||||
@@ -49,7 +49,9 @@ public sealed class Wae : Contest
|
||||
|
||||
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),
|
||||
];
|
||||
|
||||
|
||||
@@ -15,20 +15,36 @@ public sealed class YoDxHf : Contest
|
||||
|
||||
public string CabrilloName => "YO-DX-HF";
|
||||
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) =>
|
||||
[
|
||||
new ExchangeField("RST", ExchangeSlot.ReceivedReport, ExchangeFieldKind.Report),
|
||||
new ExchangeField("County", ExchangeSlot.Section, ExchangeFieldKind.Text) { Width = 3 },
|
||||
new ExchangeField("Nr", ExchangeSlot.Exchange1, ExchangeFieldKind.Number),
|
||||
];
|
||||
public IReadOnlyList<ExchangeField> ExchangeFieldsFor(StationInfo me) => me.CountryPrefix == "YO"
|
||||
?
|
||||
[
|
||||
new ExchangeField("Snt", ExchangeSlot.SentReport, ExchangeFieldKind.Report, IsRequired: false),
|
||||
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("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",
|
||||
ExchangeSlot.Exchange1 => their is not null && their.Entity.PrimaryPrefix == "YO",
|
||||
_ => false,
|
||||
Section = qso.CountryPrefix == "YO" ? 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", "Counties"];
|
||||
|
||||
public DupeScope DupeScope => DupeScope.PerBandAndMode;
|
||||
|
||||
@@ -18,7 +18,8 @@ public sealed class Yota : Contest
|
||||
|
||||
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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -281,10 +281,11 @@ public sealed class UserDefinedContest : Contest
|
||||
|
||||
/// 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
|
||||
/// the wild use it. The sent-side boxes — `SNTText`, `SntNrText` — are not
|
||||
/// exchange boxes here and fall through.
|
||||
/// the wild use it.
|
||||
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),
|
||||
"RCVNRTEXT" => new ExchangeField(label, ExchangeSlot.SerialNumber, ExchangeFieldKind.Number),
|
||||
"EXCHANGE1TEXT" => new ExchangeField(label, ExchangeSlot.Exchange1, ExchangeFieldKind.Text) { Width = 6 },
|
||||
|
||||
@@ -75,6 +75,7 @@ public sealed class AvailableStations
|
||||
return ExchangeSlots.ApplyHistory(
|
||||
CountryFields.Apply(qso, position.Session.Countries),
|
||||
position.Session.History,
|
||||
position.Entry.Exchange);
|
||||
position.Contest,
|
||||
position.Me);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,9 +65,9 @@ public sealed class EntryFields
|
||||
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
|
||||
/// round to the callsign again from the last one. The report boxes are
|
||||
/// stepped over, because nobody types 599 — N1MM steps over them too, and
|
||||
/// fills them in instead.
|
||||
/// round to the callsign again from the last one. The report boxes and the
|
||||
/// sent serial are stepped over, because nobody types 599 or the number
|
||||
/// they are sending — N1MM steps over the same three and fills them in.
|
||||
public void Advance() => Focus = Next(1);
|
||||
|
||||
public void Retreat() => Focus = Next(-1);
|
||||
@@ -78,7 +78,7 @@ public sealed class EntryFields
|
||||
for (int step = 0; step < FieldCount; step++)
|
||||
{
|
||||
at = (at + by + FieldCount) % FieldCount;
|
||||
if (at == 0 || Exchange[at - 1].Kind != ExchangeFieldKind.Report)
|
||||
if (at == 0 || !IsFilledIn(Exchange[at - 1]))
|
||||
{
|
||||
return at;
|
||||
}
|
||||
@@ -86,6 +86,9 @@ public sealed class EntryFields
|
||||
return Focus;
|
||||
}
|
||||
|
||||
private static bool IsFilledIn(ExchangeField field) =>
|
||||
field.IsSent || field.Kind == ExchangeFieldKind.Report;
|
||||
|
||||
/// True when everything the contest asks for has been typed.
|
||||
public bool IsComplete =>
|
||||
Call.Trim().Length > 0 &&
|
||||
|
||||
@@ -10,6 +10,9 @@ public static class ExchangeSlots
|
||||
{
|
||||
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.SerialNumber => qso with { ReceivedNumber = 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,
|
||||
/// 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,
|
||||
// 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
|
||||
/// a contact. Used where nobody has copied the exchange yet: a spotted
|
||||
/// 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);
|
||||
if (known is null)
|
||||
{
|
||||
return qso;
|
||||
}
|
||||
foreach (ExchangeField field in exchange)
|
||||
string exchange1 = "";
|
||||
foreach (ExchangeField field in contest.ExchangeFieldsFor(me))
|
||||
{
|
||||
string value = ValueFrom(known, field);
|
||||
if (value.Length > 0)
|
||||
if (value.Length == 0)
|
||||
{
|
||||
qso = Apply(qso, field.Slot, value);
|
||||
continue;
|
||||
}
|
||||
qso = Apply(qso, field.Slot, value);
|
||||
exchange1 = field.Slot == ExchangeSlot.Exchange1 ? value : exchange1;
|
||||
}
|
||||
return qso;
|
||||
return exchange1.Length > 0 ? contest.FromExchange1(qso, exchange1) : qso;
|
||||
}
|
||||
|
||||
private static string Digits(int value) => value > 0 ? value.ToString() : "";
|
||||
|
||||
@@ -16,6 +16,11 @@ public sealed class OperatingPosition
|
||||
Session = session;
|
||||
RadioNumber = radioNumber;
|
||||
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; }
|
||||
@@ -38,8 +43,9 @@ public sealed class OperatingPosition
|
||||
public bool IsRunning { get; set; }
|
||||
|
||||
/// 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
|
||||
/// them into the log — so neither counts towards a complete exchange.
|
||||
/// of any exchange: the name is what a contact being quick edited already
|
||||
/// held, and the note is what Ctrl+N asks for. Neither counts towards a
|
||||
/// complete exchange.
|
||||
public string OtherName { 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)
|
||||
{
|
||||
bool modeChanged = mode is not null && mode.Category != Mode.Category;
|
||||
Frequency = frequency;
|
||||
Mode = mode ?? Mode;
|
||||
if (modeChanged)
|
||||
{
|
||||
FillSentExchange();
|
||||
}
|
||||
TransmitFrequency = transmitFrequency ?? TransmitFrequency;
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
@@ -209,6 +220,7 @@ public sealed class OperatingPosition
|
||||
StampSentTime();
|
||||
Qso stored = Session.Add(Noted(BuildQso(Entry.Call.Trim())));
|
||||
Entry.Clear();
|
||||
FillSentExchange();
|
||||
OtherName = "";
|
||||
Comment = "";
|
||||
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
|
||||
/// 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
|
||||
@@ -406,6 +431,7 @@ public sealed class OperatingPosition
|
||||
Editing = null;
|
||||
held = null;
|
||||
Entry.Clear();
|
||||
FillSentExchange();
|
||||
OtherName = "";
|
||||
Comment = "";
|
||||
SentTime = "";
|
||||
@@ -458,6 +484,8 @@ public sealed class OperatingPosition
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,7 +237,7 @@ public sealed class QsoEditor
|
||||
{
|
||||
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);
|
||||
kinds[mapped] = field.Kind;
|
||||
@@ -245,8 +245,10 @@ public sealed class QsoEditor
|
||||
}
|
||||
if (contest.HasQtcTraffic)
|
||||
{
|
||||
// N1MM's header for the misc column in WAE, where it holds the
|
||||
// series a QTC row belongs to
|
||||
// N1MM's two headers for WAE: the exchange column says whether the
|
||||
// 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("Op", QsoField.Operator, 8));
|
||||
|
||||
@@ -86,9 +86,9 @@ public class CqWorldWideRttyTests
|
||||
{
|
||||
IReadOnlyList<ExchangeField> fields = Rtty.ExchangeFieldsFor(TestLog.Germany);
|
||||
|
||||
Assert.Equal(3, fields.Count);
|
||||
Assert.Equal(ExchangeSlot.Section, fields[2].Slot);
|
||||
Assert.False(fields[2].IsRequired);
|
||||
Assert.Equal(4, fields.Count);
|
||||
Assert.Equal(ExchangeSlot.Section, fields[3].Slot);
|
||||
Assert.False(fields[3].IsRequired);
|
||||
}
|
||||
|
||||
/// The Canadian areas this contest counts are its own list, which splits
|
||||
|
||||
@@ -61,17 +61,24 @@ public class OkOmDxTests
|
||||
/// The entry window walks the county box for an OK or OM station and the
|
||||
/// serial box for everyone else.
|
||||
[Fact]
|
||||
public void OnlyOneOfTheTwoBoxesIsWalked()
|
||||
public void TheOneExchangeBoxHoldsACountyOrASerialNumber()
|
||||
{
|
||||
IReadOnlyList<ExchangeField> fields = Cw.ExchangeFieldsFor(TestLog.Slovakia);
|
||||
CountryLookup? okom = TestLog.CountryFile.Find("OM3XYZ");
|
||||
CountryLookup? dx = TestLog.CountryFile.Find("IK2XYZ");
|
||||
|
||||
Assert.Equal(
|
||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
|
||||
fields.Where(f => !Cw.SkipsField(f, dx)).Select(f => f.Slot).ToList());
|
||||
Assert.Equal(
|
||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.Section],
|
||||
fields.Where(f => !Cw.SkipsField(f, okom)).Select(f => f.Slot).ToList());
|
||||
[ExchangeSlot.SentReport, ExchangeSlot.ReceivedReport, ExchangeSlot.Exchange1],
|
||||
fields.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]
|
||||
public void SomethingThatIsNotASectionCountsForNothing() =>
|
||||
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.
|
||||
[Fact]
|
||||
public void TheSentBoxesAreLeftOutOfTheExchange()
|
||||
public void TheSentBoxesAreBoxesOfTheirOwn()
|
||||
{
|
||||
UserDefinedContest contest = new(UdcFile.Parse("""
|
||||
[Contest]
|
||||
@@ -103,7 +103,12 @@ public class UserDefinedContestTests
|
||||
"""));
|
||||
|
||||
Assert.Equal(
|
||||
[ExchangeSlot.ReceivedReport, ExchangeSlot.SerialNumber],
|
||||
[
|
||||
ExchangeSlot.SentReport,
|
||||
ExchangeSlot.SentNumber,
|
||||
ExchangeSlot.ReceivedReport,
|
||||
ExchangeSlot.SerialNumber,
|
||||
],
|
||||
contest.ExchangeFieldsFor(TestLog.Germany).Select(f => f.Slot).ToList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,34 @@ public class OperatingPositionTests
|
||||
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
|
||||
/// sent rather than the time the contact was logged.
|
||||
[Fact]
|
||||
@@ -152,12 +180,12 @@ public class OperatingPositionTests
|
||||
[Fact]
|
||||
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();
|
||||
|
||||
Assert.Equal(0, session.Entry.Focus);
|
||||
session.Entry.Advance();
|
||||
Assert.Equal(2, session.Entry.Focus);
|
||||
Assert.Equal(3, session.Entry.Focus);
|
||||
session.Entry.Advance();
|
||||
Assert.Equal(0, session.Entry.Focus);
|
||||
}
|
||||
@@ -166,7 +194,7 @@ public class OperatingPositionTests
|
||||
public void GoingBackStepsOverTheReportToo()
|
||||
{
|
||||
OperatingPosition session = Session();
|
||||
session.Entry.FocusOn(2);
|
||||
session.Entry.FocusOn(3);
|
||||
|
||||
session.Entry.Retreat();
|
||||
|
||||
@@ -388,8 +416,7 @@ public class OperatingPositionTests
|
||||
session.Entry.Call = "W3LPL";
|
||||
|
||||
Assert.True(session.FillFromHistory());
|
||||
Assert.Equal("MDC", session.Entry.ValueOf(ExchangeSlot.Section));
|
||||
Assert.Equal("56", session.Entry.ValueOf(ExchangeSlot.Check));
|
||||
Assert.Equal("56MDC", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
||||
}
|
||||
|
||||
/// What the other station actually sends beats what was published, so a box
|
||||
@@ -401,11 +428,11 @@ public class OperatingPositionTests
|
||||
new Sweepstakes(ModeCategory.Cw),
|
||||
"!!Order!!,Call,Sect\nW3LPL,MDC");
|
||||
session.Entry.Call = "W3LPL";
|
||||
session.Entry.Set(ExchangeSlot.Section, "EPA");
|
||||
session.Entry.Set(ExchangeSlot.Exchange1, "72A 74EPA");
|
||||
|
||||
session.FillFromHistory();
|
||||
|
||||
Assert.Equal("EPA", session.Entry.ValueOf(ExchangeSlot.Section));
|
||||
Assert.Equal("72A 74EPA", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -417,7 +444,7 @@ public class OperatingPositionTests
|
||||
session.Entry.Call = "JA1XYZ";
|
||||
|
||||
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.
|
||||
@@ -445,7 +472,7 @@ public class OperatingPositionTests
|
||||
|
||||
session.FillFromHistory();
|
||||
|
||||
Assert.Equal("CT", session.Entry.ValueOf(ExchangeSlot.Section));
|
||||
Assert.Equal("00CT", session.Entry.ValueOf(ExchangeSlot.Exchange1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -485,9 +512,9 @@ public class OperatingPositionTests
|
||||
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
|
||||
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);
|
||||
Assert.Equal(2, session.Entry.Focus);
|
||||
Assert.Equal(3, session.Entry.Focus);
|
||||
session.MoveFocus(forward: true);
|
||||
Assert.Equal(0, session.Entry.Focus);
|
||||
}
|
||||
@@ -498,10 +525,10 @@ public class OperatingPositionTests
|
||||
OperatingPosition session = Session(new CqWorldWide(ModeCategory.Digital));
|
||||
session.Entry.Call = "K1ABC";
|
||||
|
||||
session.MoveFocus(forward: true);
|
||||
Assert.Equal(2, session.Entry.Focus);
|
||||
session.MoveFocus(forward: true);
|
||||
Assert.Equal(3, session.Entry.Focus);
|
||||
session.MoveFocus(forward: true);
|
||||
Assert.Equal(4, session.Entry.Focus);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -58,6 +58,35 @@ public class QsoEditorTests
|
||||
public void AContestWithSerialNumbersGetsASentNumberColumn() =>
|
||||
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]
|
||||
public void ChangingTheCallLooksTheCountryUpAgain()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user