Count the log under any two of its fields

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
This commit is contained in:
2026-09-04 07:47:30 +00:00
co-authored by Claude Opus 5
parent 4eb8071d57
commit acd22a9b5e
15 changed files with 1145 additions and 1 deletions
@@ -0,0 +1,166 @@
using Nonemm.Core;
namespace Nonemm.Session.Tests;
public class StatisticsPivotTests
{
private static readonly DateTime Start = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static Qso Contact(
string call = "DL1ABC",
double kilohertz = 14_030,
Mode? mode = null,
int minute = 0,
int points = 1,
string operatorCall = "OM3ABC")
{
return new Qso
{
Id = Qso.NewId(),
TimestampUtc = Start.AddMinutes(minute),
Call = Callsign.Parse(call),
Frequency = Frequency.FromKilohertz(kilohertz),
Mode = mode ?? Modes.Cw,
ContestName = "CQWW",
Points = points,
Operator = operatorCall,
};
}
private static StatisticsPivot Of(
IReadOnlyList<Qso> qsos,
string row,
string column,
bool includeDupes = false)
{
return StatisticsPivot.Of(
qsos,
StatisticsFields.Find(StatisticsFields.Rows, row),
StatisticsFields.Find(StatisticsFields.Columns, column),
Start,
includeDupes);
}
[Fact]
public void CountsTheContactsUnderEachPairOfValues()
{
List<Qso> log =
[
Contact(kilohertz: 14_030),
Contact(kilohertz: 14_040),
Contact(kilohertz: 7_030),
Contact(kilohertz: 7_040, mode: Modes.Usb),
];
StatisticsPivot pivot = Of(log, "Band", "Mode");
Assert.Equal(["40M", "20M"], pivot.Rows.Select(r => r.Label));
Assert.Equal(["CW", "USB"], pivot.Columns.Select(c => c.Label));
Assert.Equal(1, pivot.Count(0, 0));
Assert.Equal(1, pivot.Count(0, 1));
Assert.Equal(2, pivot.Count(1, 0));
Assert.Equal(0, pivot.Count(1, 1));
}
[Fact]
public void TotalsTheRowsTheColumnsAndTheWholeTable()
{
List<Qso> log =
[
Contact(kilohertz: 7_030),
Contact(kilohertz: 14_030),
Contact(kilohertz: 14_040),
Contact(kilohertz: 14_050, mode: Modes.Usb),
];
StatisticsPivot pivot = Of(log, "Band", "Mode");
Assert.Equal([1, 3], pivot.RowTotals);
Assert.Equal([1, 4], pivot.Accumulated);
Assert.Equal([3, 1], pivot.ColumnTotals);
Assert.Equal(4, pivot.Total);
}
[Fact]
public void SortsANumericFieldByItsNumberAndNotByItsText()
{
List<Qso> log = [Contact(points: 10), Contact(points: 2), Contact(points: 1)];
StatisticsPivot pivot = Of(log, "Points", "Mode");
Assert.Equal(["1", "2", "10"], pivot.Rows.Select(r => r.Label));
}
[Fact]
public void SortsABandByItsFrequencyAndNotByItsName()
{
List<Qso> log = [Contact(kilohertz: 14_030), Contact(kilohertz: 3_530), Contact(kilohertz: 7_030)];
StatisticsPivot pivot = Of(log, "Band", "Mode");
Assert.Equal(["80M", "40M", "20M"], pivot.Rows.Select(r => r.Label));
}
[Fact]
public void PutsTheDayAndTheHourInTwoLabelColumns()
{
List<Qso> log = [Contact(minute: 5), Contact(minute: 70), Contact(minute: 75)];
StatisticsPivot pivot = Of(log, "Hour", "Mode");
Assert.Equal(["Day", "Hr"], pivot.Row.Headings);
Assert.Equal([("2026-01-01", "00"), ("2026-01-01", "01")], pivot.Rows.Select(r => (r.First, r.Second)));
Assert.Equal([1, 2], pivot.RowTotals);
}
[Fact]
public void CountsTheMinuteFieldsFromTheStartOfTheContest()
{
List<Qso> log = [Contact(minute: 0), Contact(minute: 4), Contact(minute: 6), Contact(minute: 12)];
StatisticsPivot pivot = Of(log, "5 Minute", "Mode");
Assert.Equal(["0", "5", "10"], pivot.Rows.Select(r => r.Label));
Assert.Equal([2, 1, 1], pivot.RowTotals);
}
[Fact]
public void LeavesOutTheContactsNotInTheClaimedScoreUnlessTheyAreAskedFor()
{
List<Qso> log =
[
Contact(),
Contact() with { IsClaimed = false },
Contact() with { ContactType = "D" },
];
Assert.Equal(1, Of(log, "Band", "Mode").Total);
Assert.Equal(3, Of(log, "Band", "Mode", includeDupes: true).Total);
}
[Fact]
public void KeepsAFieldWithNoValueAsARowOfItsOwn()
{
List<Qso> log = [Contact(operatorCall: "OM3ABC"), Contact(operatorCall: "")];
StatisticsPivot pivot = Of(log, "Operator", "Mode");
Assert.Equal(["", "OM3ABC"], pivot.Rows.Select(r => r.Label));
}
[Fact]
public void SaysTheTableIsEmptyWhenNothingIsCounted()
{
Assert.True(Of([], "Band", "Mode").IsEmpty);
Assert.Equal(0, Of([], "Band", "Mode").Total);
}
[Fact]
public void OffersEveryRowFieldAsAColumnExceptTheOnesThatWouldNotFit()
{
Assert.Equal(29, StatisticsFields.Rows.Count);
Assert.Equal(20, StatisticsFields.Columns.Count);
Assert.DoesNotContain(StatisticsFields.Columns, f => f.Name == "Call");
Assert.Contains(StatisticsFields.Rows, f => f.Name == "Call");
}
}