using MeterVault.Core.Analysis; namespace MeterVault.Core.Tests.Analysis; /// /// One rule for change figures everywhere (D-08): the absolute difference whenever both values are known, /// a percentage only against a positive baseline, and "unknown" never read as zero. /// public sealed class ChangeTests { [Fact] public void A_rise_from_a_positive_baseline_has_an_absolute_difference_a_percentage_and_an_upward_direction() { var change = Change.Between(120, 100); Assert.Equal(20, change.Absolute!.Value, 9); Assert.Equal(20, change.Percent!.Value, 9); Assert.Equal(1, change.Direction); Assert.True(change.IsAvailable); Assert.True(change.PercentApplicable); } [Fact] public void A_fall_is_negative_in_both_figures() { var change = Change.Between(75, 100); Assert.Equal(-25, change.Absolute!.Value, 9); Assert.Equal(-25, change.Percent!.Value, 9); Assert.Equal(-1, change.Direction); } [Fact] public void Against_a_zero_baseline_the_percentage_is_not_applicable_but_the_difference_is_shown() { // The old dashboard chip read "+0.0 %" here. var change = Change.Between(120, 0); Assert.Equal(120, change.Absolute!.Value, 9); Assert.Null(change.Percent); Assert.False(change.PercentApplicable); Assert.Equal(1, change.Direction); } [Fact] public void Against_a_negative_baseline_the_percentage_is_not_applicable() { // A credit shrinking from -10 to -5 is a rise of 5; dividing by -10 would call it -50 %. var change = Change.Between(-5, -10); Assert.Equal(5, change.Absolute!.Value, 9); Assert.Null(change.Percent); Assert.Equal(1, change.Direction); } [Fact] public void A_signed_change_from_positive_to_negative_keeps_its_percentage() { var change = Change.Between(-50, 100); Assert.Equal(-150, change.Absolute!.Value, 9); Assert.Equal(-150, change.Percent!.Value, 9); Assert.Equal(-1, change.Direction); } [Fact] public void Equal_values_are_no_change_even_after_floating_point_noise() { var exact = Change.Between(100, 100); var noisy = Change.Between(0.1 + 0.2, 0.3); Assert.Equal(0, exact.Direction); Assert.Equal(0, exact.Percent!.Value, 9); Assert.Equal(0, noisy.Direction); Assert.NotNull(noisy.Absolute); } [Fact] public void A_baseline_within_the_tolerance_of_zero_counts_as_zero() { Assert.Null(Change.Between(5, 1e-12).Percent); } [Theory] [InlineData(null, 100.0)] [InlineData(100.0, null)] [InlineData(null, null)] [InlineData(double.NaN, 100.0)] [InlineData(100.0, double.PositiveInfinity)] public void A_missing_or_non_finite_value_makes_the_change_unavailable_not_minus_100_percent(double? current, double? previous) { var change = Change.Between(current, previous); Assert.Equal(Change.Unavailable, change); Assert.Null(change.Absolute); Assert.Null(change.Percent); Assert.Equal(0, change.Direction); Assert.False(change.IsAvailable); } [Fact] public void A_difference_that_overflows_is_unavailable() { Assert.Equal(Change.Unavailable, Change.Between(double.MaxValue, -double.MaxValue)); } [Fact] public void A_difference_below_the_callers_display_tolerance_has_no_direction_but_keeps_its_exact_value() { // 0.004 € shows as "+0.00 €"; an upward arrow beside it would claim a change nobody can see. var cents = Change.Between(100.004, 100, tolerance: 0.005); var noiseOnly = Change.Between(100.004, 100); Assert.Equal(0, cents.Direction); Assert.Equal(0.004, cents.Absolute!.Value, 9); Assert.NotNull(cents.Percent); Assert.Equal(1, noiseOnly.Direction); } [Fact] public void A_baseline_that_displays_as_zero_under_the_callers_tolerance_has_no_percentage() { Assert.Null(Change.Between(5, 0.004, tolerance: 0.005).Percent); Assert.NotNull(Change.Between(5, 0.004).Percent); } [Theory] [InlineData(-0.001)] [InlineData(double.NaN)] [InlineData(double.PositiveInfinity)] public void A_negative_or_non_finite_tolerance_is_rejected(double tolerance) { Assert.Throws(() => Change.Between(1, 2, tolerance)); } [Fact] public void An_observed_zero_now_against_a_positive_baseline_is_a_real_minus_100_percent() { var change = Change.Between(0, 80); Assert.Equal(-80, change.Absolute!.Value, 9); Assert.Equal(-100, change.Percent!.Value, 9); Assert.Equal(-1, change.Direction); } }