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:
@@ -117,6 +117,12 @@ public sealed class AnalysisComponentRenderTests
|
||||
|
||||
Assert.Contains("<th scope=\"row\" class=\"mv-row-label\">Feb</th>", html, StringComparison.Ordinal);
|
||||
Assert.Contains(">0 kWh", html, StringComparison.Ordinal);
|
||||
|
||||
// A-42: the total above the rows, then March, February, January.
|
||||
Assert.Equal(
|
||||
["Total", "Mar", "Feb", "Jan"],
|
||||
System.Text.RegularExpressions.Regex.Matches(html, "<th scope=\"row\" class=\"mv-row-label\">([^<]+)</th>")
|
||||
.Select(m => m.Groups[1].Value));
|
||||
Assert.Contains("mv-unknown\">—", html, StringComparison.Ordinal);
|
||||
Assert.Contains("No data covers this period", html, StringComparison.Ordinal);
|
||||
Assert.Contains("href=\"/drill/2\"", html, StringComparison.Ordinal);
|
||||
@@ -286,6 +292,55 @@ public sealed class AnalysisComponentRenderTests
|
||||
Assert.Contains("* Partial, estimated or not fully priced", some, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_comparison_line_names_the_figure_it_is_about_when_a_page_shows_several()
|
||||
{
|
||||
// A-41: on one energy type page the tank's use shares no covered day with the year before while the burner
|
||||
// beside it compares month by month. Unqualified, the line contradicts the card and the table under it.
|
||||
var period = Range(D(2025, 10, 1), D(2026, 9, 30));
|
||||
var resolution = ComparisonResolver.Resolve(period, new ComparisonRequest(ComparisonKind.PreviousYear), Now);
|
||||
|
||||
var alone = await RenderAsync<ComparisonSummary>("en", new()
|
||||
{
|
||||
[nameof(ComparisonSummary.Period)] = period,
|
||||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||||
[nameof(ComparisonSummary.Matched)] = MatchedCoverageResult.NotComparable,
|
||||
});
|
||||
Assert.Contains("Not comparable: the two periods share no covered days", alone, StringComparison.Ordinal);
|
||||
|
||||
var named = await RenderAsync<ComparisonSummary>("en", new()
|
||||
{
|
||||
[nameof(ComparisonSummary.Period)] = period,
|
||||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||||
[nameof(ComparisonSummary.Matched)] = MatchedCoverageResult.NotComparable,
|
||||
[nameof(ComparisonSummary.Subject)] = "Total use",
|
||||
});
|
||||
Assert.Contains("Total use: not comparable — the two periods share no covered days", named, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("Not comparable: the two", named, StringComparison.Ordinal);
|
||||
|
||||
var matched = await RenderAsync<ComparisonSummary>("de", new()
|
||||
{
|
||||
[nameof(ComparisonSummary.Period)] = period,
|
||||
[nameof(ComparisonSummary.Resolution)] = resolution,
|
||||
[nameof(ComparisonSummary.Matched)] = Matched(D(2025, 10, 1), D(2026, 5, 1)),
|
||||
[nameof(ComparisonSummary.Subject)] = "Laufzeit",
|
||||
});
|
||||
Assert.Contains("Laufzeit: Veränderung über den gemeinsam abgedeckten Zeitraum – ", matched, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
/// <summary>One matched stretch of the current period and the same stretch a year earlier.</summary>
|
||||
private static MatchedCoverageResult Matched(DateOnly first, DateOnly last)
|
||||
{
|
||||
MatchedRange RangeOf(int years) => new(
|
||||
new DateTimeOffset(first.AddYears(years).ToDateTime(TimeOnly.MinValue), TimeSpan.Zero),
|
||||
new DateTimeOffset(last.AddYears(years).AddDays(1).ToDateTime(TimeOnly.MinValue), TimeSpan.Zero),
|
||||
first.AddYears(years),
|
||||
last.AddYears(years));
|
||||
|
||||
var piece = new MatchedPiece(RangeOf(0), RangeOf(-1));
|
||||
return new MatchedCoverageResult(piece.Current, piece.Comparison, [piece]);
|
||||
}
|
||||
|
||||
/// <summary>Renders a component and returns its HTML with entities decoded (the renderer encodes every non-ASCII letter).</summary>
|
||||
private static async Task<string> RenderAsync<TComponent>(string culture, Dictionary<string, object?> parameters)
|
||||
where TComponent : IComponent => WebUtility.HtmlDecode(await RenderRawAsync<TComponent>(culture, parameters));
|
||||
|
||||
Reference in New Issue
Block a user