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:
@@ -202,13 +202,15 @@ public sealed class MeterDetailServiceTests(TimescaleFixture fx)
|
||||
var meter = await box.MeterAsync(type, MeterMode.CumulativeCounter, "kWh");
|
||||
var other = await box.TypeAsync();
|
||||
await box.TypePriceAsync(type, 0.30, D(2025, 1, 1));
|
||||
await box.TypePriceAsync(type, 0.31, D(2025, 5, 1));
|
||||
await box.TypePriceAsync(type, 0.32, D(2025, 7, 1));
|
||||
await box.MeterPriceAsync(meter, 0.25, D(2025, 3, 1));
|
||||
await box.TypePriceAsync(other, 0.99, D(2025, 3, 1));
|
||||
await using (var db = fx.CreateContext())
|
||||
{
|
||||
db.MeterEvents.Add(new MeterEvent { MeterId = meter, Time = Midnight(2025, 2, 10), EventType = MeterEventType.Note, Notes = "before" });
|
||||
db.MeterEvents.Add(new MeterEvent { MeterId = meter, Time = Midnight(2025, 5, 10), EventType = MeterEventType.Note, Notes = "inside" });
|
||||
db.MeterEvents.Add(new MeterEvent { MeterId = meter, Time = Midnight(2025, 4, 10), EventType = MeterEventType.Note, Notes = "inside" });
|
||||
db.MeterEvents.Add(new MeterEvent { MeterId = meter, Time = Midnight(2025, 5, 10), EventType = MeterEventType.Note, Notes = "later" });
|
||||
await db.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -216,16 +218,21 @@ public sealed class MeterDetailServiceTests(TimescaleFixture fx)
|
||||
var markers = await service.GetMarkersAsync(
|
||||
meter, new RecordRange(Midnight(2025, 3, 1), Midnight(2025, 7, 1)), D(2025, 3, 1), D(2025, 6, 30));
|
||||
|
||||
Assert.Equal("inside", Assert.Single(markers.Events).Notes);
|
||||
// Both lists newest first (A-42): the page draws them as one dated list.
|
||||
Assert.Equal(["later", "inside"], markers.Events.Select(e => e.Notes));
|
||||
Assert.False(markers.MoreEvents);
|
||||
var change = Assert.Single(markers.TariffChanges);
|
||||
Assert.Equal((TariffScope.Meter, 0.25), (change.Scope, change.Value));
|
||||
Assert.Equal([D(2025, 5, 1), D(2025, 3, 1)], markers.TariffChanges.Select(t => t.ValidFrom));
|
||||
Assert.Equal((TariffScope.Meter, 0.25), (markers.TariffChanges[1].Scope, markers.TariffChanges[1].Value));
|
||||
|
||||
// The tariff list: the meter's own and its type's (and any global), never another type's.
|
||||
// The tariff list: the meter's own and its type's (and any global), never another type's — newest first
|
||||
// inside each scope, so the price that applies now is at the top (A-42).
|
||||
var tariffs = await service.GetTariffsAsync(meter);
|
||||
Assert.Contains(tariffs, t => t.Scope == TariffScope.Meter && t.ScopeId == meter);
|
||||
Assert.Equal(2, tariffs.Count(t => t.Scope == TariffScope.EnergyType && t.ScopeId == type));
|
||||
Assert.Equal(3, tariffs.Count(t => t.Scope == TariffScope.EnergyType && t.ScopeId == type));
|
||||
Assert.DoesNotContain(tariffs, t => t.Scope == TariffScope.EnergyType && t.ScopeId == other);
|
||||
Assert.Equal(
|
||||
[D(2025, 7, 1), D(2025, 5, 1), D(2025, 1, 1)],
|
||||
tariffs.Where(t => t.Scope == TariffScope.EnergyType && t.ScopeId == type).Select(t => t.ValidFrom));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user