Copy the statistics table and graph to the clipboard
Copy All takes whichever tab is open, as N1MM's does: the table off the table tab, a picture of the graph off the graph tab. Copy Table as Text and Copy Graph as Image do either one from either tab. `StatisticsTable` lays the pivot out as the window prints it — the headings, the rows, the Total line — and pads it into columns for the text form, so the grid, the clipboard and Print to File all say the same thing. X11 asks for a picture when the other program pastes rather than when it is copied, so the bitmap given to the clipboard is held until the clipboard is given a different one. Disposing it at the end of the copy killed the program on the next paste; the table pasted into a second Avalonia program reads back lined up, and the picture reads back as a 916x438 bitmap. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PdAYHcdRktqKry7nk414TU
This commit is contained in:
@@ -49,7 +49,7 @@ scorer: red for a dupe, green for a new multiplier, blue for points.
|
|||||||
| While typing | dupe check, multiplier check, points, country and zone from the country file, exchange filled from a call history file |
|
| While typing | dupe check, multiplier check, points, country and zone from the country file, exchange filled from a call history file |
|
||||||
| While transmitting | a digital message is held in the transmit pane and fed to the engine a character at a time, so what has not gone out can still be rewritten; what has gone out is red, and the engine idles when it catches up with the cursor |
|
| While transmitting | a digital message is held in the transmit pane and fed to the engine a character at a time, so what has not gone out can still be rewritten; what has gone out is red, and the engine idles when it catches up with the cursor |
|
||||||
| Windows | entry, log, check, bandmap, available mults and Qs, score summary, statistics, grey line, telnet, digital interface |
|
| Windows | entry, log, check, bandmap, available mults and Qs, score summary, statistics, grey line, telnet, digital interface |
|
||||||
| Statistics | the log counted under any two of its fields, as N1MM's table with its `Tot` and `Accum` columns or as a stacked column graph |
|
| Statistics | the log counted under any two of its fields, as N1MM's table with its `Tot` and `Accum` columns or as a stacked column graph; either one copies to the clipboard, as spaced text or as a picture |
|
||||||
| Editing | double-click a cell in the log, or open the whole contact with Ctrl+Y; Delete removes it. All of it goes out to the other stations |
|
| Editing | double-click a cell in the log, or open the whole contact with Ctrl+Y; Delete removes it. All of it goes out to the other stations |
|
||||||
| Radio | one or two radios over hamlib `rigctld`, split, reconnecting on its own |
|
| Radio | one or two radios over hamlib `rigctld`, split, reconnecting on its own |
|
||||||
| Cluster | DX cluster over telnet: a telnet window with the node's traffic, the published list of nodes, command buttons and spot filters, spots feeding the bandmap, Alt+P to spot a station |
|
| Cluster | DX cluster over telnet: a telnet window with the node's traffic, the published list of nodes, command buttons and spot filters, spots feeding the bandmap, Alt+P to spot a station |
|
||||||
|
|||||||
+11
-6
@@ -668,12 +668,17 @@ What still differs, and why:
|
|||||||
the log under any two of its fields, in N1MM's own two lists — 29 fields down
|
the log under any two of its fields, in N1MM's own two lists — 29 fields down
|
||||||
the rows, 20 of them across the columns — with N1MM's `Tot` and `Accum` columns,
|
the rows, 20 of them across the columns — with N1MM's `Tot` and `Accum` columns,
|
||||||
its `Total` line, its "Include Dupes & X-QSOs" box and its stacked column graph.
|
its `Total` line, its "Include Dupes & X-QSOs" box and its stacked column graph.
|
||||||
Left out: printing to a printer, which needs a print dialog this program has
|
Copy All takes whichever tab is open, as N1MM's does: the table as spaced text
|
||||||
nowhere else, and N1MM's font size spinner, because the theme dialog sets the
|
off the table tab, a picture of the graph off the graph tab. File ▸ Copy Table
|
||||||
fonts here. Copy All copies the table as text on both tabs; N1MM copies the
|
as Text and File ▸ Copy Graph as Image do either from either tab, and File ▸
|
||||||
graph as an image on the graph tab, which the clipboard on Linux would need a
|
Save Graph writes the picture to a PNG. Left out: printing to a printer, which
|
||||||
per-toolkit path for, so the graph is written to a PNG through File ▸ Save Graph
|
needs a print dialog this program has nowhere else, and N1MM's font size
|
||||||
instead.
|
spinner, because the theme dialog sets the fonts here.
|
||||||
|
|
||||||
|
X11 hands over a picture only when the other program pastes, not when it is
|
||||||
|
copied, so `StatisticsWindow` holds the bitmap it gave the clipboard until it
|
||||||
|
gives it a different one. Disposing it at the end of the copy killed the
|
||||||
|
program on the next paste.
|
||||||
|
|
||||||
The palette names are N1MM's twelve, which are the .NET chart control's. The
|
The palette names are N1MM's twelve, which are the .NET chart control's. The
|
||||||
colours are not: they are chosen to match the name, because the control's own
|
colours are not: they are chosen to match the name, because the control's own
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
<Menu DockPanel.Dock="Top">
|
<Menu DockPanel.Dock="Top">
|
||||||
<MenuItem Header="_File">
|
<MenuItem Header="_File">
|
||||||
<MenuItem Header="Copy All" Click="OnCopyAll" InputGesture="Ctrl+C" />
|
<MenuItem Header="Copy All" Click="OnCopyAll" InputGesture="Ctrl+C" />
|
||||||
|
<MenuItem Header="Copy Table as Text" Click="OnCopyTable" />
|
||||||
|
<MenuItem Header="Copy Graph as Image" Click="OnCopyGraph" />
|
||||||
<MenuItem Name="PrintToFileItem" Header="Print to File…" Click="OnPrintToFile" />
|
<MenuItem Name="PrintToFileItem" Header="Print to File…" Click="OnPrintToFile" />
|
||||||
<MenuItem Name="SaveGraphItem" Header="Save Graph…" Click="OnSaveGraph" />
|
<MenuItem Name="SaveGraphItem" Header="Save Graph…" Click="OnSaveGraph" />
|
||||||
<MenuItem Name="ShowTitleItem" Header="Show Title" ToggleType="CheckBox"
|
<MenuItem Name="ShowTitleItem" Header="Show Title" ToggleType="CheckBox"
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
/// does not count as the operator choosing it.
|
/// does not count as the operator choosing it.
|
||||||
private bool loading = true;
|
private bool loading = true;
|
||||||
|
|
||||||
|
/// The table as it is printed, kept so the copy and print actions do not
|
||||||
|
/// read the values back out of the grid.
|
||||||
|
private StatisticsTable? laid;
|
||||||
|
|
||||||
|
/// The picture the clipboard is showing, if any.
|
||||||
|
private RenderTargetBitmap? copied;
|
||||||
|
|
||||||
public StatisticsWindow(AppSession session)
|
public StatisticsWindow(AppSession session)
|
||||||
{
|
{
|
||||||
this.session = session;
|
this.session = session;
|
||||||
@@ -66,6 +73,7 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
Chart.Columns = [];
|
Chart.Columns = [];
|
||||||
Chart.Series = [];
|
Chart.Series = [];
|
||||||
Chart.InvalidateVisual();
|
Chart.InvalidateVisual();
|
||||||
|
laid = null;
|
||||||
Empty("no contest is open");
|
Empty("no contest is open");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -86,6 +94,7 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
Chart.Columns = [];
|
Chart.Columns = [];
|
||||||
Chart.Series = [];
|
Chart.Series = [];
|
||||||
Chart.InvalidateVisual();
|
Chart.InvalidateVisual();
|
||||||
|
laid = null;
|
||||||
Empty("no contacts are counted");
|
Empty("no contacts are counted");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -104,63 +113,22 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
|
|
||||||
private void FillTable(StatisticsPivot pivot)
|
private void FillTable(StatisticsPivot pivot)
|
||||||
{
|
{
|
||||||
List<string> headers =
|
laid = StatisticsTable.Of(pivot);
|
||||||
[
|
|
||||||
.. pivot.Row.Headings,
|
|
||||||
.. pivot.Columns.Select(c => c.Label.Length > 0 ? c.Label : "(none)"),
|
|
||||||
"Tot",
|
|
||||||
"Accum",
|
|
||||||
];
|
|
||||||
Table.Columns.Clear();
|
Table.Columns.Clear();
|
||||||
for (int at = 0; at < headers.Count; at++)
|
for (int at = 0; at < laid.Headings.Count; at++)
|
||||||
{
|
{
|
||||||
Table.Columns.Add(new DataGridTextColumn
|
Table.Columns.Add(new DataGridTextColumn
|
||||||
{
|
{
|
||||||
Header = headers[at],
|
Header = laid.Headings[at],
|
||||||
Width = DataGridLength.Auto,
|
Width = DataGridLength.Auto,
|
||||||
Binding = new Binding($"[{at}]"),
|
Binding = new Binding($"[{at}]"),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Table.ItemsSource = Lines(pivot);
|
Table.ItemsSource = laid.Rows
|
||||||
|
.Select((cells, at) => new StatisticsRow(cells, isTotal: at == laid.Rows.Count - 1))
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<StatisticsRow> Lines(StatisticsPivot pivot)
|
|
||||||
{
|
|
||||||
List<StatisticsRow> lines = [];
|
|
||||||
int labels = pivot.Row.Headings.Count;
|
|
||||||
for (int down = 0; down < pivot.Rows.Count; down++)
|
|
||||||
{
|
|
||||||
List<string> cells = [pivot.Rows[down].First];
|
|
||||||
if (labels == 2)
|
|
||||||
{
|
|
||||||
cells.Add(pivot.Rows[down].Second);
|
|
||||||
}
|
|
||||||
for (int across = 0; across < pivot.Columns.Count; across++)
|
|
||||||
{
|
|
||||||
cells.Add(Number(pivot.Count(down, across)));
|
|
||||||
}
|
|
||||||
cells.Add(Number(pivot.RowTotals[down]));
|
|
||||||
cells.Add(Number(pivot.Accumulated[down]));
|
|
||||||
lines.Add(new StatisticsRow(cells));
|
|
||||||
}
|
|
||||||
|
|
||||||
List<string> total = ["Total"];
|
|
||||||
if (labels == 2)
|
|
||||||
{
|
|
||||||
total.Add("");
|
|
||||||
}
|
|
||||||
total.AddRange(pivot.ColumnTotals.Select(Number));
|
|
||||||
total.Add(Number(pivot.Total));
|
|
||||||
total.Add(Number(pivot.Total));
|
|
||||||
lines.Add(new StatisticsRow(total, isTotal: true));
|
|
||||||
return lines;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// N1MM leaves a cell with nothing in it blank rather than printing a zero,
|
|
||||||
/// so the shape of the operating shows through the table.
|
|
||||||
private static string Number(int count) =>
|
|
||||||
count == 0 ? "" : count.ToString(CultureInfo.InvariantCulture);
|
|
||||||
|
|
||||||
private void FillChart(StatisticsPivot pivot)
|
private void FillChart(StatisticsPivot pivot)
|
||||||
{
|
{
|
||||||
Chart.Series = [.. pivot.Columns.Select(c => c.Label)];
|
Chart.Series = [.. pivot.Columns.Select(c => c.Label)];
|
||||||
@@ -249,12 +217,43 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
Refresh();
|
Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// N1MM's Copy All takes whichever tab is open: the table off the table
|
||||||
|
/// tab, a picture of the graph off the graph tab.
|
||||||
private async void OnCopyAll(object? sender, RoutedEventArgs e)
|
private async void OnCopyAll(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (Clipboard is { } clipboard)
|
if (Tabs.SelectedIndex == 1)
|
||||||
{
|
{
|
||||||
await clipboard.SetTextAsync(AsText(", "));
|
await CopyGraph();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
await CopyTable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnCopyTable(object? sender, RoutedEventArgs e) => await CopyTable();
|
||||||
|
|
||||||
|
private async void OnCopyGraph(object? sender, RoutedEventArgs e) => await CopyGraph();
|
||||||
|
|
||||||
|
private async Task CopyTable()
|
||||||
|
{
|
||||||
|
if (Clipboard is { } clipboard && laid is not null)
|
||||||
|
{
|
||||||
|
await clipboard.SetTextAsync(laid.ToText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CopyGraph()
|
||||||
|
{
|
||||||
|
if (Clipboard is not { } clipboard)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
RenderTargetBitmap picture = Picture();
|
||||||
|
await clipboard.SetBitmapAsync(picture);
|
||||||
|
// X11 does not take a copy: it asks this program for the bytes when
|
||||||
|
// another one pastes, so the picture has to stay alive until the
|
||||||
|
// clipboard holds a different one
|
||||||
|
copied?.Dispose();
|
||||||
|
copied = picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnPrintToFile(object? sender, RoutedEventArgs e)
|
private async void OnPrintToFile(object? sender, RoutedEventArgs e)
|
||||||
@@ -265,9 +264,9 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
SuggestedFileName = "statistics.txt",
|
SuggestedFileName = "statistics.txt",
|
||||||
SuggestedStartLocation = await Folder(session.Paths.ExportFiles),
|
SuggestedStartLocation = await Folder(session.Paths.ExportFiles),
|
||||||
});
|
});
|
||||||
if (file?.TryGetLocalPath() is { } path)
|
if (file?.TryGetLocalPath() is { } path && laid is not null)
|
||||||
{
|
{
|
||||||
await File.WriteAllTextAsync(path, AsText("\t"));
|
await File.WriteAllTextAsync(path, laid.ToText());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,30 +282,21 @@ public sealed partial class StatisticsWindow : RefreshableWindow
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
using RenderTargetBitmap picture = Picture();
|
||||||
|
picture.Save(path, new PngBitmapEncoderOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The graph as a picture, at the size it is on the screen.
|
||||||
|
private RenderTargetBitmap Picture()
|
||||||
|
{
|
||||||
PixelSize size = Chart.Bounds.Width >= 1 && Chart.Bounds.Height >= 1
|
PixelSize size = Chart.Bounds.Width >= 1 && Chart.Bounds.Height >= 1
|
||||||
? new PixelSize((int)Chart.Bounds.Width, (int)Chart.Bounds.Height)
|
? new PixelSize((int)Chart.Bounds.Width, (int)Chart.Bounds.Height)
|
||||||
: GraphSize;
|
: GraphSize;
|
||||||
using RenderTargetBitmap bitmap = new(size);
|
RenderTargetBitmap picture = new(size);
|
||||||
bitmap.Render(Chart);
|
picture.Render(Chart);
|
||||||
bitmap.Save(path, new PngBitmapEncoderOptions());
|
return picture;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IStorageFolder?> Folder(string path) =>
|
private async Task<IStorageFolder?> Folder(string path) =>
|
||||||
Directory.Exists(path) ? await StorageProvider.TryGetFolderFromPathAsync(path) : null;
|
Directory.Exists(path) ? await StorageProvider.TryGetFolderFromPathAsync(path) : null;
|
||||||
|
|
||||||
/// The table as text, one line per row. N1MM's Copy All separates the
|
|
||||||
/// values with commas and its Print to File lines the columns up.
|
|
||||||
private string AsText(string between)
|
|
||||||
{
|
|
||||||
StringBuilder text = new();
|
|
||||||
text.AppendLine(string.Join(between, Table.Columns.Select(c => c.Header)));
|
|
||||||
if (Table.ItemsSource is IEnumerable<StatisticsRow> lines)
|
|
||||||
{
|
|
||||||
foreach (StatisticsRow line in lines)
|
|
||||||
{
|
|
||||||
text.AppendLine(string.Join(between, line.Cells));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return text.ToString();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nonemm.Session;
|
||||||
|
|
||||||
|
/// The pivot laid out the way the statistics window prints it: N1MM's headings,
|
||||||
|
/// one line per row with the label columns first and the `Tot` and `Accum`
|
||||||
|
/// columns last, and the `Total` line at the bottom.
|
||||||
|
public sealed class StatisticsTable
|
||||||
|
{
|
||||||
|
/// What goes between two columns in the text form.
|
||||||
|
private const string Between = " ";
|
||||||
|
|
||||||
|
private StatisticsTable(
|
||||||
|
IReadOnlyList<string> headings,
|
||||||
|
IReadOnlyList<IReadOnlyList<string>> rows,
|
||||||
|
int labelColumns)
|
||||||
|
{
|
||||||
|
Headings = headings;
|
||||||
|
Rows = rows;
|
||||||
|
LabelColumns = labelColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static StatisticsTable Of(StatisticsPivot pivot)
|
||||||
|
{
|
||||||
|
List<string> headings =
|
||||||
|
[
|
||||||
|
.. pivot.Row.Headings,
|
||||||
|
.. pivot.Columns.Select(Heading),
|
||||||
|
"Tot",
|
||||||
|
"Accum",
|
||||||
|
];
|
||||||
|
int labels = pivot.Row.Headings.Count;
|
||||||
|
List<IReadOnlyList<string>> rows = [];
|
||||||
|
for (int down = 0; down < pivot.Rows.Count; down++)
|
||||||
|
{
|
||||||
|
List<string> cells = [pivot.Rows[down].First];
|
||||||
|
if (labels == 2)
|
||||||
|
{
|
||||||
|
cells.Add(pivot.Rows[down].Second);
|
||||||
|
}
|
||||||
|
for (int across = 0; across < pivot.Columns.Count; across++)
|
||||||
|
{
|
||||||
|
cells.Add(Count(pivot.Count(down, across)));
|
||||||
|
}
|
||||||
|
cells.Add(Count(pivot.RowTotals[down]));
|
||||||
|
cells.Add(Count(pivot.Accumulated[down]));
|
||||||
|
rows.Add(cells);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<string> total = ["Total"];
|
||||||
|
if (labels == 2)
|
||||||
|
{
|
||||||
|
total.Add("");
|
||||||
|
}
|
||||||
|
total.AddRange(pivot.ColumnTotals.Select(Count));
|
||||||
|
total.Add(Count(pivot.Total));
|
||||||
|
total.Add(Count(pivot.Total));
|
||||||
|
rows.Add(total);
|
||||||
|
return new StatisticsTable(headings, rows, labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Headings { get; }
|
||||||
|
|
||||||
|
/// Every line, the `Total` line last.
|
||||||
|
public IReadOnlyList<IReadOnlyList<string>> Rows { get; }
|
||||||
|
|
||||||
|
/// How many columns at the left hold the row's own value rather than a
|
||||||
|
/// count. Two for Hour and Mode & Band, one for every other field.
|
||||||
|
public int LabelColumns { get; }
|
||||||
|
|
||||||
|
/// The table with spaces between the columns, so it lines up under a
|
||||||
|
/// monospace font. The label columns are left-aligned and the counts are
|
||||||
|
/// right-aligned, as they are on the screen.
|
||||||
|
public string ToText()
|
||||||
|
{
|
||||||
|
int[] widths = new int[Headings.Count];
|
||||||
|
for (int at = 0; at < Headings.Count; at++)
|
||||||
|
{
|
||||||
|
widths[at] = Math.Max(Headings[at].Length, Rows.Max(r => At(r, at).Length));
|
||||||
|
}
|
||||||
|
StringBuilder text = new();
|
||||||
|
Line(text, Headings, widths);
|
||||||
|
foreach (IReadOnlyList<string> row in Rows)
|
||||||
|
{
|
||||||
|
Line(text, row, widths);
|
||||||
|
}
|
||||||
|
return text.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Line(StringBuilder text, IReadOnlyList<string> cells, int[] widths)
|
||||||
|
{
|
||||||
|
for (int at = 0; at < widths.Length; at++)
|
||||||
|
{
|
||||||
|
string cell = At(cells, at);
|
||||||
|
text.Append(at < LabelColumns ? cell.PadRight(widths[at]) : cell.PadLeft(widths[at]));
|
||||||
|
if (at < widths.Length - 1)
|
||||||
|
{
|
||||||
|
text.Append(Between);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// a row of blanks at the right would otherwise leave the line ragged
|
||||||
|
while (text.Length > 0 && text[^1] == ' ')
|
||||||
|
{
|
||||||
|
text.Length--;
|
||||||
|
}
|
||||||
|
text.Append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string At(IReadOnlyList<string> cells, int at) => at < cells.Count ? cells[at] : "";
|
||||||
|
|
||||||
|
/// A column with no value of its own is headed the way the window heads it,
|
||||||
|
/// so the text and the screen say the same thing.
|
||||||
|
private static string Heading(PivotKey column) =>
|
||||||
|
column.Label.Length > 0 ? column.Label : "(none)";
|
||||||
|
|
||||||
|
/// N1MM leaves a cell with nothing in it blank rather than printing a zero,
|
||||||
|
/// so the shape of the operating shows through the table.
|
||||||
|
private static string Count(int count) => count == 0 ? "" : count.ToString();
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using Nonemm.Core;
|
||||||
|
|
||||||
|
namespace Nonemm.Session.Tests;
|
||||||
|
|
||||||
|
public class StatisticsTableTests
|
||||||
|
{
|
||||||
|
private static readonly DateTime Start = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
private static Qso Contact(double kilohertz, Mode? mode = null, int minute = 0) => new()
|
||||||
|
{
|
||||||
|
Id = Qso.NewId(),
|
||||||
|
TimestampUtc = Start.AddMinutes(minute),
|
||||||
|
Call = Callsign.Parse("DL1ABC"),
|
||||||
|
Frequency = Frequency.FromKilohertz(kilohertz),
|
||||||
|
Mode = mode ?? Modes.Cw,
|
||||||
|
ContestName = "CQWW",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static StatisticsTable Of(IReadOnlyList<Qso> log, string row = "Band", string column = "Mode") =>
|
||||||
|
StatisticsTable.Of(StatisticsPivot.Of(
|
||||||
|
log,
|
||||||
|
StatisticsFields.Find(StatisticsFields.Rows, row),
|
||||||
|
StatisticsFields.Find(StatisticsFields.Columns, column),
|
||||||
|
Start));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void HeadsTheCountsWithTheColumnValuesAndEndsWithTotAndAccum()
|
||||||
|
{
|
||||||
|
StatisticsTable table = Of([Contact(14_030), Contact(7_030, Modes.Usb)]);
|
||||||
|
|
||||||
|
Assert.Equal(["Band", "CW", "USB", "Tot", "Accum"], table.Headings);
|
||||||
|
Assert.Equal(1, table.LabelColumns);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PutsTheTotalLineLast()
|
||||||
|
{
|
||||||
|
StatisticsTable table = Of([Contact(14_030), Contact(14_040), Contact(7_030)]);
|
||||||
|
|
||||||
|
Assert.Equal(["40M", "1", "1", "1"], table.Rows[0]);
|
||||||
|
Assert.Equal(["20M", "2", "2", "3"], table.Rows[1]);
|
||||||
|
Assert.Equal(["Total", "3", "3", "3"], table.Rows[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LinesTheColumnsUpUnderTheirHeadings()
|
||||||
|
{
|
||||||
|
string text = Of([Contact(14_030), Contact(3_530, Modes.Usb), Contact(3_540, Modes.Usb)]).ToText();
|
||||||
|
|
||||||
|
Assert.Equal(
|
||||||
|
"""
|
||||||
|
Band CW USB Tot Accum
|
||||||
|
80M 2 2 2
|
||||||
|
20M 1 1 3
|
||||||
|
Total 1 2 3 3
|
||||||
|
""",
|
||||||
|
text.TrimEnd('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KeepsTheTwoLabelColumnsOfAFieldThatHasThem()
|
||||||
|
{
|
||||||
|
StatisticsTable table = Of([Contact(14_030, minute: 5), Contact(14_040, minute: 70)], row: "Hour");
|
||||||
|
|
||||||
|
Assert.Equal(2, table.LabelColumns);
|
||||||
|
Assert.Equal(["Day", "Hr", "CW", "Tot", "Accum"], table.Headings);
|
||||||
|
Assert.Equal(["2026-01-01", "00", "1", "1", "1"], table.Rows[0]);
|
||||||
|
Assert.Equal(["Total", "", "2", "2", "2"], table.Rows[^1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void LeavesNoSpacesHangingOffTheEndOfALine()
|
||||||
|
{
|
||||||
|
string text = Of([Contact(14_030)]).ToText();
|
||||||
|
|
||||||
|
Assert.All(text.Split('\n'), line => Assert.Equal(line.TrimEnd(), line));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user