Read call history files, and fill the exchange from them
N1MM's format, so a file published for a contest is read as it is: # comments, !!Order!! naming the columns of the lines after it, semicolons in preference to commas so user text can hold a comma, and a default column order for a file that names none. The directives that change what is stored are acted on; !!Order!! can appear again part way through, because these files are usually several files stuck together. Leaving the callsign box fills what the file knows into the exchange boxes that are still empty. A box with something in it is left alone: what the station actually sends beats what somebody published months ago. The zone comes from the CqZone or the ITUZone column depending on which the contest asks for, and a file with no Sect falls back to State. The check window's two missing columns are in. History lists matching calls from the file. Exchange is not about callsigns at all: it offers the values the exchange box with the cursor in it can hold, which is what N1MM's exchange pane does — it searches the contest's list of valid exchanges, not the log. CheckCandidate.Call is now Text, because an exchange value is not a call. The section-validating directives are read and passed over rather than acted on. Acting on them means holding N1MM's section lists and its rules about retired sections, and getting that wrong throws away good data. Running it caught what the unit tests could not: the exchange was filled and scored but the boxes on screen still looked empty, because nothing resynced them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -3,19 +3,24 @@ using Nonemm.Core.Calls;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// One callsign the check window offers, with what the log says about it so it
|
||||
/// is coloured the same as everywhere else.
|
||||
public sealed record CheckCandidate(string Call, PartialMatchQuality Quality, Verdict? Verdict);
|
||||
/// One thing the check window offers: a callsign in most columns, an exchange
|
||||
/// value in the exchange column. `Verdict` is what the log says about a call,
|
||||
/// so it is coloured the same as everywhere else, and null for anything that is
|
||||
/// not a call.
|
||||
public sealed record CheckCandidate(string Text, PartialMatchQuality Quality, Verdict? Verdict);
|
||||
|
||||
/// Where a column of candidates came from. They stay apart because they answer
|
||||
/// different questions: the log is what this station copied itself, the
|
||||
/// database is a guess about the whole world, and the bandmap is somebody
|
||||
/// else's claim.
|
||||
/// database is a guess about the whole world, the call history is what was
|
||||
/// published before the contest, the bandmap is somebody else's claim, and the
|
||||
/// exchange column is not about callsigns at all.
|
||||
public enum CheckSource
|
||||
{
|
||||
Log,
|
||||
Database,
|
||||
History,
|
||||
Bandmap,
|
||||
Exchange,
|
||||
}
|
||||
|
||||
/// One column of the check window.
|
||||
|
||||
@@ -19,9 +19,12 @@ public sealed class CheckWindowSources
|
||||
this.bandmap = bandmap;
|
||||
}
|
||||
|
||||
public IReadOnlyList<CheckColumn> Columns(string typed, int limit = 24)
|
||||
/// The columns are read off what the operator has typed: the callsign box
|
||||
/// feeds the call columns, and whichever exchange box has the cursor feeds
|
||||
/// the exchange column.
|
||||
public IReadOnlyList<CheckColumn> Columns(int limit = 24)
|
||||
{
|
||||
string query = typed.Trim().ToUpperInvariant();
|
||||
string query = session.Entry.Call.Trim().ToUpperInvariant();
|
||||
return
|
||||
[
|
||||
Column(CheckSource.Log, LoggedCalls(), query, limit),
|
||||
@@ -31,10 +34,36 @@ public sealed class CheckWindowSources
|
||||
query.Length < PartialMatching.ShortestUsefulQuery
|
||||
? []
|
||||
: database.Matches(query, limit).Select(Judged).ToList()),
|
||||
Column(CheckSource.History, HistoryCalls(), query, limit),
|
||||
Column(CheckSource.Bandmap, SpottedCalls(), query, limit),
|
||||
ExchangeColumn(limit),
|
||||
];
|
||||
}
|
||||
|
||||
/// What the exchange box with the cursor in it could hold: the ARRL
|
||||
/// sections, or the states and provinces. A serial number or a report has
|
||||
/// no list behind it, so the column stays empty.
|
||||
private CheckColumn ExchangeColumn(int limit)
|
||||
{
|
||||
int focus = session.Entry.Focus;
|
||||
if (focus == 0)
|
||||
{
|
||||
return new CheckColumn(CheckSource.Exchange, 0, []);
|
||||
}
|
||||
ExchangeField field = session.Entry.Exchange[focus - 1];
|
||||
IReadOnlyList<string> values = ExchangeValues.For(field.Kind);
|
||||
string query = session.Entry[focus].Trim().ToUpperInvariant();
|
||||
if (query.Length == 0)
|
||||
{
|
||||
return new CheckColumn(CheckSource.Exchange, values.Count, []);
|
||||
}
|
||||
List<CheckCandidate> found = [.. values
|
||||
.Where(v => v.StartsWith(query, StringComparison.OrdinalIgnoreCase))
|
||||
.Take(limit)
|
||||
.Select(v => new CheckCandidate(v, PartialMatchQuality.Exact, null))];
|
||||
return new CheckColumn(CheckSource.Exchange, values.Count, found);
|
||||
}
|
||||
|
||||
private CheckColumn Column(
|
||||
CheckSource source,
|
||||
IReadOnlyList<string> calls,
|
||||
@@ -57,7 +86,7 @@ public sealed class CheckWindowSources
|
||||
return new CheckColumn(
|
||||
source,
|
||||
calls.Count,
|
||||
found.OrderBy(c => c.Quality).ThenBy(c => c.Call, StringComparer.Ordinal).Take(limit).ToList());
|
||||
found.OrderBy(c => c.Quality).ThenBy(c => c.Text, StringComparer.Ordinal).Take(limit).ToList());
|
||||
}
|
||||
|
||||
private CheckCandidate Judged(PartialMatch match) =>
|
||||
@@ -81,6 +110,8 @@ public sealed class CheckWindowSources
|
||||
private IReadOnlyList<string> LoggedCalls() =>
|
||||
session.Log.Qsos.Select(q => q.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
||||
|
||||
private IReadOnlyList<string> HistoryCalls() => [.. session.Session.History.Calls];
|
||||
|
||||
private IReadOnlyList<string> SpottedCalls() =>
|
||||
bandmap.All().Select(s => s.Call.Text).Distinct(StringComparer.Ordinal).ToList();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Calls;
|
||||
using Nonemm.Core.Country;
|
||||
using Nonemm.Storage;
|
||||
|
||||
@@ -20,10 +21,12 @@ public sealed class ContestSession
|
||||
Contest contest,
|
||||
ContestInstance instance,
|
||||
StationInfo me,
|
||||
CountryFile? countries)
|
||||
CountryFile? countries,
|
||||
CallHistory? history = null)
|
||||
{
|
||||
this.store = store;
|
||||
Countries = countries;
|
||||
History = history ?? CallHistory.Empty;
|
||||
Contest = contest;
|
||||
Instance = instance;
|
||||
Me = me;
|
||||
@@ -45,6 +48,9 @@ public sealed class ContestSession
|
||||
|
||||
public CountryFile? Countries { get; }
|
||||
|
||||
/// What was published about the stations in this contest before it started.
|
||||
public CallHistory History { get; }
|
||||
|
||||
/// Serial numbers count up across the station, not per radio.
|
||||
public int SentNumber { get; private set; }
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Nonemm.Contests;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Core.Calls;
|
||||
using Nonemm.Core.Country;
|
||||
using Nonemm.Storage;
|
||||
|
||||
@@ -87,6 +88,53 @@ public sealed class RadioPosition
|
||||
return stored;
|
||||
}
|
||||
|
||||
/// Fills what the call history knows into the exchange boxes that are still
|
||||
/// empty. What the other station actually sends wins, so a box with
|
||||
/// something in it is left alone. Returns true when anything was filled.
|
||||
public bool FillFromHistory()
|
||||
{
|
||||
CallHistoryEntry? known = Session.History.Find(Entry.Call);
|
||||
if (known is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
bool filled = false;
|
||||
for (int at = 0; at < Entry.Exchange.Count; at++)
|
||||
{
|
||||
string value = ValueFor(known, Entry.Exchange[at]);
|
||||
if (value.Length == 0 || Entry[at + 1].Trim().Length > 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
Entry[at + 1] = value;
|
||||
filled = true;
|
||||
}
|
||||
if (filled)
|
||||
{
|
||||
Session.NotifyChanged();
|
||||
}
|
||||
return filled;
|
||||
}
|
||||
|
||||
private static string ValueFor(CallHistoryEntry known, ExchangeField field) => field.Slot switch
|
||||
{
|
||||
ExchangeSlot.Name => known.Name,
|
||||
// a file for a section contest often carries the state instead
|
||||
ExchangeSlot.Section => known.Section.Length > 0 ? known.Section : known.State,
|
||||
ExchangeSlot.Check => Digits(known.Check),
|
||||
ExchangeSlot.Zone => field.Kind == ExchangeFieldKind.ItuZone
|
||||
? Digits(known.ItuZone)
|
||||
: Digits(known.CqZone),
|
||||
ExchangeSlot.Exchange1 => known.Exchange1,
|
||||
ExchangeSlot.MiscText => known.MiscText,
|
||||
ExchangeSlot.GridSquare => known.GridSquare,
|
||||
ExchangeSlot.Power => known.Power,
|
||||
ExchangeSlot.Qth => known.State,
|
||||
_ => "",
|
||||
};
|
||||
|
||||
private static string Digits(int value) => value > 0 ? value.ToString() : "";
|
||||
|
||||
public void Wipe()
|
||||
{
|
||||
Entry.Clear();
|
||||
|
||||
Reference in New Issue
Block a user