using MeterVault.Core.Analysis.Virtual;
namespace MeterVault.Core.Tests.Analysis;
///
/// The formula grammar is user input evaluated on every read, so the parser is the safety boundary (D-26): it
/// accepts only numbers, m<id>, unary minus, + - * / and parentheses, reports every rejection
/// with a position instead of throwing, and cannot be driven into a stack overflow. Ported from the old
/// ExpressionEvaluatorTests; the one deliberate change is that an unknown identifier is now an error, not 0.
///
public sealed class FormulaParserTests
{
private static double Evaluate(string text) =>
Formula.Parse(text).Evaluate(id => id switch { 1 => 411, 2 => 416, _ => throw new KeyNotFoundException($"m{id}") });
[Theory]
[InlineData("1 + 2 * 3", 7)]
[InlineData("(1 + 2) * 3", 9)]
[InlineData("-5", -5)]
[InlineData("--5", 5)]
[InlineData("+5", 5)]
[InlineData("2 * -3", -6)]
[InlineData("-(1 + 2) * 3", -9)]
[InlineData("10 / 4", 2.5)]
[InlineData("2 - 3 - 4", -5)] // left-associative
[InlineData("8 / 4 / 2", 1)] // left-associative
[InlineData("0.5 * 4", 2)]
[InlineData(".5 + 5.", 5.5)]
[InlineData("m1 - m2", -5)] // Netz Einsparung Okt 2022: Haus 411 − Netz 416
[InlineData("m1 + m2", 827)]
[InlineData("m1+m2", 827)]
[InlineData(" ( m1 ) - m2 ", -5)]
public void Evaluates_arithmetic_over_numbers_and_meter_references(string text, double expected)
{
Assert.Equal(expected, Evaluate(text), 9);
}
[Fact]
public void Numbers_are_invariant_decimals_whatever_the_current_culture()
{
var previous = Thread.CurrentThread.CurrentCulture;
try
{
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("de-DE");
Assert.Equal(0.25, Formula.Parse("0.25").Evaluate(_ => 0), 12);
Assert.Equal(FormulaErrorKind.UnexpectedCharacter, FormulaParser.Parse("0,25").Error!.Kind);
}
finally
{
Thread.CurrentThread.CurrentCulture = previous;
}
}
[Theory]
[InlineData("1 +", FormulaErrorKind.UnexpectedEnd, 3, null)]
[InlineData("(1 + 2", FormulaErrorKind.MissingClosingParenthesis, 0, "(")]
[InlineData("m1 + (m2 * (3 - 1)", FormulaErrorKind.MissingClosingParenthesis, 5, "(")]
[InlineData("1 2", FormulaErrorKind.UnexpectedToken, 2, "2")]
[InlineData("m1 m2", FormulaErrorKind.UnexpectedToken, 3, "m2")]
[InlineData(")", FormulaErrorKind.UnexpectedToken, 0, ")")]
[InlineData("(m1))", FormulaErrorKind.UnexpectedToken, 4, ")")]
[InlineData("m1 * * m2", FormulaErrorKind.UnexpectedToken, 5, "*")]
[InlineData("1 % 2", FormulaErrorKind.UnexpectedCharacter, 2, "%")]
[InlineData("1.2.3", FormulaErrorKind.InvalidNumber, 0, "1.2.3")]
[InlineData(".", FormulaErrorKind.InvalidNumber, 0, ".")]
[InlineData("1e3", FormulaErrorKind.UnexpectedToken, 1, "e3")]
[InlineData("2m1", FormulaErrorKind.UnexpectedToken, 1, "m1")]
[InlineData("m99999999999", FormulaErrorKind.MeterIdOutOfRange, 0, "m99999999999")]
public void Rejects_malformed_formulas_with_the_position_and_token(string text, FormulaErrorKind kind, int position, string? token)
{
var result = FormulaParser.Parse(text);
Assert.False(result.Success);
Assert.Equal(new FormulaError(kind, position, token), result.Error);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Empty_text_is_an_error_not_an_exception(string? text)
{
Assert.Equal(FormulaErrorKind.Empty, FormulaParser.Parse(text).Error!.Kind);
}
[Theory]
[InlineData("unknown + 1", "unknown", 0)] // the old evaluator read this as 1
[InlineData("m1 - n2", "n2", 5)]
[InlineData("M1 + m2", "M1", 0)]
[InlineData("m1a", "m1a", 0)]
[InlineData("m_1", "m_1", 0)]
[InlineData("m", "m", 0)]
[InlineData("sum(m1)", "sum", 0)]
[InlineData("m1 + Hausverbrauch", "Hausverbrauch", 5)]
public void Only_m_digits_identifiers_are_meter_references_and_anything_else_is_named(string text, string identifier, int position)
{
Assert.Equal(new FormulaError(FormulaErrorKind.UnknownIdentifier, position, identifier), FormulaParser.Parse(text).Error);
}
[Fact]
public void Nesting_64_parentheses_deep_is_accepted_and_65_is_rejected_at_the_65th()
{
static string Nested(int depth) => new string('(', depth) + "m1" + new string(')', depth);
Assert.True(FormulaParser.Parse(Nested(FormulaParser.MaxDepth)).Success);
var tooDeep = FormulaParser.Parse(Nested(FormulaParser.MaxDepth + 1));
Assert.Equal(new FormulaError(FormulaErrorKind.TooDeep, 64, "("), tooDeep.Error);
}
[Fact]
public void Two_thousand_characters_are_accepted_and_2001_rejected()
{
var chain = "m1" + string.Concat(Enumerable.Repeat(" + m1", 399)); // 1,997 characters
var exactly = chain + " ";
Assert.Equal(FormulaParser.MaxLength, exactly.Length);
var accepted = FormulaParser.Parse(exactly);
Assert.True(accepted.Success);
Assert.Equal(400 * 411, accepted.Formula.Evaluate(_ => 411), 6);
Assert.Equal(FormulaErrorKind.TooLong, FormulaParser.Parse(exactly + " ").Error!.Kind);
}
[Fact]
public void A_hundred_thousand_open_parentheses_are_rejected_without_overflowing_the_stack()
{
Assert.Equal(FormulaErrorKind.TooLong, FormulaParser.Parse(new string('(', 100_000)).Error!.Kind);
Assert.Equal(FormulaErrorKind.TooDeep, FormulaParser.Parse(new string('(', 1_999)).Error!.Kind);
}
[Fact]
public void Long_operator_chains_and_sign_runs_parse_and_evaluate_without_recursion()
{
var signs = FormulaParser.Parse(new string('-', 1_999) + "1");
Assert.True(signs.Success);
Assert.Equal(-1, signs.Formula.Evaluate(_ => 0));
var product = FormulaParser.Parse("1" + string.Concat(Enumerable.Repeat(" * 1", 499)));
Assert.True(product.Success);
Assert.Equal(1, product.Formula.Evaluate(_ => 0));
Assert.Equal(product.Formula, Formula.Parse(product.Formula.ToString()));
}
[Fact]
public void Scanning_finds_meter_tokens_even_in_text_that_does_not_parse()
{
Assert.Equal([2, 5, 12], FormulaParser.ScanMeterIds("m12 + (m5 * m2 +"));
Assert.Empty(FormulaParser.ScanMeterIds("1.5 + x2"));
Assert.Empty(FormulaParser.ScanMeterIds(null));
}
[Fact]
public void Rewriting_ids_in_text_keeps_the_users_spacing_and_even_a_syntax_error()
{
var map = new Dictionary { [1] = 41, [2] = 42 };
Assert.Equal("m41 -( m42 )", FormulaParser.RewriteMeterIds("m1 -( m2 )", id => map[id]));
Assert.Equal("m41 + ", FormulaParser.RewriteMeterIds("m1 + ", id => map[id]));
Assert.Equal("0.1m99 + xm1", FormulaParser.RewriteMeterIds("0.1m2 + xm1", _ => 99)); // "xm1" names no meter
}
}