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:
@@ -0,0 +1,111 @@
|
||||
using Nonemm.Core;
|
||||
|
||||
namespace Nonemm.Session;
|
||||
|
||||
/// N1MM's statistics table: how many contacts fall under each pair of values of
|
||||
/// the two fields chosen, with the running total down the right and the column
|
||||
/// totals along the bottom.
|
||||
public sealed class StatisticsPivot
|
||||
{
|
||||
private readonly int[,] counts;
|
||||
|
||||
private StatisticsPivot(
|
||||
StatisticsField row,
|
||||
StatisticsField column,
|
||||
IReadOnlyList<PivotKey> rows,
|
||||
IReadOnlyList<PivotKey> columns,
|
||||
int[,] counts)
|
||||
{
|
||||
Row = row;
|
||||
Column = column;
|
||||
Rows = rows;
|
||||
Columns = columns;
|
||||
this.counts = counts;
|
||||
|
||||
int[] rowTotals = new int[rows.Count];
|
||||
int[] accumulated = new int[rows.Count];
|
||||
int[] columnTotals = new int[columns.Count];
|
||||
int running = 0;
|
||||
for (int down = 0; down < rows.Count; down++)
|
||||
{
|
||||
for (int across = 0; across < columns.Count; across++)
|
||||
{
|
||||
rowTotals[down] += counts[down, across];
|
||||
columnTotals[across] += counts[down, across];
|
||||
}
|
||||
running += rowTotals[down];
|
||||
accumulated[down] = running;
|
||||
}
|
||||
RowTotals = rowTotals;
|
||||
Accumulated = accumulated;
|
||||
ColumnTotals = columnTotals;
|
||||
Total = running;
|
||||
}
|
||||
|
||||
/// `includeDupes` is N1MM's "Include Dupes & X-QSOs": with it clear, a
|
||||
/// contact left out of the claimed score and a contact marked as a dupe are
|
||||
/// both left out of the count.
|
||||
public static StatisticsPivot Of(
|
||||
IReadOnlyList<Qso> qsos,
|
||||
StatisticsField row,
|
||||
StatisticsField column,
|
||||
DateTime startUtc,
|
||||
bool includeDupes = false)
|
||||
{
|
||||
Dictionary<(PivotKey Down, PivotKey Across), int> cells = [];
|
||||
SortedSet<PivotKey> rows = [];
|
||||
SortedSet<PivotKey> columns = [];
|
||||
foreach (Qso qso in qsos)
|
||||
{
|
||||
if (!includeDupes && !IsCounted(qso))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
PivotKey down = row.KeyOf(qso, startUtc);
|
||||
PivotKey across = column.KeyOf(qso, startUtc);
|
||||
rows.Add(down);
|
||||
columns.Add(across);
|
||||
cells[(down, across)] = cells.GetValueOrDefault((down, across)) + 1;
|
||||
}
|
||||
|
||||
List<PivotKey> rowKeys = [.. rows];
|
||||
List<PivotKey> columnKeys = [.. columns];
|
||||
int[,] counts = new int[rowKeys.Count, columnKeys.Count];
|
||||
for (int down = 0; down < rowKeys.Count; down++)
|
||||
{
|
||||
for (int across = 0; across < columnKeys.Count; across++)
|
||||
{
|
||||
counts[down, across] = cells.GetValueOrDefault((rowKeys[down], columnKeys[across]));
|
||||
}
|
||||
}
|
||||
return new StatisticsPivot(row, column, rowKeys, columnKeys, counts);
|
||||
}
|
||||
|
||||
public StatisticsField Row { get; }
|
||||
|
||||
public StatisticsField Column { get; }
|
||||
|
||||
public IReadOnlyList<PivotKey> Rows { get; }
|
||||
|
||||
public IReadOnlyList<PivotKey> Columns { get; }
|
||||
|
||||
/// The contacts under one row and one column.
|
||||
public int Count(int row, int column) => counts[row, column];
|
||||
|
||||
public IReadOnlyList<int> RowTotals { get; }
|
||||
|
||||
/// The row total plus every row above it, which is N1MM's `Accum` column.
|
||||
public IReadOnlyList<int> Accumulated { get; }
|
||||
|
||||
public IReadOnlyList<int> ColumnTotals { get; }
|
||||
|
||||
public int Total { get; }
|
||||
|
||||
public bool IsEmpty => Rows.Count == 0;
|
||||
|
||||
/// N1MM counts a contact when it is in the claimed score and is not marked
|
||||
/// as a dupe. Nothing in this program writes the `D` contact type, but a log
|
||||
/// N1MM wrote can carry it.
|
||||
private static bool IsCounted(Qso qso) =>
|
||||
qso.IsClaimed && !qso.ContactType.Equals("D", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
Reference in New Issue
Block a user