using MeterVault.Core.Analysis; using MeterVault.Core.Analysis.Virtual; using MeterVault.Core.Domain; namespace MeterVault.Core.Tests.Analysis; /// Builders for virtual-meter tests: UTC buckets and source day maps in the shapes the reader produces. internal static class VirtualFixtures { public static DateTimeOffset Utc(DateOnly day) => new(day.ToDateTime(TimeOnly.MinValue), TimeSpan.Zero); public static AnalysisBucket Month(int year, int month) { var first = new DateOnly(year, month, 1); var end = first.AddMonths(1); return new AnalysisBucket(first, end, Utc(first), Utc(end), BucketSize.Month); } public static AnalysisBucket[] Months(int year, int firstMonth, int count) => [.. Enumerable.Range(0, count).Select(i => new DateOnly(year, firstMonth, 1).AddMonths(i)).Select(d => Month(d.Year, d.Month))]; public static AnalysisBucket Day(DateOnly day) => new(day, day.AddDays(1), Utc(day), Utc(day.AddDays(1)), BucketSize.Day); public static AnalysisBucket[] Days(DateOnly first, int count) => [.. Enumerable.Range(0, count).Select(i => Day(first.AddDays(i)))]; /// /// A monthly-resolution series, as an imported monthly table produces it: every day of each listed month is /// covered by a month-long run, and the month's amount is booked on its last day. Each interval lies inside its /// month, so the run is divided at months (A-02) unless says otherwise (a tank /// read on the 15th). /// public static Dictionary Monthly(bool dividedAtMonths, params (int Year, int Month, double Amount)[] months) { var days = new Dictionary(); foreach (var (year, month, amount) in months) { var first = new DateOnly(year, month, 1); var last = first.AddMonths(1).AddDays(-1); for (var day = first; day <= last; day = day.AddDays(1)) { days[day] = new SourceDay( day == last ? amount : 0d, true, ResolutionClass.Month, day == last ? Provenance.Imported : Provenance.None, dividedAtMonths); } } return days; } /// A monthly-resolution series divided at months, the shape of an imported monthly table. public static Dictionary Monthly(params (int Year, int Month, double Amount)[] months) => Monthly(true, months); /// /// One interval longer than a month over [from, to), as a quarterly reading books it: covered at /// , the whole amount on the last day, never divided. /// public static Dictionary Coarse(DateOnly from, DateOnly to, double amount) { var days = new Dictionary(); for (var day = from; day < to; day = day.AddDays(1)) { var last = day == to.AddDays(-1); days[day] = new SourceDay(last ? amount : 0d, true, ResolutionClass.Coarse, last ? Provenance.Imported : Provenance.None); } return days; } /// A daily-resolution series with the same amount on every day of [from, to). public static Dictionary Daily(DateOnly from, DateOnly to, double perDay, Provenance provenance = Provenance.Measured) { var days = new Dictionary(); for (var day = from; day < to; day = day.AddDays(1)) { days[day] = new SourceDay(perDay, true, ResolutionClass.Day, provenance); } return days; } public static VirtualSource Source(int meterId, Dictionary days) => new(meterId, days); public static CatalogMeter Physical(int id, string name, MeterMode mode, QuantityKind kind, string unit, short energyType = 1) => new(id, name, mode, kind, unit, energyType); public static CatalogMeter Virtual(int id, string name, QuantityKind kind, string unit, string? expression, short energyType = 1) => new(id, name, MeterMode.Virtual, kind, unit, energyType, expression is null ? null : new VirtualDefinition(expression, kind, unit)); /// /// The seeded electricity meters (ReferenceDataImporter): Haus 1, Netz 2, Auto 3, Solar 1 = 4, Solar 2 = 5 and the /// legacy virtual Summe Solar 6, plus a water meter 7 of another energy type. /// public static List SeededElectricity() => [ Physical(1, "Zähler Haus", MeterMode.CumulativeCounter, QuantityKind.Consumption, "kWh"), Physical(2, "Zähler Netz", MeterMode.CumulativeCounter, QuantityKind.Consumption, "kWh"), Physical(3, "Zähler Auto", MeterMode.CumulativeCounter, QuantityKind.Consumption, "kWh"), Physical(4, "Solar 1", MeterMode.GenerationCounter, QuantityKind.Generation, "kWh"), Physical(5, "Solar 2", MeterMode.GenerationCounter, QuantityKind.Generation, "kWh"), new CatalogMeter(6, "Summe Solar", MeterMode.Virtual, QuantityKind.Generation, "kWh", 1), Physical(7, "Wasser", MeterMode.CumulativeCounter, QuantityKind.Consumption, "m³", energyType: 2), ]; /// The seeded links: Solar 1 → Summe, Solar 2 → Summe, Netz → Haus, Summe → Haus, Haus → Auto. public static List SeededLinks() => [ Link(4, 6), Link(5, 6), Link(2, 1), Link(6, 1), Link(1, 3), ]; public static MeterLink Link(int from, int to) => new() { FromMeterId = from, ToMeterId = to }; }