Get WAE ready: the score split, the Cabrillo layouts and the QTC list
The score summary now splits the way N1MM's does. `ContestLog` keeps a tally per band, and per kind of row where the contest has kinds, and the score window reads those instead of adding the rows up itself. Three things were wrong with the old sum: - The band rows counted a multiplier as one whatever the band. WAE weights it by band — `ScoreRow.AddContact` adds four on 3.5, three on 7 and two on 14, 21 and 28 — so the band rows did not add up to the total. - The band rows counted contacts left out of the claimed score. - There was no split by kind. `CWAE.LogInfo` and `ScoreClass.Description` give WAE a `Q/QTC` column and a row per band and kind, so contacts, traffic sent and traffic received are read apart. The Cabrillo writer now pads the record name to seven characters, which is what `ConvertCabrilloStructStdToString` does and why: `X-QSO:` is six characters and a space, so every record name puts the frequency in the same column. Nonemm wrote five, and the `X-QSO:` lines added last commit did not line up with the rest. WAE's own layouts follow `CWAE.CabrilloString`: three spaces after the sent serial rather than the one every other column gets, and the transmitter column only for a multi-operator entry. `QtcTraffic.ToSend` no longer offers a contact marked as a dupe, which is what `DBSupport.QTCRecordsetCount` leaves out with `Trim(ContactType) = ''`. Checked by running the program against a WAE SSB log of six contacts and a series of ten received: the band rows read 4, 3, 2 and 2 and add up to the 11 in the total row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015NZkFxHde1iifAQemFTHxf
This commit is contained in:
@@ -502,6 +502,11 @@ or the same multiplier counts in full. It still goes into the Cabrillo file,
|
|||||||
under `X-QSO:` rather than `QSO:` — that is what N1MM writes, and the sponsors
|
under `X-QSO:` rather than `QSO:` — that is what N1MM writes, and the sponsors
|
||||||
read it as a contact the entrant is not claiming.
|
read it as a contact the entrant is not claiming.
|
||||||
|
|
||||||
|
Every Cabrillo line starts with its record name padded to seven characters, so
|
||||||
|
`QSO:`, `QTC:` and `X-QSO:` all put the frequency in the same column. That is
|
||||||
|
the width N1MM pads it to, and `X-QSO:` is the reason: six characters and one
|
||||||
|
space.
|
||||||
|
|
||||||
Correcting the country prefix by hand changes the score. The country file is a
|
Correcting the country prefix by hand changes the score. The country file is a
|
||||||
best guess for calls it has no rule for, so what the contact says now wins over
|
best guess for calls it has no rule for, so what the contact says now wins over
|
||||||
what the file says.
|
what the file says.
|
||||||
@@ -768,11 +773,23 @@ voice keyer and no digital window, so on those two modes the buttons move the
|
|||||||
cursor as they did before. N1MM's RTTY window also has a Send All button that
|
cursor as they did before. N1MM's RTTY window also has a Send All button that
|
||||||
keys the whole series at once; there is nothing to send it with here.
|
keys the whole series at once; there is nothing to send it with here.
|
||||||
|
|
||||||
In a WAE contest the log window has N1MM's **QTC?** column, which holds the
|
In a WAE contest the log window has N1MM's two columns, whose names do not read
|
||||||
series a QTC row belongs to and is empty for an ordinary contact.
|
the way they sound: **Series** says whether the row is a contact or traffic —
|
||||||
|
`QSO`, `SQTC` or `RQTC` — and **QTC?** holds the series a QTC row belongs to,
|
||||||
|
such as `3/10`.
|
||||||
|
|
||||||
|
The score summary splits the same way N1MM's does: contacts, traffic sent and
|
||||||
|
traffic received get a row each on every band, under a **Q/QTC** column, and a
|
||||||
|
band row carries that band's weight, so 80 metres reads 4 for one multiplier.
|
||||||
|
The band rows add up to the total.
|
||||||
|
|
||||||
The scoring was checked against six real WAE logs, and agrees with N1MM on
|
The scoring was checked against six real WAE logs, and agrees with N1MM on
|
||||||
points and on weighted multipliers to the contact.
|
points and on weighted multipliers to the contact. The Cabrillo layouts are
|
||||||
|
N1MM's, from `CWAE.CabrilloString`: a contact leaves three spaces after the sent
|
||||||
|
serial rather than the one every other column gets, a QTC line is written as a
|
||||||
|
`QTC:` record carrying the receiver, the series, the sender and then the time,
|
||||||
|
callsign and serial number being reported, and the transmitter column is written
|
||||||
|
only for a multi-operator entry.
|
||||||
|
|
||||||
### Band edges
|
### Band edges
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ namespace Nonemm.App.Windows;
|
|||||||
public sealed partial class ScoreWindow : RefreshableWindow
|
public sealed partial class ScoreWindow : RefreshableWindow
|
||||||
{
|
{
|
||||||
private readonly AppSession session;
|
private readonly AppSession session;
|
||||||
|
private int columnCount = 4;
|
||||||
|
|
||||||
public ScoreWindow(AppSession session)
|
public ScoreWindow(AppSession session)
|
||||||
{
|
{
|
||||||
@@ -28,33 +29,37 @@ public sealed partial class ScoreWindow : RefreshableWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Band> bands = session.Logging.Log.Qsos
|
// the log keeps the same split N1MM's score window shows, so the band
|
||||||
.Select(q => q.Band)
|
// rows carry the weighted multipliers and leave out what is not in the
|
||||||
.OfType<Band>()
|
// claimed score
|
||||||
.Distinct()
|
List<KeyValuePair<ScoreKey, ScoreTally>> lines = session.Logging.Log.Rows
|
||||||
.OrderBy(b => b.MegahertzLabel)
|
.OrderBy(r => Bands.Named(r.Key.Band)?.MegahertzLabel ?? double.MaxValue)
|
||||||
|
.ThenBy(r => r.Key.Kind, StringComparer.Ordinal)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
bool hasKinds = session.Logging.Contest.HasQtcTraffic;
|
||||||
|
|
||||||
foreach (string _ in new[] { "band", "qsos", "points", "mults" })
|
columnCount = hasKinds ? 5 : 4;
|
||||||
|
for (int column = 0; column < columnCount; column++)
|
||||||
{
|
{
|
||||||
Table.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
|
Table.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star));
|
||||||
}
|
}
|
||||||
AddRow(0, "Band", "QSOs", "Points", "Mults", header: true);
|
AddRow(0, "Band", "Q/QTC", "QSOs", "Points", "Mults", header: true);
|
||||||
|
|
||||||
int row = 1;
|
int row = 1;
|
||||||
foreach (Band band in bands)
|
foreach ((ScoreKey key, ScoreTally line) in lines)
|
||||||
{
|
{
|
||||||
IReadOnlyList<Qso> onBand = session.Logging.Log.Qsos.Where(q => q.Band == band).ToList();
|
|
||||||
AddRow(
|
AddRow(
|
||||||
row++,
|
row++,
|
||||||
band.Name,
|
key.Band,
|
||||||
onBand.Count.ToString(),
|
key.Kind,
|
||||||
onBand.Sum(q => q.Points).ToString(),
|
line.Qsos.ToString(),
|
||||||
onBand.Sum(MultiplierCount).ToString());
|
line.Points.ToString(),
|
||||||
|
line.TotalMultipliers.ToString());
|
||||||
}
|
}
|
||||||
AddRow(
|
AddRow(
|
||||||
row,
|
row,
|
||||||
"Total",
|
"Total",
|
||||||
|
hasKinds ? "All" : "",
|
||||||
session.Logging.Log.Tally.Qsos.ToString(),
|
session.Logging.Log.Tally.Qsos.ToString(),
|
||||||
session.Logging.Log.Tally.Points.ToString(),
|
session.Logging.Log.Tally.Points.ToString(),
|
||||||
session.Logging.Log.Tally.TotalMultipliers.ToString(),
|
session.Logging.Log.Tally.TotalMultipliers.ToString(),
|
||||||
@@ -86,13 +91,19 @@ public sealed partial class ScoreWindow : RefreshableWindow
|
|||||||
private static string Hours(TimeSpan time) =>
|
private static string Hours(TimeSpan time) =>
|
||||||
$"{(int)time.TotalHours}:{time.Minutes:00}";
|
$"{(int)time.TotalHours}:{time.Minutes:00}";
|
||||||
|
|
||||||
private static int MultiplierCount(Qso qso) =>
|
private void AddRow(
|
||||||
(qso.IsMultiplier1 ? 1 : 0) + (qso.IsMultiplier2 ? 1 : 0) + (qso.IsMultiplier3 ? 1 : 0);
|
int row,
|
||||||
|
string band,
|
||||||
private void AddRow(int row, string band, string qsos, string points, string mults, bool header = false)
|
string kind,
|
||||||
|
string qsos,
|
||||||
|
string points,
|
||||||
|
string mults,
|
||||||
|
bool header = false)
|
||||||
{
|
{
|
||||||
Table.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
Table.RowDefinitions.Add(new RowDefinition(GridLength.Auto));
|
||||||
string[] cells = [band, qsos, points, mults];
|
string[] cells = columnCount == 5
|
||||||
|
? [band, kind, qsos, points, mults]
|
||||||
|
: [band, qsos, points, mults];
|
||||||
for (int column = 0; column < cells.Length; column++)
|
for (int column = 0; column < cells.Length; column++)
|
||||||
{
|
{
|
||||||
TextBlock text = new()
|
TextBlock text = new()
|
||||||
|
|||||||
@@ -106,6 +106,12 @@ public interface Contest
|
|||||||
/// The log window then shows the series they belong to.
|
/// The log window then shows the series they belong to.
|
||||||
bool HasQtcTraffic => false;
|
bool HasQtcTraffic => false;
|
||||||
|
|
||||||
|
/// Which kind of row this is, for a contest whose score is broken out by
|
||||||
|
/// kind. WAE separates contacts from the traffic sent and received, and
|
||||||
|
/// N1MM's score window gives each kind a row of its own on every band.
|
||||||
|
/// Every other contest has one kind and the column is not shown.
|
||||||
|
string RowKindFor(Qso qso) => "";
|
||||||
|
|
||||||
string SentExchangeFor(StationInfo me);
|
string SentExchangeFor(StationInfo me);
|
||||||
|
|
||||||
int PointsFor(QsoContext qso);
|
int PointsFor(QsoContext qso);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public sealed class ContestLog
|
|||||||
private readonly List<Qso> qsos = [];
|
private readonly List<Qso> qsos = [];
|
||||||
private readonly Dictionary<string, DateTime> workedKeys = new(StringComparer.Ordinal);
|
private readonly Dictionary<string, DateTime> workedKeys = new(StringComparer.Ordinal);
|
||||||
private readonly HashSet<string> claimedMultipliers = new(StringComparer.Ordinal);
|
private readonly HashSet<string> claimedMultipliers = new(StringComparer.Ordinal);
|
||||||
|
private readonly Dictionary<ScoreKey, ScoreTally> rows = [];
|
||||||
private ScoreTally tally = new();
|
private ScoreTally tally = new();
|
||||||
|
|
||||||
public ContestLog(
|
public ContestLog(
|
||||||
@@ -42,6 +43,11 @@ public sealed class ContestLog
|
|||||||
|
|
||||||
public ScoreTally Tally => tally;
|
public ScoreTally Tally => tally;
|
||||||
|
|
||||||
|
/// The same score split the way N1MM's score window splits it: a row per
|
||||||
|
/// band, and per kind of row as well where the contest has kinds. The
|
||||||
|
/// rows add up to `Tally`.
|
||||||
|
public IReadOnlyDictionary<ScoreKey, ScoreTally> Rows => rows;
|
||||||
|
|
||||||
public int TotalScore => contest.TotalScore(tally, Entry);
|
public int TotalScore => contest.TotalScore(tally, Entry);
|
||||||
|
|
||||||
/// What logging this contact right now would do, without logging it.
|
/// What logging this contact right now would do, without logging it.
|
||||||
@@ -181,9 +187,11 @@ public sealed class ContestLog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
bool counts = qso.IsInScore;
|
bool counts = qso.IsInScore;
|
||||||
|
ScoreTally row = RowFor(qso);
|
||||||
if (counts)
|
if (counts)
|
||||||
{
|
{
|
||||||
tally.AddQso(verdict.Points);
|
tally.AddQso(verdict.Points);
|
||||||
|
row.AddQso(verdict.Points);
|
||||||
}
|
}
|
||||||
if (!contest.IsContact(qso))
|
if (!contest.IsContact(qso))
|
||||||
{
|
{
|
||||||
@@ -196,6 +204,7 @@ public sealed class ContestLog
|
|||||||
if (counts)
|
if (counts)
|
||||||
{
|
{
|
||||||
tally.AddMultiplier(multiplier);
|
tally.AddMultiplier(multiplier);
|
||||||
|
row.AddMultiplier(multiplier);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -208,10 +217,22 @@ public sealed class ContestLog
|
|||||||
|
|
||||||
/// Rescores the whole log. A removed or edited contact can hand its
|
/// Rescores the whole log. A removed or edited contact can hand its
|
||||||
/// multiplier to a later one, which only a full pass gets right.
|
/// multiplier to a later one, which only a full pass gets right.
|
||||||
|
private ScoreTally RowFor(Qso qso)
|
||||||
|
{
|
||||||
|
ScoreKey key = new(qso.Band?.Name ?? "", contest.RowKindFor(qso));
|
||||||
|
if (!rows.TryGetValue(key, out ScoreTally? row))
|
||||||
|
{
|
||||||
|
row = new ScoreTally();
|
||||||
|
rows[key] = row;
|
||||||
|
}
|
||||||
|
return row;
|
||||||
|
}
|
||||||
|
|
||||||
private void Rebuild()
|
private void Rebuild()
|
||||||
{
|
{
|
||||||
workedKeys.Clear();
|
workedKeys.Clear();
|
||||||
claimedMultipliers.Clear();
|
claimedMultipliers.Clear();
|
||||||
|
rows.Clear();
|
||||||
tally = new ScoreTally();
|
tally = new ScoreTally();
|
||||||
List<Qso> ordered = qsos.OrderBy(q => q.TimestampUtc).ToList();
|
List<Qso> ordered = qsos.OrderBy(q => q.TimestampUtc).ToList();
|
||||||
qsos.Clear();
|
qsos.Clear();
|
||||||
|
|||||||
@@ -65,6 +65,12 @@ public sealed class Wae : Contest
|
|||||||
|
|
||||||
public bool HasQtcTraffic => true;
|
public bool HasQtcTraffic => true;
|
||||||
|
|
||||||
|
/// N1MM's score window gives contacts, traffic sent and traffic received a
|
||||||
|
/// row each on every band, under the heading `Q/QTC`. The value is what the
|
||||||
|
/// `Exchange1` column holds.
|
||||||
|
public string RowKindFor(Qso qso) =>
|
||||||
|
WaeQtc.From(qso) is { } qtc ? (qtc.IsSent ? WaeQtc.Sent : WaeQtc.Received) : WaeQtc.Contact;
|
||||||
|
|
||||||
public string SentExchangeFor(StationInfo me) => "001";
|
public string SentExchangeFor(StationInfo me) => "001";
|
||||||
|
|
||||||
/// A QTC line counts, and so does every contact on a contest band. The
|
/// A QTC line counts, and so does every contact on a contest band. The
|
||||||
@@ -106,13 +112,20 @@ public sealed class Wae : Contest
|
|||||||
[
|
[
|
||||||
new CabrilloField(me.Callsign, 13),
|
new CabrilloField(me.Callsign, 13),
|
||||||
new CabrilloField(qso.SentReport, 3),
|
new CabrilloField(qso.SentReport, 3),
|
||||||
new CabrilloField($"{qso.SentNumber:0000}", 4),
|
// N1MM leaves three spaces after the sent serial rather
|
||||||
|
// than the one every other column gets
|
||||||
|
new CabrilloField($"{qso.SentNumber:0000}", 6),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
new CabrilloField(qso.Call.Text, 13),
|
new CabrilloField(qso.Call.Text, 13),
|
||||||
new CabrilloField(qso.ReceivedReport, 3),
|
new CabrilloField(qso.ReceivedReport, 3),
|
||||||
new CabrilloField($"{qso.ReceivedNumber:0000}", 4),
|
new CabrilloField($"{qso.ReceivedNumber:0000}", 4),
|
||||||
]);
|
])
|
||||||
|
{
|
||||||
|
// N1MM writes the transmitter column for a multi-operator
|
||||||
|
// entry and nothing at all for a single operator
|
||||||
|
AddsTransmitter = entry.OperatorCategory == "MULTI-OP",
|
||||||
|
};
|
||||||
}
|
}
|
||||||
string receiver = qtc.IsSent ? qtc.Station.Text : me.Callsign;
|
string receiver = qtc.IsSent ? qtc.Station.Text : me.Callsign;
|
||||||
string sender = qtc.IsSent ? me.Callsign : qtc.Station.Text;
|
string sender = qtc.IsSent ? me.Callsign : qtc.Station.Text;
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nonemm.Contests;
|
||||||
|
|
||||||
|
/// Which row of the score window a contact belongs to. `Kind` is empty for
|
||||||
|
/// every contest but WAE, which gives contacts, traffic sent and traffic
|
||||||
|
/// received a row each on every band.
|
||||||
|
public readonly record struct ScoreKey(string Band, string Kind);
|
||||||
@@ -62,6 +62,8 @@ public sealed class CabrilloWriter
|
|||||||
return text.ToString();
|
return text.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const int TagWidth = 7;
|
||||||
|
|
||||||
public string QsoLine(Qso qso)
|
public string QsoLine(Qso qso)
|
||||||
{
|
{
|
||||||
CabrilloExchange exchange = contest.CabrilloExchange(qso, me, entry);
|
CabrilloExchange exchange = contest.CabrilloExchange(qso, me, entry);
|
||||||
@@ -82,13 +84,17 @@ public sealed class CabrilloWriter
|
|||||||
/// A contact the operator has excluded from the claimed score is still
|
/// A contact the operator has excluded from the claimed score is still
|
||||||
/// written, under `X-QSO:` rather than `QSO:`. That is what N1MM writes,
|
/// written, under `X-QSO:` rather than `QSO:`. That is what N1MM writes,
|
||||||
/// and the sponsors read it as a contact the entrant is not claiming.
|
/// and the sponsors read it as a contact the entrant is not claiming.
|
||||||
|
///
|
||||||
|
/// The tag is padded to seven so that the columns after it line up whichever
|
||||||
|
/// tag the row carries. That is the width N1MM pads it to, and `X-QSO:` is
|
||||||
|
/// why: six characters and one space.
|
||||||
private string RecordName(Qso qso)
|
private string RecordName(Qso qso)
|
||||||
{
|
{
|
||||||
if (!contest.IsContact(qso))
|
if (!contest.IsContact(qso))
|
||||||
{
|
{
|
||||||
return "QTC: ";
|
return "QTC:".PadRight(TagWidth);
|
||||||
}
|
}
|
||||||
return qso.IsClaimed ? "QSO: " : "X-QSO: ";
|
return (qso.IsClaimed ? "QSO:" : "X-QSO:").PadRight(TagWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Every column is the width the sponsor asks for and one space, and a
|
/// Every column is the width the sponsor asks for and one space, and a
|
||||||
|
|||||||
@@ -245,9 +245,10 @@ public sealed class QsoEditor
|
|||||||
}
|
}
|
||||||
if (contest.HasQtcTraffic)
|
if (contest.HasQtcTraffic)
|
||||||
{
|
{
|
||||||
// N1MM's two headers for WAE: the exchange column says whether the
|
// N1MM's two headers for WAE, which do not read the way they
|
||||||
// row is a contact or QTC traffic — `QSO`, `SQTC` or `RQTC` — and
|
// sound: `Series` is the exchange column and says whether the row
|
||||||
// the misc column holds the series a QTC row belongs to
|
// is a contact or QTC traffic — `QSO`, `SQTC` or `RQTC` — and
|
||||||
|
// `QTC?` is the misc column and holds the series, such as `3/10`
|
||||||
columns.Add(new QsoColumn("Series", QsoField.Exchange1, 6));
|
columns.Add(new QsoColumn("Series", QsoField.Exchange1, 6));
|
||||||
columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6));
|
columns.Add(new QsoColumn("QTC?", QsoField.MiscText, 6));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -102,7 +102,8 @@ public sealed class QtcTraffic
|
|||||||
/// The contacts to report to this station: the ones not reported to anybody
|
/// The contacts to report to this station: the ones not reported to anybody
|
||||||
/// yet, oldest first, and never the station itself. Ten at a time unless
|
/// yet, oldest first, and never the station itself. Ten at a time unless
|
||||||
/// the operator asks for a shorter series, and fewer when the station has
|
/// the operator asks for a shorter series, and fewer when the station has
|
||||||
/// already had some.
|
/// already had some. A contact marked as a dupe is not offered, which is
|
||||||
|
/// what N1MM's `QTCRecordsetCount` leaves out.
|
||||||
public IReadOnlyList<WaeQtc> ToSend(Callsign other, int mostPerSeries = WaeQtc.MostPerSeries)
|
public IReadOnlyList<WaeQtc> ToSend(Callsign other, int mostPerSeries = WaeQtc.MostPerSeries)
|
||||||
{
|
{
|
||||||
int room = Math.Min(Math.Clamp(mostPerSeries, 1, WaeQtc.MostPerSeries), Remaining(other));
|
int room = Math.Min(Math.Clamp(mostPerSeries, 1, WaeQtc.MostPerSeries), Remaining(other));
|
||||||
@@ -116,6 +117,7 @@ public sealed class QtcTraffic
|
|||||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
List<Qso> worth = position.Log.Qsos
|
List<Qso> worth = position.Log.Qsos
|
||||||
.Where(q => !WaeQtc.IsQtc(q))
|
.Where(q => !WaeQtc.IsQtc(q))
|
||||||
|
.Where(q => q.ContactType.Length == 0)
|
||||||
.Where(q => !q.Call.Text.Equals(other.Text, StringComparison.OrdinalIgnoreCase))
|
.Where(q => !q.Call.Text.Equals(other.Text, StringComparison.OrdinalIgnoreCase))
|
||||||
.Where(q => !reported.Contains(ReportedKey(q.Call.Text, q.SentNumber)))
|
.Where(q => !reported.Contains(ReportedKey(q.Call.Text, q.SentNumber)))
|
||||||
.OrderBy(q => q.TimestampUtc)
|
.OrderBy(q => q.TimestampUtc)
|
||||||
|
|||||||
@@ -115,6 +115,32 @@ public class WaeTests
|
|||||||
Assert.Empty(log.Judge(On("D2X", 14_025)).NewMultipliers);
|
Assert.Empty(log.Judge(On("D2X", 14_025)).NewMultipliers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM's score window gives contacts, traffic sent and traffic received a
|
||||||
|
/// row of their own on every band, and the multipliers in a band row carry
|
||||||
|
/// that band's weight: four on 80, three on 40, two on 20, 15 and 10.
|
||||||
|
[Fact]
|
||||||
|
public void TheScoreSplitsByBandAndByKindOfRow()
|
||||||
|
{
|
||||||
|
ContestLog log = LogFor(Cw, TestLog.Germany);
|
||||||
|
log.Add(On("JA1XYZ", 3_525));
|
||||||
|
log.Add(On("K1ABC", 14_025));
|
||||||
|
log.Add(new WaeQtc(
|
||||||
|
Callsign.Parse("JA1XYZ"), IsSent: false, 1, 10, "1435", Callsign.Parse("W1AW"), 42)
|
||||||
|
.ApplyTo(On("JA1XYZ", 3_525)));
|
||||||
|
|
||||||
|
Assert.Equal(4, log.Rows[new ScoreKey("80M", WaeQtc.Contact)].TotalMultipliers);
|
||||||
|
Assert.Equal(1, log.Rows[new ScoreKey("80M", WaeQtc.Contact)].Points);
|
||||||
|
Assert.Equal(2, log.Rows[new ScoreKey("20M", WaeQtc.Contact)].TotalMultipliers);
|
||||||
|
|
||||||
|
ScoreTally traffic = log.Rows[new ScoreKey("80M", WaeQtc.Received)];
|
||||||
|
Assert.Equal(1, traffic.Qsos);
|
||||||
|
Assert.Equal(1, traffic.Points);
|
||||||
|
Assert.Equal(0, traffic.TotalMultipliers);
|
||||||
|
|
||||||
|
Assert.Equal(log.Tally.Points, log.Rows.Values.Sum(r => r.Points));
|
||||||
|
Assert.Equal(log.Tally.TotalMultipliers, log.Rows.Values.Sum(r => r.TotalMultipliers));
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void AQtcIsAPointAndNothingElse()
|
public void AQtcIsAPointAndNothingElse()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -105,6 +105,41 @@ public class CabrilloWriterTests
|
|||||||
new CabrilloWriter(new YoDxHf(), Me).QsoLine(qso));
|
new CabrilloWriter(new YoDxHf(), Me).QsoLine(qso));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string WaeLine(string tag, string exchange) =>
|
||||||
|
tag.PadRight(7) + " 3799 PH 2026-05-30 1234 " + exchange;
|
||||||
|
|
||||||
|
/// WAE writes contacts and QTC traffic under different tags and in
|
||||||
|
/// different columns. N1MM's layouts are in `CWAE.CabrilloString`: a
|
||||||
|
/// contact leaves three spaces after the sent serial, and a QTC line is
|
||||||
|
/// the receiver, the series, the sender, then the time, callsign and
|
||||||
|
/// serial number of the contact being reported.
|
||||||
|
[Fact]
|
||||||
|
public void WaeWritesContactsAndTrafficUnderTheirOwnTags()
|
||||||
|
{
|
||||||
|
CabrilloWriter writer = new(new Wae(ModeCategory.Phone), Me);
|
||||||
|
Qso contact = Contact(3_799, Modes.Usb) with { SentNumber = 12, ReceivedNumber = 34 };
|
||||||
|
Assert.Equal(
|
||||||
|
WaeLine("QSO:", "DL1ABC 599 0012 JA1XYZ 599 0034 "),
|
||||||
|
writer.QsoLine(contact));
|
||||||
|
|
||||||
|
Qso sent = new WaeQtc(
|
||||||
|
Callsign.Parse("JA1XYZ"),
|
||||||
|
IsSent: true,
|
||||||
|
Series: 3,
|
||||||
|
CountInSeries: 10,
|
||||||
|
TimeUtc: "1201",
|
||||||
|
Call: Callsign.Parse("G3ABC"),
|
||||||
|
Number: 45).ApplyTo(contact);
|
||||||
|
Assert.Equal(
|
||||||
|
WaeLine("QTC:", "JA1XYZ 3/10 DL1ABC 1201 G3ABC 0045 "),
|
||||||
|
writer.QsoLine(sent));
|
||||||
|
|
||||||
|
Qso received = sent with { Exchange1 = WaeQtc.Received };
|
||||||
|
Assert.Equal(
|
||||||
|
WaeLine("QTC:", "DL1ABC 3/10 JA1XYZ 1201 G3ABC 0045 "),
|
||||||
|
writer.QsoLine(received));
|
||||||
|
}
|
||||||
|
|
||||||
/// The SA 10 file, whose sponsor asks for its own columns: the operator's
|
/// The SA 10 file, whose sponsor asks for its own columns: the operator's
|
||||||
/// call in the first 13, then every column padded to the width the file
|
/// call in the first 13, then every column padded to the width the file
|
||||||
/// gives, with one space after it.
|
/// gives, with one space after it.
|
||||||
|
|||||||
Reference in New Issue
Block a user