using MeterVault.Core.Analysis.Quantities; using MeterVault.Core.Domain; namespace MeterVault.Core.Tests.Analysis; public sealed class TariffUnitTests { // ---- Parsing ---------------------------------------------------------------------------------- [Theory] [InlineData("EUR/kWh", "EUR", 1, "kWh", 1)] [InlineData("€/kWh", "EUR", 1, "kWh", 1)] [InlineData("eur/kwh", "EUR", 1, "kWh", 1)] [InlineData("Euro/kWh", "EUR", 1, "kWh", 1)] [InlineData("ct/kWh", "ct", 0.01, "kWh", 1)] [InlineData("Cent/kWh", "ct", 0.01, "kWh", 1)] [InlineData("EUR/MWh", "EUR", 1, "MWh", 1)] [InlineData("EUR/m3", "EUR", 1, "m³", 1)] [InlineData("EUR/m³", "EUR", 1, "m³", 1)] [InlineData("EUR/cbm", "EUR", 1, "m³", 1)] [InlineData("EUR/100L", "EUR", 1, "L", 100)] [InlineData("EUR / 100 l", "EUR", 1, "L", 100)] [InlineData("EUR/1.000 L", "EUR", 1, "L", 1000)] [InlineData("EUR/1000L", "EUR", 1, "L", 1000)] [InlineData("EUR/hl", "EUR", 1, "hL", 1)] [InlineData("EUR/h", "EUR", 1, "h", 1)] [InlineData("EUR/Std", "EUR", 1, "h", 1)] [InlineData("EUR/t", "EUR", 1, "t", 1)] [InlineData("EUR per kWh", "EUR", 1, "kWh", 1)] [InlineData("EUR je 100 L", "EUR", 1, "L", 100)] [InlineData("SEK/kWh", "SEK", 1, "kWh", 1)] [InlineData("Fr./kWh", "CHF", 1, "kWh", 1)] [InlineData("Rp./kWh", "Rp", 0.01, "kWh", 1)] [InlineData("p/kWh", "p", 0.01, "kWh", 1)] public void A_quantity_price_parses_to_currency_scale_and_denominator( string unit, string currency, double currencyScale, string denominator, double amount) { var parsed = TariffUnit.Parse(unit); Assert.Equal(TariffUnitBasis.Quantity, parsed.Basis); Assert.True(parsed.IsRecognised); Assert.Equal(currency, parsed.Currency); Assert.Equal(currencyScale, parsed.CurrencyScale); Assert.Equal(denominator, parsed.Denominator); Assert.Equal(amount, parsed.DenominatorAmount); Assert.Null(parsed.Period); } [Theory] [InlineData("EUR/month", BillingPeriod.Month)] [InlineData("EUR/Monat", BillingPeriod.Month)] [InlineData("€/Mon.", BillingPeriod.Month)] [InlineData("€ pro Monat", BillingPeriod.Month)] [InlineData("EUR monatlich", BillingPeriod.Month)] [InlineData("EUR/1 Monat", BillingPeriod.Month)] [InlineData("EUR/Jahr", BillingPeriod.Year)] [InlineData("EUR/year", BillingPeriod.Year)] [InlineData("EUR/a", BillingPeriod.Year)] [InlineData("EUR p.a.", BillingPeriod.Year)] [InlineData("EUR jährlich", BillingPeriod.Year)] [InlineData("EUR/Tag", BillingPeriod.Day)] [InlineData("EUR/day", BillingPeriod.Day)] [InlineData("EUR/d", BillingPeriod.Day)] [InlineData("EUR/12 Monate", BillingPeriod.Year)] [InlineData("EUR/1 Jahr", BillingPeriod.Year)] [InlineData("EUR/Quartal", BillingPeriod.Quarter)] [InlineData("EUR/quarter", BillingPeriod.Quarter)] [InlineData("EUR pro Quartal", BillingPeriod.Quarter)] [InlineData("EUR vierteljährlich", BillingPeriod.Quarter)] [InlineData("EUR/3 Monate", BillingPeriod.Quarter)] public void A_standing_charge_parses_to_its_billing_period(string unit, BillingPeriod period) { var parsed = TariffUnit.Parse(unit); Assert.Equal(TariffUnitBasis.Period, parsed.Basis); Assert.Equal(period, parsed.Period); Assert.Equal("EUR", parsed.Currency); Assert.Null(parsed.Denominator); } [Theory] [InlineData("pauschal")] [InlineData("EUR")] [InlineData("kWh")] [InlineData("")] [InlineData(" ")] [InlineData(null)] [InlineData("EUR/100")] [InlineData("EUR/kWh/h")] [InlineData("EUR/0 L")] [InlineData("EUR/1,5 L")] [InlineData("EUR/1.00 L")] [InlineData("kWh/EUR")] [InlineData("Taler/kWh")] [InlineData("(brutto)")] [InlineData("brutto EUR/kWh")] public void Anything_else_is_unparseable(string? unit) { var parsed = TariffUnit.Parse(unit); Assert.Equal(TariffUnitBasis.Unparseable, parsed.Basis); Assert.False(parsed.IsRecognised); Assert.Null(parsed.Denominator); Assert.Null(parsed.Period); } [Fact] public void An_unknown_denominator_is_kept_to_match_itself() { var parsed = TariffUnit.Parse("EUR/Stk"); Assert.Equal(TariffUnitBasis.OtherDenominator, parsed.Basis); Assert.Equal("Stk", parsed.Denominator); Assert.False(parsed.IsRecognised); } [Theory] [InlineData("EUR/100L", "100 L")] [InlineData("EUR/m3", "m³")] [InlineData("EUR/Monat", "month")] [InlineData("EUR/a", "year")] [InlineData("pauschal", "pauschal")] public void The_denominator_text_says_what_the_price_is_quoted_per(string unit, string text) { Assert.Equal(text, TariffUnit.Parse(unit).DenominatorText); } [Theory] [InlineData("EUR/kWh", TariffComponent.UnitPrice, true)] [InlineData("ct/kWh", TariffComponent.FeedIn, true)] [InlineData("EUR/month", TariffComponent.UnitPrice, false)] [InlineData("EUR/Stk", TariffComponent.UnitPrice, true)] [InlineData("pauschal", TariffComponent.UnitPrice, false)] [InlineData("EUR/month", TariffComponent.BasePrice, true)] [InlineData("EUR/Jahr", TariffComponent.BasePrice, true)] [InlineData("EUR/kWh", TariffComponent.BasePrice, false)] [InlineData("pauschal", TariffComponent.BasePrice, false)] [InlineData("%", TariffComponent.Tax, true)] public void The_editor_accepts_only_a_unit_shaped_for_its_component(string unit, TariffComponent component, bool suits) { Assert.Equal(suits, TariffUnit.Parse(unit).Suits(component)); } // ---- Unit and feed-in prices -------------------------------------------------------------------- [Theory] [InlineData("EUR/kWh", "kWh", 1)] [InlineData("€/kWh", "kWh", 1)] [InlineData("ct/kWh", "kWh", 0.01)] [InlineData("EUR/100L", "L", 0.01)] [InlineData("EUR/MWh", "kWh", 0.001)] [InlineData("EUR/m3", "m³", 1)] [InlineData("EUR/m3", "m3", 1)] [InlineData("EUR/m³", "L", 0.001)] [InlineData("EUR/100L", "m³", 10)] [InlineData("EUR/kWh", "Wh", 0.001)] [InlineData("EUR/kWh", "MWh", 1000)] [InlineData("ct/100 L", "L", 0.0001)] [InlineData("EUR/h", "h", 1)] [InlineData("EUR/t", "kg", 0.001)] public void A_matching_unit_price_applies_with_the_factor_to_currency_per_meter_unit( string tariffUnit, string meterUnit, double factor) { var fit = TariffUnit.Applicability(tariffUnit, meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.True(fit.Applies); Assert.False(fit.NeedsWarning); Assert.Equal(TariffUnitIssue.None, fit.Issue); Assert.Equal(factor, fit.Factor, 12); } [Fact] public void The_seeded_water_tariff_prices_the_seeded_water_meter_at_face_value() { // Seed: water 5.00 "EUR/m3" on a meter whose unit is "m3". Dec 2022 is 14 m³ = 70 € (D-56). var fit = TariffUnit.Applicability("EUR/m3", "m3", TariffComponent.UnitPrice); Assert.Equal(70d, 14 * fit.Convert(5.00), 9); } [Fact] public void A_heating_oil_price_per_100_litres_is_charged_per_litre() { // 98.50 €/100 L on the seeded Öltank (L): 1,000 L cost 985 €, not 98,500 €. var fit = TariffUnit.Applicability("EUR/100L", "L", TariffComponent.UnitPrice); Assert.Equal(985d, 1000 * fit.Convert(98.50), 9); } [Fact] public void A_feed_in_price_in_cents_is_converted_like_a_unit_price() { var fit = TariffUnit.Applicability("ct/kWh", "kWh", TariffComponent.FeedIn); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(0.082, fit.Convert(8.2), 12); } [Fact] public void An_energy_price_does_not_price_water() { var fit = TariffUnit.Applicability("EUR/kWh", "m³", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.False(fit.Applies); Assert.Equal(TariffUnitIssue.IncompatibleUnit, fit.Issue); Assert.Equal("kWh", fit.TariffDenominator); Assert.Equal("m³", fit.MeterUnit); Assert.Equal("EUR/kWh", fit.TariffUnit); Assert.Equal(0d, fit.Convert(0.30)); } [Theory] // A global electricity price used to price water and burner hours too (costing review, §10.2). [InlineData("EUR/kWh", "h")] [InlineData("EUR/100L", "h")] [InlineData("EUR/h", "L")] [InlineData("EUR/kWh", "kW")] [InlineData("EUR/L", "L/min")] [InlineData("EUR/m³", "m³/h")] public void A_price_in_another_dimension_is_a_mismatch(string tariffUnit, string meterUnit) { var fit = TariffUnit.Applicability(tariffUnit, meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.IncompatibleUnit, fit.Issue); } [Theory] [InlineData("EUR/kWh", "Stk", 1)] [InlineData("EUR/kWh", "kWh (el)", 1)] [InlineData("EUR/m³", "Nm³", 1)] [InlineData("ct/kWh", "Einheiten", 0.01)] [InlineData("EUR/kWh", "mwh", 1)] public void A_meter_unit_the_module_does_not_recognise_is_unverified_not_a_mismatch( string tariffUnit, string meterUnit, double factor) { var fit = TariffUnit.Applicability(tariffUnit, meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.True(fit.Applies); Assert.True(fit.NeedsWarning); Assert.Equal(TariffUnitIssue.MeterUnitUnrecognised, fit.Issue); Assert.Equal(factor, fit.Factor, 12); } [Theory] [InlineData("EUR/kWh", "Kilowattstunden", 1)] [InlineData("EUR/kWh", "kilowatt-hours", 1)] [InlineData("EUR/MWh", "Kilowattstunde", 0.001)] [InlineData("ct/kWh", "Wattstunden", 0.00001)] [InlineData("EUR/Megawattstunde", "kWh", 0.001)] public void Long_form_and_German_energy_units_are_priced_like_their_symbols(string tariffUnit, string meterUnit, double factor) { var fit = TariffUnit.Applicability(tariffUnit, meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(factor, fit.Factor, 12); } [Theory] [InlineData("mWh", 0.000_001)] [InlineData("MWh", 1000)] [InlineData("MWH", 1000)] public void A_milli_unit_is_never_priced_as_a_mega_unit(string meterUnit, double factor) { var fit = TariffUnit.Applicability("EUR/kWh", meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(factor, fit.Factor, 12); } [Fact] public void A_standing_charge_unit_on_a_unit_price_is_a_mismatch() { var fit = TariffUnit.Applicability("EUR/month", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.PeriodForQuantity, fit.Issue); Assert.Equal("month", fit.TariffDenominator); } [Fact] public void An_unparseable_unit_price_applies_at_face_value_with_a_warning() { var fit = TariffUnit.Applicability("pauschal", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.True(fit.Applies); Assert.True(fit.NeedsWarning); Assert.Equal(TariffUnitIssue.Unparseable, fit.Issue); Assert.Equal(1d, fit.Factor); Assert.Equal("pauschal", fit.TariffDenominator); } [Fact] public void An_unparseable_unit_in_cents_still_converts_the_currency() { var fit = TariffUnit.Applicability("ct", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.Equal(0.01, fit.Factor); } [Fact] public void An_unknown_denominator_applies_to_a_meter_in_that_unit() { var fit = TariffUnit.Applicability("EUR/Stk", "stk", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(1d, fit.Factor); } [Fact] public void An_unknown_denominator_on_another_meter_applies_with_a_warning() { var fit = TariffUnit.Applicability("EUR/Stk", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.Equal(TariffUnitIssue.UnknownDenominator, fit.Issue); Assert.Equal(1d, fit.Factor); } // ---- Qualifiers --------------------------------------------------------------------------------- [Theory] [InlineData("€/kWh brutto", TariffUnitBasis.Quantity, "kWh", "brutto")] [InlineData("EUR/kWh netto", TariffUnitBasis.Quantity, "kWh", "netto")] [InlineData("EUR/kWh zzgl. 19 % MwSt.", TariffUnitBasis.Quantity, "kWh", "zzgl. 19 % MwSt.")] [InlineData("ct/kWh, inkl. USt", TariffUnitBasis.Quantity, "kWh", "inkl. USt")] [InlineData("EUR/kWh (Grundversorgung)", TariffUnitBasis.Quantity, "kWh", "Grundversorgung")] [InlineData("EUR/100 L [Heizöl EL] incl. VAT", TariffUnitBasis.Quantity, "100 L", "incl. VAT Heizöl EL")] [InlineData("€/Jahr inkl. MwSt.", TariffUnitBasis.Period, "year", "inkl. MwSt.")] [InlineData("EUR/Monat (netto)", TariffUnitBasis.Period, "month", "netto")] [InlineData("EUR per kWh gross", TariffUnitBasis.Quantity, "kWh", "gross")] public void VAT_notes_and_bracketed_text_are_set_aside_before_the_unit_is_read( string unit, TariffUnitBasis basis, string denominator, string qualifier) { var parsed = TariffUnit.Parse(unit); Assert.Equal(basis, parsed.Basis); Assert.Equal(denominator, parsed.DenominatorText); Assert.Equal(qualifier, parsed.Qualifier); Assert.Equal(unit, parsed.Raw); } [Fact] public void A_unit_without_a_note_has_no_qualifier() { Assert.Null(TariffUnit.Parse("EUR/kWh").Qualifier); Assert.Null(TariffUnit.Parse("pauschal").Qualifier); } [Fact] public void A_price_with_a_VAT_note_applies_cleanly() { var fit = TariffUnit.Applicability("€/kWh brutto", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(1d, fit.Factor); Assert.Equal("€/kWh brutto", fit.TariffUnit); } [Theory] [InlineData(null)] [InlineData("")] public void A_meter_without_a_unit_cannot_be_checked(string? meterUnit) { var fit = TariffUnit.Applicability("EUR/100L", meterUnit, TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.Equal(TariffUnitIssue.MeterUnitUnknown, fit.Issue); Assert.Equal(0.01, fit.Factor); } [Theory] [InlineData(TariffComponent.Bonus)] [InlineData(TariffComponent.Discount)] [InlineData(TariffComponent.Tax)] public void Bonus_discount_and_tax_are_not_applied(TariffComponent component) { var fit = TariffUnit.Applicability("EUR/kWh", "kWh", component); Assert.Equal(TariffUnitFit.NotApplied, fit.Fit); Assert.False(fit.Applies); Assert.Equal(TariffUnitIssue.ComponentNotApplied, fit.Issue); Assert.Equal(0d, fit.Convert(12)); } // ---- Standing charges --------------------------------------------------------------------------- [Theory] [InlineData("EUR/month", BillingPeriod.Month)] [InlineData("EUR/Jahr", BillingPeriod.Year)] [InlineData("EUR/Tag", BillingPeriod.Day)] public void A_standing_charge_applies_per_its_period_whatever_the_meter_unit(string unit, BillingPeriod period) { var fit = TariffUnit.Applicability(unit, "m³", TariffComponent.BasePrice); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(period, fit.Period); Assert.Equal(1d, fit.Factor); } [Fact] public void A_monthly_charge_accrues_over_the_days_of_each_local_month() { var accrual = TariffUnit.BaseAccrual("EUR/month"); Assert.Equal(BillingPeriod.Month, accrual.Period); Assert.False(accrual.Assumed); Assert.Equal(12d / 28, accrual.PerDay(12, new DateOnly(2026, 2, 10)), 12); Assert.Equal(12d / 29, accrual.PerDay(12, new DateOnly(2024, 2, 10)), 12); Assert.Equal(12d / 31, accrual.PerDay(12, new DateOnly(2026, 1, 31)), 12); Assert.Equal(12d, SumOverDays(accrual, 12, new DateOnly(2026, 2, 1), new DateOnly(2026, 3, 1)), 9); Assert.Equal(12d, SumOverDays(accrual, 12, new DateOnly(2024, 2, 1), new DateOnly(2024, 3, 1)), 9); } [Fact] public void A_yearly_charge_accrues_over_the_days_of_the_local_year() { var accrual = TariffUnit.BaseAccrual("EUR/Jahr"); Assert.Equal(BillingPeriod.Year, accrual.Period); Assert.Equal(120d / 366, accrual.PerDay(120, new DateOnly(2024, 7, 1)), 12); Assert.Equal(120d / 365, accrual.PerDay(120, new DateOnly(2026, 7, 1)), 12); Assert.Equal(120d, SumOverDays(accrual, 120, new DateOnly(2024, 1, 1), new DateOnly(2025, 1, 1)), 9); Assert.Equal(120d, SumOverDays(accrual, 120, new DateOnly(2026, 1, 1), new DateOnly(2027, 1, 1)), 9); } [Fact] public void A_daily_charge_is_charged_as_is_and_a_charge_in_cents_is_converted() { Assert.Equal(0.5, TariffUnit.BaseAccrual("EUR/Tag").PerDay(0.5, new DateOnly(2026, 2, 1)), 12); Assert.Equal(0.5, TariffUnit.BaseAccrual("ct/Tag").PerDay(50, new DateOnly(2026, 2, 1)), 12); } [Fact] public void The_accrual_takes_the_month_length_directly() { var accrual = TariffUnit.BaseAccrual("EUR/Monat"); Assert.Equal(1d, accrual.PerDay(30, daysInMonth: 30, daysInYear: 365), 12); Assert.Throws(() => accrual.PerDay(30, daysInMonth: 0, daysInYear: 365)); } [Fact] public void An_unparseable_standing_charge_is_taken_per_month_with_a_warning() { var fit = TariffUnit.Applicability("pauschal", "kWh", TariffComponent.BasePrice); var accrual = TariffUnit.BaseAccrual("pauschal"); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.True(fit.NeedsWarning); Assert.Equal(BillingPeriod.Month, fit.Period); Assert.Equal(TariffUnitIssue.Unparseable, fit.Issue); Assert.Equal(BillingPeriod.Month, accrual.Period); Assert.True(accrual.Assumed); Assert.Equal(10d / 30, accrual.PerDay(10, new DateOnly(2026, 4, 15)), 12); } [Theory] [InlineData("€/Jahr inkl. MwSt.", 120d / 365)] [InlineData("EUR/Jahr brutto", 120d / 365)] [InlineData("EUR/12 Monate", 120d / 365)] [InlineData("EUR/Quartal", 120d / 92)] public void A_standing_charge_is_never_taken_per_month_when_its_period_can_be_read(string unit, double perDay) { var accrual = TariffUnit.BaseAccrual(unit); Assert.False(accrual.Assumed); Assert.Equal(TariffUnitFit.Applies, accrual.Fit); Assert.Equal(perDay, accrual.PerDay(120, new DateOnly(2026, 7, 1)), 12); } [Fact] public void A_quarterly_charge_accrues_over_the_days_of_its_local_calendar_quarter() { var accrual = TariffUnit.BaseAccrual("EUR/Quartal"); Assert.Equal(BillingPeriod.Quarter, accrual.Period); Assert.Equal(90, accrual.DaysInPeriod(new DateOnly(2026, 2, 14))); Assert.Equal(91, accrual.DaysInPeriod(new DateOnly(2024, 3, 31))); Assert.Equal(91, accrual.DaysInPeriod(new DateOnly(2026, 4, 1))); Assert.Equal(92, accrual.DaysInPeriod(new DateOnly(2026, 8, 31))); Assert.Equal(92, accrual.DaysInPeriod(new DateOnly(2026, 12, 31))); Assert.Equal(30d, SumOverDays(accrual, 30, new DateOnly(2024, 1, 1), new DateOnly(2024, 4, 1)), 9); Assert.Equal(30d, SumOverDays(accrual, 30, new DateOnly(2026, 7, 1), new DateOnly(2026, 10, 1)), 9); Assert.Equal(120d, SumOverDays(accrual, 30, new DateOnly(2026, 1, 1), new DateOnly(2027, 1, 1)), 9); } [Fact] public void A_quarterly_charge_cannot_be_spread_by_month_and_year_lengths_alone() { var accrual = TariffUnit.BaseAccrual("EUR/Quartal"); Assert.Throws(() => accrual.PerDay(30, daysInMonth: 31, daysInYear: 365)); } [Theory] [InlineData("EUR/2 Monate", "2 Monate")] [InlineData("EUR/Woche", "Woche")] [InlineData("EUR/Halbjahr", "Halbjahr")] [InlineData("EUR/7 Tage", "7 Tage")] [InlineData("EUR halbjährlich", "halbjährlich")] public void A_period_that_is_read_but_cannot_be_accrued_is_a_mismatch_not_a_month(string unit, string written) { var parsed = TariffUnit.Parse(unit); var fit = TariffUnit.Applicability(unit, "kWh", TariffComponent.BasePrice); var accrual = TariffUnit.BaseAccrual(unit); Assert.Equal(TariffUnitBasis.UnsupportedPeriod, parsed.Basis); Assert.True(parsed.IsRecognised); Assert.Equal(written, parsed.DenominatorText); Assert.False(parsed.Suits(TariffComponent.BasePrice)); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.UnsupportedPeriod, fit.Issue); Assert.Equal(0d, fit.Factor); Assert.Equal(TariffUnitFit.Mismatch, accrual.Fit); Assert.False(accrual.Applies); Assert.False(accrual.Assumed); Assert.Equal(0d, accrual.PerDay(24, new DateOnly(2026, 5, 1))); } [Fact] public void A_period_on_a_unit_price_is_a_mismatch_even_when_it_cannot_be_accrued() { var fit = TariffUnit.Applicability("EUR/2 Monate", "kWh", TariffComponent.UnitPrice); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.PeriodForQuantity, fit.Issue); } [Fact] public void A_standing_charge_quoted_per_quantity_is_taken_per_month_with_a_warning() { var accrual = TariffUnit.BaseAccrual("EUR/kWh"); Assert.True(accrual.Assumed); Assert.Equal(BillingPeriod.Month, accrual.Period); Assert.Equal(TariffUnitIssue.QuantityForPeriod, accrual.Issue); } // ---- Currency ----------------------------------------------------------------------------------- [Theory] [InlineData("USD/kWh", "EUR")] [InlineData("SEK/kWh", "EUR")] [InlineData("£/kWh", "EUR")] [InlineData("p/kWh", "EUR")] [InlineData("Rp./kWh", "EUR")] [InlineData("ct/kWh", "GBP")] [InlineData("EUR/kWh", "CHF")] [InlineData("USD", "EUR")] public void A_price_in_another_currency_does_not_price_the_instance_currency(string tariffUnit, string currency) { var fit = TariffUnit.Applicability(tariffUnit, "kWh", TariffComponent.UnitPrice, currency); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.CurrencyMismatch, fit.Issue); Assert.Equal(0d, fit.Convert(0.30)); } [Theory] [InlineData("EUR/kWh", "EUR", 1)] [InlineData("€/kWh", "eur", 1)] [InlineData("EUR/kWh", "€", 1)] [InlineData("ct/kWh", "EUR", 0.01)] [InlineData("Cent/kWh", "USD", 0.01)] [InlineData("p/kWh", "GBP", 0.01)] [InlineData("Rp./kWh", "CHF", 0.01)] [InlineData("Fr./kWh", "CHF", 1)] [InlineData("SEK/kWh", "sek", 1)] public void A_minor_unit_applies_only_under_its_own_major_currency(string tariffUnit, string currency, double factor) { var fit = TariffUnit.Applicability(tariffUnit, "kWh", TariffComponent.UnitPrice, currency); Assert.Equal(TariffUnitFit.Applies, fit.Fit); Assert.Equal(factor, fit.Factor, 12); } [Fact] public void Without_an_expected_currency_the_currency_is_not_checked() { Assert.Equal(TariffUnitFit.Applies, TariffUnit.Applicability("USD/kWh", "kWh", TariffComponent.UnitPrice).Fit); } [Fact] public void A_unit_without_a_currency_cannot_be_checked_against_one() { var fit = TariffUnit.Applicability("pauschal", "kWh", TariffComponent.UnitPrice, "EUR"); Assert.Equal(TariffUnitFit.Unverified, fit.Fit); Assert.Equal(TariffUnitIssue.Unparseable, fit.Issue); } [Fact] public void A_standing_charge_in_another_currency_accrues_nothing() { var fit = TariffUnit.Applicability("USD/month", "kWh", TariffComponent.BasePrice, "EUR"); var accrual = TariffUnit.BaseAccrual("USD/month", "EUR"); Assert.Equal(TariffUnitFit.Mismatch, fit.Fit); Assert.Equal(TariffUnitIssue.CurrencyMismatch, fit.Issue); Assert.Equal(BillingPeriod.Month, fit.Period); Assert.Equal(TariffUnitFit.Mismatch, accrual.Fit); Assert.Equal(0d, accrual.PerDay(12, new DateOnly(2026, 2, 1))); Assert.Equal(TariffUnitFit.Applies, TariffUnit.BaseAccrual("EUR/month", "EUR").Fit); } [Fact] public void Bonus_discount_and_tax_stay_not_applied_whatever_their_currency() { Assert.Equal(TariffUnitFit.NotApplied, TariffUnit.Applicability("USD/kWh", "kWh", TariffComponent.Tax, "EUR").Fit); } [Fact] public void The_expected_currency_must_be_named() { Assert.Throws(() => TariffUnit.Applicability("EUR/kWh", "kWh", TariffComponent.UnitPrice, " ")); Assert.Throws(() => TariffUnit.BaseAccrual("EUR/month", "")); } // ---- Parsed once -------------------------------------------------------------------------------- [Theory] [InlineData("EUR/kWh", "MWh", TariffComponent.UnitPrice)] [InlineData("EUR/100L", "L", TariffComponent.UnitPrice)] [InlineData("pauschal", "kWh", TariffComponent.UnitPrice)] [InlineData("EUR/Jahr", "m³", TariffComponent.BasePrice)] [InlineData("USD/kWh", "kWh", TariffComponent.FeedIn)] [InlineData("EUR/kWh", "kWh", TariffComponent.Tax)] public void A_unit_parsed_once_is_checked_exactly_like_its_text(string tariffUnit, string meterUnit, TariffComponent component) { var parsed = TariffUnit.Parse(tariffUnit); Assert.Equal( TariffUnit.Applicability(tariffUnit, meterUnit, component), TariffUnit.Applicability(parsed, meterUnit, component)); Assert.Equal( TariffUnit.Applicability(tariffUnit, meterUnit, component, "EUR"), TariffUnit.Applicability(parsed, meterUnit, component, "EUR")); Assert.Equal(TariffUnit.BaseAccrual(tariffUnit), TariffUnit.BaseAccrual(parsed)); Assert.Equal(TariffUnit.BaseAccrual(tariffUnit, "EUR"), TariffUnit.BaseAccrual(parsed, "EUR")); } // ---- Alias tables ------------------------------------------------------------------------------- public static TheoryData CurrencyAliases { get { var data = new TheoryData(); foreach (var entry in TariffUnit.CurrencyAliasTable) { data.Add(entry.Alias, entry.Code, entry.Scale, entry.Majors[0]); } return data; } } public static TheoryData PeriodAliases { get { var data = new TheoryData(); foreach (var entry in TariffUnit.PeriodAliasTable) { data.Add(entry.Alias); } return data; } } [Theory] [MemberData(nameof(CurrencyAliases))] public void Every_currency_spelling_parses_to_its_code_and_scale_and_prices_its_major_currency( string alias, string code, double scale, string major) { var parsed = TariffUnit.Parse($"{alias}/kWh"); Assert.Equal(TariffUnitBasis.Quantity, parsed.Basis); Assert.Equal(code, parsed.Currency); Assert.Equal(scale, parsed.CurrencyScale); Assert.Equal(TariffUnitFit.Applies, TariffUnit.Applicability(parsed, "kWh", TariffComponent.UnitPrice, major).Fit); } [Theory] [MemberData(nameof(PeriodAliases))] public void Every_period_spelling_is_read_as_a_period(string alias) { var slashed = TariffUnit.Parse($"EUR/{alias}"); var spaced = TariffUnit.Parse($"EUR pro {alias}"); Assert.Contains(slashed.Basis, new[] { TariffUnitBasis.Period, TariffUnitBasis.UnsupportedPeriod }); Assert.Equal(slashed.Basis, spaced.Basis); Assert.Equal(slashed.Period, spaced.Period); if (slashed.Basis == TariffUnitBasis.Period) { // A supported period round-trips through the token it is written with. Assert.Equal(slashed.Period, TariffUnit.Parse($"EUR/{TariffUnit.PeriodToken(slashed.Period!.Value)}").Period); } } // ---- Suggestions -------------------------------------------------------------------------------- [Theory] [InlineData(TariffComponent.UnitPrice, "m3", "EUR/m³")] [InlineData(TariffComponent.FeedIn, "kWh", "EUR/kWh")] [InlineData(TariffComponent.BasePrice, "kWh", "EUR/month")] [InlineData(TariffComponent.UnitPrice, "", "EUR")] [InlineData(TariffComponent.Tax, "kWh", "EUR")] public void The_editor_suggests_a_unit_for_the_component(TariffComponent component, string meterUnit, string expected) { Assert.Equal(expected, TariffUnit.Suggest(component, meterUnit)); } [Theory] [InlineData(TariffComponent.UnitPrice, "m3")] [InlineData(TariffComponent.FeedIn, "kWh")] [InlineData(TariffComponent.UnitPrice, "L")] [InlineData(TariffComponent.BasePrice, "kWh")] public void A_suggested_unit_applies_cleanly_to_the_meter_it_was_suggested_for(TariffComponent component, string meterUnit) { var suggested = TariffUnit.Suggest(component, meterUnit); Assert.Equal(TariffUnitFit.Applies, TariffUnit.Applicability(suggested, meterUnit, component).Fit); Assert.True(TariffUnit.Parse(suggested).Suits(component)); } private static double SumOverDays(BasePriceAccrual accrual, double value, DateOnly from, DateOnly to) { var total = 0d; for (var day = from; day < to; day = day.AddDays(1)) { total += accrual.PerDay(value, day); } return total; } }