Say how many QTCs are left with the station being worked

WAE gives a point for every QTC line, so an operator needs to know what is
still there to exchange. The entry window now carries N1MM's QTCinfo line in
WAE: how many QTC lines have already gone with the station in the callsign
box, how many more may go, and how many contacts are left to report to
anybody. The QTC window shows the same line.

The numbers come from the log rows, the way N1MM works them out:

- exchanged is every QTC row with that call, both directions and all bands,
  and ten is the lot
- available is every contact not reported to anybody yet, without the dupes
  and without the station itself, which is N1MM's QTCRecordsetCount
- what is left with the station is what is free of its ten, and no more than
  the contacts there are to report when we are the one reporting

Europe on CW and SSB only takes traffic down, so its line leaves the last
number out, as N1MM's does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0157i7PiZyqBkquxS9A6FTdQ
This commit is contained in:
2026-09-10 19:30:25 +00:00
co-authored by Claude Opus 5
parent 00bbb0e6b3
commit 3e326053a0
6 changed files with 223 additions and 13 deletions
+79 -12
View File
@@ -15,6 +15,28 @@ public enum QtcDirection
Either,
}
/// The QTC tally for one station, as N1MM writes it beside the callsign:
/// how many lines have already gone with it, how many more may go, and how
/// many logged contacts are left to report to anybody.
public sealed record QtcCounts(QtcDirection Role, int Exchanged, int LeftWith, int Available)
{
/// N1MM's wording, one line per role. Europe on CW and SSB only takes
/// traffic down, so there is no pile of contacts to report and the line
/// leaves that number out.
public string Text => Role switch
{
QtcDirection.Receive => $"QTCs: {Exchanged} rcvd/{LeftWith} left to exch.",
QtcDirection.Send =>
$"QTCs: {Exchanged} sent/{LeftWith} left to exch/{Available} total QTCs avail",
QtcDirection.Either =>
$"QTCs: {Exchanged} exch/{LeftWith} left to exch/{Available} total QTCs avail",
_ => "",
};
/// N1MM puts this in the status bar as well when a station has had its ten.
public bool IsFull => Exchanged >= WaeQtc.MostPerStation;
}
/// The WAE QTC rules that do not need a window: who may exchange traffic with
/// whom, which contacts are still worth reporting, how the series are numbered,
/// and whether a line an operator has typed reads as a QTC.
@@ -87,6 +109,46 @@ public sealed class QtcTraffic
public int Remaining(Callsign other) =>
Math.Max(0, WaeQtc.MostPerStation - ExchangedWith(other));
/// Which way traffic goes here whoever is worked: Europe takes it down on
/// CW and SSB, the rest of the world reads it out, and on RTTY it goes
/// both ways. `DirectionFor` answers the same question about one station;
/// this one ignores the station, so the counts still read out when the
/// callsign box is empty or holds a station with nothing left.
public QtcDirection Role
{
get
{
if (position.Contest is not Wae contest)
{
return QtcDirection.None;
}
if (contest.Modes.Contains(ModeCategory.Digital))
{
return QtcDirection.Either;
}
return position.Me.Continent == "EU" ? QtcDirection.Receive : QtcDirection.Send;
}
}
/// How many logged contacts are still worth reporting to this station: the
/// whole pile rather than one series of ten. This is N1MM's
/// `QTCRecordsetCount`, which is zero for a European station on CW and SSB
/// because it only takes traffic down.
public int Available(Callsign other) =>
Role == QtcDirection.Receive ? 0 : Reportable(other).Count();
/// The three numbers the entry window shows. `LeftWith` is what may still
/// go with this station: what is free of its ten, and no more than the
/// contacts there are to report when we are the one reporting.
public QtcCounts CountsFor(Callsign other)
{
int exchanged = ExchangedWith(other);
int free = Math.Max(0, WaeQtc.MostPerStation - exchanged);
int available = Available(other);
int left = Role == QtcDirection.Send ? Math.Min(free, available) : free;
return new QtcCounts(Role, exchanged, left, available);
}
/// Series are numbered across the whole contest, not per station: the
/// hundredth series a station sends is `100/10` whoever gets it.
public int NextSeries()
@@ -111,18 +173,7 @@ public sealed class QtcTraffic
{
return [];
}
HashSet<string> reported = position.Log.Qsos
.Where(q => q.Exchange1.Equals(WaeQtc.Sent, StringComparison.OrdinalIgnoreCase))
.Select(q => ReportedKey(q.ReceivedReport, q.ReceivedNumber))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
List<Qso> worth = position.Log.Qsos
.Where(q => !WaeQtc.IsQtc(q))
.Where(q => q.ContactType.Length == 0)
.Where(q => !q.Call.Text.Equals(other.Text, StringComparison.OrdinalIgnoreCase))
.Where(q => !reported.Contains(ReportedKey(q.Call.Text, q.SentNumber)))
.OrderBy(q => q.TimestampUtc)
.Take(room)
.ToList();
List<Qso> worth = [.. Reportable(other).OrderBy(q => q.TimestampUtc).Take(room)];
int series = NextSeries();
return [.. worth.Select(q => new WaeQtc(
other,
@@ -196,6 +247,22 @@ public sealed class QtcTraffic
private static bool IsClockTime(string text) =>
int.Parse(text[..2]) < 24 && int.Parse(text[2..]) < 60;
/// The contacts not reported to anybody yet, without the dupes and without
/// the station in front of us. The caller puts them in order. What N1MM's
/// `QTCRecordsetCount` leaves out is the dupes.
private IEnumerable<Qso> Reportable(Callsign other)
{
HashSet<string> reported = position.Log.Qsos
.Where(q => q.Exchange1.Equals(WaeQtc.Sent, StringComparison.OrdinalIgnoreCase))
.Select(q => ReportedKey(q.ReceivedReport, q.ReceivedNumber))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return position.Log.Qsos
.Where(q => !WaeQtc.IsQtc(q))
.Where(q => q.ContactType.Length == 0)
.Where(q => !q.Call.Text.Equals(other.Text, StringComparison.OrdinalIgnoreCase))
.Where(q => !reported.Contains(ReportedKey(q.Call.Text, q.SentNumber)));
}
private static string ReportedKey(string call, int number) => $"{call}|{number}";
private string ContinentOf(Callsign call) =>