Skip to content

Commit 209cd44

Browse files
committed
Refactoring + new features
1 parent f5adf86 commit 209cd44

25 files changed

+511
-206
lines changed

README.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,40 @@
11
# String Math
2-
Calculates the value of a math expression from a string.
2+
Calculates the value of a math expression from a string returning a decimal.
33
Supports variables and user defined operators.
44

55
```csharp
6-
// Simple expression
7-
decimal result = Calculator.Evaluate("1 * (2 - 3) ^ 2"); // 1
6+
// Evaluating a simple expression
7+
Calculator myCalculator = new Calculator();
8+
decimal result = myCalculator.Evaluate("1 * (2 - 3) ^ 2"); // 1
89
9-
// Variables
10-
decimal result = Calculator.Evaluate("{a} + 2 * {b}", new Replacement("a", 2), new Replacement("b", 1)); // 4
10+
// Using custom operators
11+
myCalculator.AddOperator("abs", a => a > 0 ? a : -a);
12+
decimal result = myCalculator.Evaluate("abs -5"); // 5
13+
14+
// Using custom operator precedence (you can specify an int for precedence)
15+
myCalculator.AddOperator("max", (a, b) => a > b ? a : b, Precedence.Power);
16+
decimal result = myCalculator.Evaluate("2 * 3 max 4"); // 8
17+
```
1118

12-
// Adding custom operators
13-
Calculator.AddOperator("abs", a => a > 0 ? a : -a);
14-
Calculator.AddOperator("max", (a, b) => a > b ? a : b);
19+
```csharp
20+
// Using a variables collection
21+
Replacements variables = new Replacements
22+
{
23+
["a"] = 5,
24+
["PI"] = 3.1415926535897931
25+
};
26+
variables["a"] = 0;
1527

16-
// Using custom operators
17-
decimal result = Calculator.Evaluate("abs -5"); // 5
18-
decimal result = Calculator.Evaluate("3 max 4"); // 4
28+
Calculator myCalculator = new Calculator(variables);
29+
30+
// Replacing existing variables
31+
myCalculator.Replace("a", 2);
32+
myCalculator.Replace("b", 1);
33+
decimal result = calculator.Evaluate("{a} + 2 * {b} + {PI}"); // 7.1415926535897931
34+
```
35+
36+
```csharp
37+
// Using the static class SMath
38+
decimal result = SMath.Evaluate("1 + 1"); // 2
39+
decimal result = SMath.Evaluate("1 + {myVar}", new Replacements { ["myVar"] = 1 }); // 2
1940
```
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
using NUnit.Framework;
2+
using System;
3+
4+
namespace StringMath.Tests
5+
{
6+
[TestFixture]
7+
internal class CalculatorTests
8+
{
9+
private Calculator _calculator;
10+
11+
[Test]
12+
[TestCase("1 +\t 2", 3)]
13+
[TestCase("-1.5 + 3", 1.5)]
14+
[TestCase("4!", 24)]
15+
[TestCase("(3! + 1) * 2", 14)]
16+
[TestCase("2 ^ 3", 8)]
17+
[TestCase("1 + 16 log 2", 5)]
18+
[TestCase("1 + sqrt 4", 3)]
19+
[TestCase("((1 + 1) + ((1 + 1) + (((1) + 1)) + 1))", 7)]
20+
public void TestEvaluationResult(string input, decimal expected)
21+
{
22+
Assert.AreEqual(expected, _calculator.Evaluate(input));
23+
}
24+
25+
[Test]
26+
[TestCase("{a}+2", 1, 3)]
27+
[TestCase("2*{a}+2", 3, 8)]
28+
[TestCase("2*{a}+2*{a}", 3, 12)]
29+
[TestCase("{b}+3*{a}", 3, 11)]
30+
[TestCase("({a})", 3, 3)]
31+
[TestCase("{PI}", Math.PI, Math.PI)]
32+
[TestCase("{E}", Math.E, Math.E)]
33+
public void ReplacementEvaluationResult(string input, decimal replacement, decimal expected)
34+
{
35+
_calculator.Replace("a", replacement);
36+
_calculator.Replace("b", 2);
37+
38+
Assert.AreEqual(expected, _calculator.Evaluate(input));
39+
}
40+
41+
[OneTimeSetUp]
42+
public void Setup()
43+
{
44+
_calculator = new Calculator();
45+
46+
_calculator.AddOperator("abs", a => a > 0 ? a : -a);
47+
_calculator.AddOperator("x", (a, b) => a * b);
48+
_calculator.AddOperator("<<", (a, b) => (decimal)Math.ScaleB((double)a, (int)b));
49+
_calculator.AddOperator("<>", (a, b) => decimal.Parse($"{a}{b}"), Precedence.Prefix);
50+
_calculator.AddOperator("e", (a, b) => decimal.Parse($"{a}e{b}"), Precedence.Power);
51+
}
52+
53+
[Test]
54+
[TestCase("abs -5", 5)]
55+
[TestCase("abs(-1)", 1)]
56+
[TestCase("3 max 2", 3)]
57+
[TestCase("2 x\r\n 5", 10)]
58+
[TestCase("3 << 2", 12)]
59+
[TestCase("-3 <> 2", -32)]
60+
public void CustomOperators(string input, decimal expected)
61+
{
62+
Assert.AreEqual(expected, _calculator.Evaluate(input));
63+
}
64+
}
65+
}

StringMath.Tests/EvaluatorTest.cs

Lines changed: 0 additions & 47 deletions
This file was deleted.

StringMath.Tests/LexerTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public void Setup()
1313
{
1414
_context = new MathContext();
1515
_context.AddBinaryOperator("pow", default);
16+
_context.AddBinaryOperator("<<<", default);
1617
}
1718

1819
[Test]
@@ -22,6 +23,7 @@ public void Setup()
2223
[TestCase("2 pow 3", new[] { TokenType.Number, TokenType.Operator, TokenType.Number })]
2324
[TestCase("{a} + 2", new[] { TokenType.Identifier, TokenType.Operator, TokenType.Number })]
2425
[TestCase("(-1) + 2", new[] { TokenType.OpenParen, TokenType.Operator, TokenType.Number, TokenType.CloseParen, TokenType.Operator, TokenType.Number })]
26+
[TestCase("<<<", new[] { TokenType.Operator })]
2527
public void TestCorrectSyntax(string input, TokenType[] expected)
2628
{
2729
Assert.AreEqual(expected, input.GetTokens(_context).Select(t => t.Type).ToArray());

StringMath.Tests/SMathTests.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
using NUnit.Framework;
2+
using System;
3+
4+
namespace StringMath.Tests
5+
{
6+
[TestFixture]
7+
internal class SMathTests
8+
{
9+
[Test]
10+
[TestCase("1 +\t 2", 3)]
11+
[TestCase("-1.5 + 3", 1.5)]
12+
[TestCase("4!", 24)]
13+
[TestCase("(3! + 1) * 2", 14)]
14+
[TestCase("2 ^ 3", 8)]
15+
[TestCase("1 + 16 log 2", 5)]
16+
[TestCase("1 + sqrt 4", 3)]
17+
[TestCase("((1 + 1) + ((1 + 1) + (((1) + 1)) + 1))", 7)]
18+
public void TestEvaluationResult(string input, decimal expected)
19+
{
20+
Assert.AreEqual(expected, SMath.Evaluate(input));
21+
}
22+
23+
[Test]
24+
[TestCase("{a}+2", 1, 3)]
25+
[TestCase("2*{a}+2", 3, 8)]
26+
[TestCase("2*{a}+2*{a}", 3, 12)]
27+
[TestCase("{b}+3*{a}", 3, 11)]
28+
[TestCase("({a})", 3, 3)]
29+
public void ReplacementEvaluationResult(string input, decimal replacement, decimal expected)
30+
{
31+
Assert.AreEqual(expected, SMath.Evaluate(input, new Replacements
32+
{
33+
["a"] = replacement,
34+
["b"] = 2,
35+
}));
36+
}
37+
38+
[OneTimeSetUp]
39+
public void Setup()
40+
{
41+
SMath.AddOperator("abs", a => a > 0 ? a : -a);
42+
SMath.AddOperator("max", (a, b) => a > b ? a : b);
43+
SMath.AddOperator("x", (a, b) => a * b);
44+
SMath.AddOperator("<<", (a, b) => (decimal)Math.ScaleB((double)a, (int)b));
45+
SMath.AddOperator("<>", (a, b) => decimal.Parse($"{a}{b}"), Precedence.Prefix);
46+
SMath.AddOperator("e", (a, b) => decimal.Parse($"{a}e{b}"), Precedence.Power);
47+
}
48+
49+
[Test]
50+
[TestCase("abs -5", 5)]
51+
[TestCase("abs(-1)", 1)]
52+
[TestCase("3 max 2", 3)]
53+
[TestCase("2 x\r\n 5", 10)]
54+
[TestCase("3 << 2", 12)]
55+
[TestCase("-3 <> 2", -32)]
56+
public void CustomOperators(string input, decimal expected)
57+
{
58+
Assert.AreEqual(expected, SMath.Evaluate(input));
59+
}
60+
}
61+
}

StringMath/Evaluator/Calculator.cs

Lines changed: 49 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,85 +1,63 @@
11
using System;
2-
using System.Collections.Generic;
3-
using System.Linq;
4-
using System.Runtime.CompilerServices;
52

6-
[assembly: InternalsVisibleTo("StringMath.Tests")]
73
namespace StringMath
84
{
9-
public static class Calculator
5+
public class Calculator
106
{
11-
internal static MathContext MathContext { get; } = new MathContext();
7+
private readonly MathContext _mathContext = new MathContext();
8+
private readonly Replacements _replacements;
129

13-
public static void AddOperator(string operatorName, Func<decimal, decimal, decimal> operation)
14-
=> MathContext.AddBinaryOperator(operatorName, operation);
10+
private static Reducer Reducer { get; } = new Reducer();
1511

16-
public static void AddOperator(string operatorName, Func<decimal, decimal> operation)
17-
=> MathContext.AddUnaryOperator(operatorName, operation);
18-
19-
private static readonly Dictionary<Type, Func<Expression, Replacement[], Expression>> _expressionEvaluators = new Dictionary<Type, Func<Expression, Replacement[], Expression>>
20-
{
21-
[typeof(BinaryExpression)] = EvaluateBinaryExpression,
22-
[typeof(UnaryExpression)] = EvaluateUnaryExpression,
23-
[typeof(ConstantExpression)] = EvaluateConstantExpression,
24-
[typeof(GroupingExpression)] = EvaluateGroupingExpression,
25-
[typeof(ReplacementExpression)] = EvaluateReplacementExpression
26-
};
27-
28-
public static decimal Evaluate(string expression)
29-
=> Evaluate(expression, default);
30-
31-
public static decimal Evaluate(string expression, params Replacement[] replacements)
32-
{
33-
SourceText text = new SourceText(expression);
34-
Lexer lex = new Lexer(text, MathContext);
35-
Parser parse = new Parser(lex, MathContext);
36-
37-
Expression expr = Reduce(parse.Parse(), replacements);
38-
return ((NumberExpression)expr).Value;
39-
}
40-
41-
internal static Expression Reduce(Expression expression, Replacement[] replacements)
42-
{
43-
if (expression is NumberExpression)
12+
/// <summary>
13+
/// Create an instance of a Calculator which has it's own operators and variable definitions.
14+
/// </summary>
15+
/// <param name="replacements"></param>
16+
public Calculator(Replacements replacements = default)
17+
=> _replacements = replacements ?? new Replacements
4418
{
45-
return expression;
46-
}
47-
48-
var expr = _expressionEvaluators[expression.Type](expression, replacements);
49-
return Reduce(expr, replacements);
50-
}
51-
52-
private static Expression EvaluateConstantExpression(Expression arg, Replacement[] replacements)
53-
=> new NumberExpression(decimal.Parse(((ConstantExpression)arg).Value));
54-
55-
private static Expression EvaluateGroupingExpression(Expression arg, Replacement[] replacements)
56-
=> ((GroupingExpression)arg).Inner;
57-
58-
private static Expression EvaluateUnaryExpression(Expression arg, Replacement[] replacements)
19+
["PI"] = (decimal)Math.PI,
20+
["E"] = (decimal)Math.E
21+
};
22+
23+
/// <summary>
24+
/// Add a new binary operator or overwrite an existing operator's logic.
25+
/// </summary>
26+
/// <param name="operatorName">The operator's string representation.</param>
27+
/// <param name="operation">The operation to execute for this operator.</param>
28+
/// <param name="precedence">Logarithmic precedence by default.</param>
29+
public void AddOperator(string operatorName, Func<decimal, decimal, decimal> operation, Precedence precedence = default)
30+
=> _mathContext.AddBinaryOperator(operatorName, operation, precedence);
31+
32+
/// <summary>
33+
/// Add a new unary operator or overwrite an existing operator's logic.
34+
/// <see cref="Precedence"/> is always <see cref="Precedence.Prefix" />
35+
/// </summary>
36+
/// <param name="operatorName">The operator's string representation.</param>
37+
/// <param name="operation">The operation to execute for this operator.</param>
38+
public void AddOperator(string operatorName, Func<decimal, decimal> operation)
39+
=> _mathContext.AddUnaryOperator(operatorName, operation);
40+
41+
/// <summary>
42+
/// Evaluates a mathematical expression which can contain variables and returns a decimal value.
43+
/// </summary>
44+
/// <param name="expression">The math expression to evaluate.</param>
45+
/// <returns>The result as a decimal value.</returns>
46+
public decimal Evaluate(string expression)
5947
{
60-
var unary = (UnaryExpression)arg;
61-
var value = (NumberExpression)Reduce(unary.Operand, replacements);
62-
63-
var result = MathContext.EvaluateUnary(unary.OperatorName, value.Value);
64-
return new NumberExpression(result);
65-
}
66-
67-
private static Expression EvaluateBinaryExpression(Expression expr, Replacement[] replacements)
68-
{
69-
var binary = (BinaryExpression)expr;
70-
var left = (NumberExpression)Reduce(binary.Left, replacements);
71-
var right = (NumberExpression)Reduce(binary.Right, replacements);
48+
SourceText text = new SourceText(expression);
49+
Lexer lex = new Lexer(text, _mathContext);
50+
Parser parse = new Parser(lex, _mathContext);
7251

73-
var result = MathContext.EvaluateBinary(binary.OperatorName, left.Value, right.Value);
74-
return new NumberExpression(result);
52+
return Reducer.Reduce<ResultExpression>(parse.Parse(), _mathContext, _replacements).Value;
7553
}
7654

77-
private static Expression EvaluateReplacementExpression(Expression expr, Replacement[] replacements)
78-
{
79-
var replacement = (ReplacementExpression)expr;
80-
var value = replacements.FirstOrDefault(r => r.Identifier == replacement.Name);
81-
82-
return new NumberExpression(value.Value);
83-
}
55+
/// <summary>
56+
/// Replaces the value of a variable.
57+
/// </summary>
58+
/// <param name="name">The variable's name.</param>
59+
/// <param name="value">The new value.</param>
60+
public void Replace(string name, decimal value)
61+
=> _replacements[name] = value;
8462
}
8563
}

0 commit comments

Comments
 (0)