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 rows, IReadOnlyList 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 qsos, StatisticsField row, StatisticsField column, DateTime startUtc, bool includeDupes = false) { Dictionary<(PivotKey Down, PivotKey Across), int> cells = []; SortedSet rows = []; SortedSet columns = []; foreach (Qso qso in qsos) { if (!includeDupes && !qso.IsInScore) { 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 rowKeys = [.. rows]; List 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 Rows { get; } public IReadOnlyList Columns { get; } /// The contacts under one row and one column. public int Count(int row, int column) => counts[row, column]; public IReadOnlyList RowTotals { get; } /// The row total plus every row above it, which is N1MM's `Accum` column. public IReadOnlyList Accumulated { get; } public IReadOnlyList ColumnTotals { get; } public int Total { get; } public bool IsEmpty => Rows.Count == 0; }