N1MM's statistics window. `StatisticsPivot` counts the contacts under one row field and one column field, with the row total, the running total and the column totals N1MM prints. The fields are N1MM's own two lists: 29 down the rows, 20 of them across the columns, sorted the way N1MM's SQL sorts them, so zone 10 comes after zone 2 and 80M before 20M. The window has N1MM's two tabs. The table is the grid with `Tot` and `Accum` and a `Total` line; the graph is a stacked column per row, one segment per column, in one of N1MM's twelve palettes. "Include Dupes & X-QSOs" counts the contacts left out of the claimed score and the ones marked as dupes. Checked against a 3775-contact CQ WW SSB log: 3762 counted, which is what `select count(*) ... where CLAIMEDQSO = 1 and ContactType <> 'D'` returns, and the per-band totals match band by band. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
103 lines
4.2 KiB
C#
103 lines
4.2 KiB
C#
using System.Globalization;
|
|
using Nonemm.Core;
|
|
|
|
namespace Nonemm.Session;
|
|
|
|
/// The fields N1MM's statistics window offers, in N1MM's own order. Every one
|
|
/// of them can go down the rows; nine of them cannot go across the columns,
|
|
/// because a column per call or per five minutes is wider than any screen.
|
|
public static class StatisticsFields
|
|
{
|
|
private static readonly string[] NotColumns =
|
|
[
|
|
"Hour", "CountryPrefix", "CK", "WPXPrefix", "MiscText",
|
|
"5 Minute", "10 Minute", "15 Minute", "Call",
|
|
];
|
|
|
|
public static readonly IReadOnlyList<StatisticsField> Rows =
|
|
[
|
|
new("Band", (q, _) => BandKey(q)),
|
|
Text("Operator", q => q.Operator),
|
|
new("Hour",
|
|
(q, _) => new PivotKey(
|
|
DayText(q),
|
|
q.TimestampUtc.ToString("HH", CultureInfo.InvariantCulture),
|
|
SecondOrder: q.TimestampUtc.Hour),
|
|
"Day",
|
|
"Hr"),
|
|
Number("RadioNr", q => q.RadioNumber),
|
|
Text("Mode", q => q.Mode.Name),
|
|
Text("CountryPrefix", q => q.CountryPrefix),
|
|
Number("Zone", q => q.Zone),
|
|
Text("Section", q => q.Section),
|
|
Number("CK", q => q.Check),
|
|
Text("Prec", q => q.Precedence),
|
|
Number("Points", q => q.Points),
|
|
Flag("Mult1", q => q.IsMultiplier1),
|
|
Flag("Mult2", q => q.IsMultiplier2),
|
|
Flag("Mult3", q => q.IsMultiplier3),
|
|
Text("WPXPrefix", q => q.WpxPrefix),
|
|
// N1MM counts a four-character field, so every station in a square
|
|
// lands in one row however precise a grid it sent
|
|
Text("GridSquare", q => q.GridSquare.Length > 4 ? q.GridSquare[..4] : q.GridSquare),
|
|
Number("Run1Run2", q => q.RunPosition),
|
|
Text("MiscText", q => q.MiscText),
|
|
Text("ContactType", q => q.ContactType),
|
|
Flag("IsRunQSO", q => q.IsRunQso),
|
|
new("Day", (q, _) => new PivotKey(DayText(q))),
|
|
Minutes(5),
|
|
Minutes(10),
|
|
Minutes(15),
|
|
new("Mode & Band",
|
|
(q, _) => new PivotKey(
|
|
q.Mode.Name,
|
|
q.Band?.Name ?? "",
|
|
SecondOrder: q.Band?.MegahertzLabel ?? 0),
|
|
"Mode",
|
|
"Band"),
|
|
Text("Exchange1", q => q.Exchange1),
|
|
Text("Call", q => q.Call.Text),
|
|
Text("RoverLocation", q => q.RoverLocation),
|
|
Text("Computer Name", q => q.StationName),
|
|
];
|
|
|
|
public static readonly IReadOnlyList<StatisticsField> Columns =
|
|
Rows.Where(f => !NotColumns.Contains(f.Name)).ToList();
|
|
|
|
/// The field of that name, or the first field when there is none: a name
|
|
/// read back out of the settings can be one this version no longer has.
|
|
public static StatisticsField Find(IReadOnlyList<StatisticsField> among, string name) =>
|
|
among.FirstOrDefault(f => f.Name == name) ?? among[0];
|
|
|
|
private static PivotKey BandKey(Qso qso) =>
|
|
new(qso.Band?.Name ?? "", Order: qso.Band?.MegahertzLabel ?? 0);
|
|
|
|
private static string DayText(Qso qso) =>
|
|
qso.TimestampUtc.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
|
|
|
private static StatisticsField Text(string name, Func<Qso, string> value) =>
|
|
new(name, (q, _) => new PivotKey(value(q)));
|
|
|
|
private static StatisticsField Number(string name, Func<Qso, int> value) =>
|
|
new(name, (q, _) =>
|
|
{
|
|
int found = value(q);
|
|
return new PivotKey(found.ToString(CultureInfo.InvariantCulture), Order: found);
|
|
});
|
|
|
|
/// N1MM reads the flag columns back as VB booleans, so the labels are
|
|
/// `True` and `False` and False sorts first.
|
|
private static StatisticsField Flag(string name, Func<Qso, bool> value) =>
|
|
new(name, (q, _) => value(q) ? new PivotKey("True", Order: 1) : new PivotKey("False"));
|
|
|
|
/// Minutes since the contest started, in steps of `span`. N1MM works this
|
|
/// out in SQLite with integer division, which cuts towards zero.
|
|
private static StatisticsField Minutes(int span) =>
|
|
new($"{span} Minute", (q, start) =>
|
|
{
|
|
long seconds = (long)(q.TimestampUtc - start).TotalSeconds;
|
|
long bucket = span * (seconds / (span * 60));
|
|
return new PivotKey(bucket.ToString(CultureInfo.InvariantCulture), Order: bucket);
|
|
});
|
|
}
|