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 +"));
}
}