Analysis: read a rarely-read meter as coarse, not absent; newest rows first
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:
Florian Schmidt
2026-09-20 12:44:32 +02:00
parent 5a6f34a467
commit a08e9f781f
38 changed files with 1301 additions and 184 deletions
@@ -37,6 +37,7 @@ public sealed class AdminPagesRenderTests(TimescaleFixture fx) : IAsyncLifetime
Tariff[] tariffs =
[
Tariff(TariffScope.Meter, _meter, TariffComponent.UnitPrice, 1.2345, "EUR/m3"),
Tariff(TariffScope.Meter, _meter, TariffComponent.UnitPrice, 1.9999, "EUR/m3", new DateOnly(2023, 1, 1)),
Tariff(TariffScope.EnergyType, _type, TariffComponent.Bonus, 2.3456, "EUR"),
Tariff(TariffScope.EnergyType, _otherType, TariffComponent.UnitPrice, 9.8765, "EUR/kWh"),
];
@@ -67,9 +68,15 @@ public sealed class AdminPagesRenderTests(TimescaleFixture fx) : IAsyncLifetime
Assert.DoesNotContain("9.8765", scoped, StringComparison.Ordinal); // another type's price is not listed
Assert.Contains("Bonus, discount and tax tariffs are stored but not applied to costs yet.", scoped, StringComparison.Ordinal);
// A-42: inside a component the newest validity is listed first, so the price in force is the top row.
Assert.True(
scoped.IndexOf("1.9999", StringComparison.Ordinal) < scoped.IndexOf("1.2345", StringComparison.Ordinal),
"The 2023 price must be listed above the 2020 one.");
var all = WebUtility.HtmlDecode(await client.GetStringAsync(new Uri(TariffLinks.Path, UriKind.Relative)));
Assert.Contains("9.8765", all, StringComparison.Ordinal);
Assert.DoesNotContain("Tariffs that can price", all, StringComparison.Ordinal);
Assert.True(all.IndexOf("1.9999", StringComparison.Ordinal) < all.IndexOf("1.2345", StringComparison.Ordinal));
// The deep link of a missing price renders; its dialog opens only once the page is interactive.
var link = TariffLinks.New(TariffScope.Meter, _meter, TariffComponent.UnitPrice, new DateOnly(2027, 1, 1));
@@ -99,6 +106,6 @@ public sealed class AdminPagesRenderTests(TimescaleFixture fx) : IAsyncLifetime
Assert.Contains("Auswertungsdaten", deutsch, StringComparison.Ordinal);
}
private static Tariff Tariff(TariffScope scope, int id, TariffComponent component, double value, string unit) =>
new() { ScopeType = scope, ScopeId = id, Component = component, Value = value, Unit = unit, ValidFrom = new DateOnly(2020, 1, 1) };
private static Tariff Tariff(TariffScope scope, int id, TariffComponent component, double value, string unit, DateOnly? from = null) =>
new() { ScopeType = scope, ScopeId = id, Component = component, Value = value, Unit = unit, ValidFrom = from ?? new DateOnly(2020, 1, 1) };
}