Files
Nonemm/src/Nonemm.Session/EntryFields.cs
T
erikbrockoandClaude Opus 5 211298e04c Let the walk and the completeness check read one rule
A box the walk between the entry boxes steps over cannot be typed in, so it
must not hold the contact back either. `EntryFields.IsComplete` counted every
box marked required, while `MoveFocus` stepped over the ones the contest does
not ask this station for, so a contest with a box for some stations only
could lock the operator out of his own log.

The seven contests the scoring audit named do not do that any more —
`bae40bb` gave each of them the boxes N1MM shows, and fifteen reproductions
across all seven and both station types log cleanly — but the rule they broke
was still there to break again. Completeness now takes the same
applicability rule the cursor uses, and `OperatingPosition` answers it in one
place for the entry window, the ESM window and logging alike.

The audit is marked up for the three findings that are already fixed and for
item 14, which turns out to be what N1MM does rather than a fault.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
2026-09-10 20:33:30 +00:00

135 lines
4.0 KiB
C#

using Nonemm.Contests;
namespace Nonemm.Session;
/// What the operator has typed: the callsign, a value per exchange box, and
/// which box the cursor is in. The callsign box is field 0.
public sealed class EntryFields
{
private readonly List<string> values;
public EntryFields(IReadOnlyList<ExchangeField> exchange)
{
Exchange = exchange;
values = [.. Enumerable.Repeat("", exchange.Count)];
}
public IReadOnlyList<ExchangeField> Exchange { get; }
/// Raised when the callsign box goes from empty to holding something. That
/// is where N1MM takes the serial number for the contact, so the number is
/// out on the network before the contact is worked.
public event EventHandler? CallStarted;
public string Call
{
get;
set
{
bool wasEmpty = field.Trim().Length == 0;
field = value;
if (wasEmpty && field.Trim().Length > 0)
{
CallStarted?.Invoke(this, EventArgs.Empty);
}
}
} = "";
/// 0 is the callsign box; 1 and up are the exchange boxes in order.
public int Focus { get; private set; }
public int FieldCount => Exchange.Count + 1;
public string this[int field]
{
get => field == 0 ? Call : values[field - 1];
set
{
if (field == 0)
{
Call = value;
}
else
{
values[field - 1] = value;
}
}
}
public string ValueOf(ExchangeSlot slot)
{
for (int at = 0; at < Exchange.Count; at++)
{
if (Exchange[at].Slot == slot)
{
return values[at];
}
}
return "";
}
public void Set(ExchangeSlot slot, string value)
{
for (int at = 0; at < Exchange.Count; at++)
{
if (Exchange[at].Slot == slot)
{
values[at] = value;
}
}
}
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 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);
private int Next(int by)
{
int at = Focus;
for (int step = 0; step < FieldCount; step++)
{
at = (at + by + FieldCount) % FieldCount;
if (at == 0 || !IsFilledIn(Exchange[at - 1]))
{
return at;
}
}
return Focus;
}
private static bool IsFilledIn(ExchangeField field) =>
field.IsSent || field.Kind == ExchangeFieldKind.Report;
/// True when the operator has typed nothing. The boxes holding what this
/// station sends are filled in by the program, so they do not count.
public bool IsEmpty =>
Call.Trim().Length == 0 &&
Exchange.Where((f, at) => !f.IsSent && values[at].Trim().Length > 0).Any() == false;
/// True when everything the contest asks for has been typed. `applies`
/// says whether a box is one this station is asked for: the walk between
/// the boxes steps over the ones it is not, so an empty box the cursor
/// never lands in cannot be what is missing. `OperatingPosition` asks the
/// contest, and both use the same answer.
public bool IsComplete(Func<ExchangeField, bool> applies) =>
Call.Trim().Length > 0 &&
Exchange.Where((f, at) =>
f.IsRequired && applies(f) && values[at].Trim().Length == 0).Any() == false;
public void Clear()
{
Call = "";
for (int at = 0; at < values.Count; at++)
{
values[at] = "";
}
Focus = 0;
}
}