Analysis: read a rarely-read meter as coarse, not absent; newest rows first
ci / build-test (push) Successful in 2m41s
ci / build-test (push) Successful in 2m41s
Three things a reported Heizoel page got wrong at once. Its tank is dipped a few times a year and its burner read every few months, which is exactly the shape the coverage rules had not been walked through. "No data" for data that exists. A tank books nothing until the next dipstick closes the interval, so the stretch after the last dipstick is covered by no run at all, and a bucket no run covers was reported missing. The burner, whose run reaches into the window, said "only coarser data" -- the honest answer -- so one card claimed there was nothing while the coverage panel beside it listed years of data. A bucket that no run covers, no gap overlaps and no opening balance explains now reports the meter's resolution when its preceding coverage is within one interval of its own class: it is not silent, it is read rarely. A meter that does book its own buckets and stops -- a dead hourly source, a sheet asked about a later month -- still reads missing. Auto answering twelve months with one bar. Coarse only means "longer than a month", so a dipstick taken each autumn straddles a New Year as surely as a month start: coarsening the chart to years bought nothing and cost every point. The planning resolution now caps coarse at month when a run crosses a local year edge, and a series that cannot resolve the natural size no longer coarsens the whole chart -- it is drawn at that size with its buckets marked, which the chart and table already explain. A page contradicting itself. The comparison line above the ranking was fed the leading measure's matched coverage but worded as if it spoke for the page, directly above a burner row that did compare. It now names the figure it is about. Alongside: the "largest changes" ranking no longer drops a meter whose change is not comparable. It ranks what can be ranked, then lists the rest with their values and the reason -- the tank had been vanishing from its own energy type. And every dated table now reads newest first, as lists are read; charts stay chronological left to right, and the CSV export stays ascending for spreadsheets. A-41 to A-43 in the note record the three rules.
This commit is contained in:
@@ -42,9 +42,17 @@ public sealed record AnalysisCsvRow(
|
||||
/// or a script reads the same figures the page shows, never a fabricated zero.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Names and units are user data. A cell that starts with <c>=</c>, <c>+</c>, <c>-</c>, <c>@</c> or a control character
|
||||
/// is prefixed with an apostrophe, so a spreadsheet shows it as text instead of running it as a formula; numbers are
|
||||
/// written by this class and never need it.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Chronological, unlike the pages (A-42).</b> The rows stay oldest first, in the plan's own order. The tables on
|
||||
/// screen read newest first because a reader starts at the top; a CSV is not read, it is sorted, charted and
|
||||
/// differenced, and every tool that does so — a spreadsheet's chart, a running total, a diff against last month's
|
||||
/// file — expects time to run forwards. Reversing it here would only make every consumer sort it back.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public static class AnalysisCsvWriter
|
||||
{
|
||||
|
||||
@@ -201,14 +201,23 @@ public sealed record AnalysisTableRow(AnalysisBucket? Bucket, string Label, IRea
|
||||
/// says in words what the chart only marks.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// A change is stated per bucket only where both figures are complete — a partial bucket against a whole one is not a
|
||||
/// like-for-like change (D-07); the total row takes the reader's change over the matched coverage. Unknown values read
|
||||
/// "—", never 0.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>Newest first (A-42).</b> The rows are the reverse of the plan: the latest bucket is the first row, and the total
|
||||
/// row sits above them all. The chart stays chronological — it is read left to right — but a table is read top down,
|
||||
/// and what a reader wants first is the period they are in. The plan itself is never reordered: <see cref="Build"/>
|
||||
/// takes the buckets oldest first (as the reader returns them, paired with the comparison by index) and turns them
|
||||
/// round once, here, so nothing that indexes the plan has to know.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed record AnalysisTableModel(IReadOnlyList<AnalysisTableColumn> Columns, IReadOnlyList<AnalysisTableRow> Rows)
|
||||
{
|
||||
/// <summary>Builds the table.</summary>
|
||||
/// <param name="buckets">The buckets, oldest first.</param>
|
||||
/// <summary>Builds the table: the total row first, then the buckets newest first (A-42).</summary>
|
||||
/// <param name="buckets">The buckets, oldest first (the plan's own order).</param>
|
||||
/// <param name="series">The series.</param>
|
||||
/// <param name="pairs">The comparison buckets paired with <paramref name="buckets"/>, to name each row's compared bucket.</param>
|
||||
/// <param name="includeTotal">Adds the total row.</param>
|
||||
@@ -258,6 +267,10 @@ public sealed record AnalysisTableModel(IReadOnlyList<AnalysisTableColumn> Colum
|
||||
rows.Add(new AnalysisTableRow(buckets[i], labels[i], cells, IsTotal: false, qualified));
|
||||
}
|
||||
|
||||
// A-42: the latest period is the first row. The pairing above is by index against the plan, so the order is
|
||||
// turned round only once everything is built.
|
||||
rows.Reverse();
|
||||
|
||||
if (includeTotal && buckets.Count > 0)
|
||||
{
|
||||
var cells = new List<AnalysisTableCell>(columns.Count);
|
||||
@@ -269,7 +282,8 @@ public sealed record AnalysisTableModel(IReadOnlyList<AnalysisTableColumn> Colum
|
||||
AddCells(cells, item, total, item.Costs is null ? null : item.CostTotal ?? Unknown, item.Comparison is null ? null : item.ComparisonTotal ?? Unknown, null, item.TotalChange);
|
||||
}
|
||||
|
||||
rows.Add(new AnalysisTableRow(null, Strings.AnalysisTable_Total, cells, IsTotal: true, qualified));
|
||||
// Above the rows, where a total belongs once they descend: it sums what follows it, not what precedes it.
|
||||
rows.Insert(0, new AnalysisTableRow(null, Strings.AnalysisTable_Total, cells, IsTotal: true, qualified));
|
||||
}
|
||||
|
||||
return new AnalysisTableModel(columns, rows);
|
||||
|
||||
Reference in New Issue
Block a user