using MeterVault.Core.Analysis.Virtual; namespace MeterVault.Core.Tests.Analysis; /// /// A parsed formula knows its own shape: which meters it reads, whether it is linear (additive, so buckets sum to /// the total and the quantity can be priced) or a pure sum (so costs can be the sources' own), and a canonical text /// that parses back to the same formula. /// public sealed class FormulaTests { [Fact] public void Meter_ids_are_distinct_and_ascending_and_references_keep_their_positions() { var formula = Formula.Parse("m12 + m3 - m12 * 2"); Assert.Equal([3, 12], formula.MeterIds); Assert.Equal( [new FormulaReference(12, 0, 3), new FormulaReference(3, 6, 2), new FormulaReference(12, 11, 3)], formula.References); } [Theory] [InlineData("m1 + m2", true, true)] [InlineData("(m1 + m2) * 1", true, true)] [InlineData("m1 - m2", true, false)] [InlineData("m1 + m1", true, false)] [InlineData("2 * (m1 + m2)", true, false)] [InlineData("0.5 * m1 + m2 / 4", true, false)] [InlineData("-(m1 - m2)", true, false)] [InlineData("m1 + 5 - 5", true, true)] // the constants cancel [InlineData("m1 + 5", false, false)] // a constant term: not additive across buckets [InlineData("m1 * m2", false, false)] [InlineData("m1 / m2", false, false)] [InlineData("1 / m1", false, false)] [InlineData("m1 / 0", false, false)] [InlineData("m1 * (1 / 0)", false, false)] [InlineData("(m1 - m1) * m2", false, false)] // decided by structure, not by the value that cancels public void Linearity_and_pure_sums_are_recognised(string text, bool linear, bool pureSum) { var formula = Formula.Parse(text); Assert.Equal(linear, formula.IsLinear); Assert.Equal(pureSum, formula.IsPureSum); Assert.Equal(linear, formula.Coefficients is not null); } [Fact] public void Coefficients_are_each_meters_weight_in_a_linear_formula() { Assert.Equal(new Dictionary { [1] = 1, [2] = -1 }, Formula.Parse("m1 - m2").Coefficients); Assert.Equal(new Dictionary { [1] = 0.5, [2] = 0.25 }, Formula.Parse("0.5 * m1 + m2 / 4").Coefficients); Assert.Equal(new Dictionary { [1] = -1, [2] = 1 }, Formula.Parse("-(m1 - m2)").Coefficients); Assert.Equal(new Dictionary { [1] = 0, [2] = 1 }, Formula.Parse("m1 - m1 + m2").Coefficients); } [Theory] [InlineData("m1+m2", "m1 + m2")] [InlineData("((((m1))))", "m1")] [InlineData("(m1 + m2) * 2", "(m1 + m2) * 2")] [InlineData("(m1 - m2) - m3", "m1 - m2 - m3")] [InlineData("m1 - (m2 - m3)", "m1 - (m2 - m3)")] [InlineData("m1 - (m2 + m3)", "m1 - (m2 + m3)")] [InlineData("m1 + (m2 + m3)", "m1 + (m2 + m3)")] [InlineData("m1 / (m2 * m3)", "m1 / (m2 * m3)")] [InlineData("m1 * m2 + m3", "m1 * m2 + m3")] [InlineData("-(m1 + m2)", "-(m1 + m2)")] [InlineData("-m1 * m2", "-m1 * m2")] [InlineData("--m1", "--m1")] [InlineData("+m1", "m1")] [InlineData("m1 * -m2", "m1 * -m2")] [InlineData("m1 - -5", "m1 - -5")] [InlineData("0.50 * m1", "0.50 * m1")] public void The_canonical_text_has_minimal_parentheses_and_parses_back_to_an_equal_formula(string text, string canonical) { var formula = Formula.Parse(text); Assert.Equal(canonical, formula.ToString()); Assert.Equal(text, formula.Text); Assert.Equal(formula, Formula.Parse(formula.ToString())); } [Fact] public void Equality_is_structural() { Assert.Equal(Formula.Parse("m1+m2"), Formula.Parse("(m1) + m2")); Assert.Equal(Formula.Parse("m1+m2").GetHashCode(), Formula.Parse("(m1) + m2").GetHashCode()); Assert.Equal(Formula.Parse("1.0 * m1"), Formula.Parse("1 * m1")); Assert.NotEqual(Formula.Parse("m1 + m2"), Formula.Parse("m2 + m1")); Assert.NotEqual(Formula.Parse("m1 - m2 - m3"), Formula.Parse("m1 - (m2 - m3)")); } [Fact] public void The_tree_mirrors_precedence() { var root = Assert.IsType(Formula.Parse("m1 - 2 * m2").Root); Assert.Equal(FormulaOperator.Subtract, root.Operator); Assert.Equal(1, Assert.IsType(root.Left).MeterId); var product = Assert.IsType(root.Right); Assert.Equal(FormulaOperator.Multiply, product.Operator); Assert.Equal(2, Assert.IsType(product.Left).Value); Assert.Equal(2, Assert.IsType(product.Right).MeterId); } [Fact] public void Division_by_zero_evaluates_to_a_non_finite_number_for_the_caller_to_judge() { var ratio = Formula.Parse("m1 / m2"); Assert.True(double.IsPositiveInfinity(ratio.Evaluate(id => id == 1 ? 80 : 0))); Assert.True(double.IsNaN(ratio.Evaluate(_ => 0))); } [Fact] public void Rewriting_ids_follows_meters_to_new_ids_and_keeps_the_users_text() { var map = new Dictionary { [1] = 41, [2] = 42 }; var formula = Formula.Parse("m1 + (m2*m1)"); var rewritten = formula.RewriteIds(id => map[id]); Assert.Equal("m41 + (m42*m41)", rewritten.Text); Assert.Equal([41, 42], rewritten.MeterIds); Assert.Equal(Formula.Parse("m41 + m42 * m41"), rewritten); Assert.Equal( [new FormulaReference(41, 0, 3), new FormulaReference(42, 8, 3), new FormulaReference(41, 12, 3)], rewritten.References); Assert.Equal(Formula.Parse(rewritten.Text), rewritten); } [Fact] public void Rewriting_to_a_negative_id_is_refused() { Assert.Throws(() => Formula.Parse("m1").RewriteIds(_ => -1)); } [Fact] public void Sum_and_difference_build_the_editors_simple_modes() { var sum = Formula.Sum([4, 5, 4]); Assert.Equal("m4 + m5", sum.ToString()); Assert.True(sum.IsPureSum); var difference = Formula.Difference(1, [2, 3]); Assert.Equal("m1 - m2 - m3", difference.ToString()); Assert.Equal(new Dictionary { [1] = 1, [2] = -1, [3] = -1 }, difference.Coefficients); Assert.Throws(() => Formula.Sum([])); Assert.Throws(() => Formula.Difference(1, [])); } [Fact] public void Parse_throws_only_for_trusted_text_that_turns_out_invalid() { Assert.Throws(() => Formula.Parse("m1 +")); } }