Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ on:
jobs:
build:

runs-on: ubuntu-latest
runs-on: windows-latest

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:

jobs:
publish:
runs-on: ubuntu-latest
runs-on: windows-latest

steps:
- uses: actions/checkout@v2
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

> **NEW!** Boolean math example: https://github.com/miroiu/string-math/pull/6/files

# String Math [![NuGet](https://img.shields.io/nuget/v/StringMath?style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath/) [![Downloads](https://img.shields.io/nuget/dt/StringMath?label=downloads&style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath) ![.NET](https://img.shields.io/static/v1?label=%20&message=Framework%204.6.1%20to%20NET%206&color=5C2D91&style=flat-square&logo=.net) ![](https://img.shields.io/static/v1?label=%20&message=documentation&color=yellow&style=flat-square)
# String Math [![NuGet](https://img.shields.io/nuget/v/StringMath?style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath/) [![Downloads](https://img.shields.io/nuget/dt/StringMath?label=downloads&style=flat-square&logo=nuget)](https://www.nuget.org/packages/StringMath) ![.NET](https://img.shields.io/static/v1?label=%20&message=Framework%204.6.1%20to%20NET%208&color=5C2D91&style=flat-square&logo=.net) ![](https://img.shields.io/static/v1?label=%20&message=documentation&color=yellow&style=flat-square)

Calculates the value of a math expression from a string returning a double.
Supports variables, user defined operators and expression compilation.
Expand Down
9 changes: 6 additions & 3 deletions StringMath.Benchmarks/Benchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ public class Benchmarks
[Benchmark]
public void Tokenize()
{
var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}");
var context = MathContext.Default;

var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}", context);

Token token;

Expand All @@ -24,8 +26,9 @@ public void Tokenize()
[Benchmark]
public void Parse()
{
var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}");
var parser = new Parser(tokenizer, MathContext.Default);
var context = MathContext.Default;
var tokenizer = new Tokenizer("1.23235456576878798 - ((3 + {b}) max .1) ^ sqrt(-999 / 2 * 3 max 5) + !5 - 0.00000000002 / {ahghghh}", context);
var parser = new Parser(tokenizer, context);
_ = parser.Parse();
}

Expand Down
4 changes: 2 additions & 2 deletions StringMath.Tests/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ namespace StringMath.Tests
{
static class Extensions
{
public static List<Token> ReadAllTokens(this string input)
public static List<Token> ReadAllTokens(this string input, IMathContext context)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, context);
List<Token> tokens = new List<Token>();

Token t;
Expand Down
3 changes: 2 additions & 1 deletion StringMath.Tests/MathExprTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,11 @@ public void SetOperator_Unary_Should_Not_Overwrite_Global_Operator()
[TestCase("1 + sqrt 4", 3)]
[TestCase("sind(90) + sind 30", 1.5)]
[TestCase("((1 + 1) + ((1 + 1) + (((1) + 1)) + 1))", 7)]
[TestCase("719.04+sin(60)", 718.735d)]
public void Evaluate(string input, double expected)
{
double result = input.Eval();
Assert.AreEqual(expected, result);
Assert.AreEqual(expected, result, 0.001);
}

[TestCase("{b}+3*{a}", 3, 2, 11)]
Expand Down
32 changes: 18 additions & 14 deletions StringMath.Tests/ParserTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,12 @@ public void Setup()
[TestCase("1.15215345346", "1.15215345346")]
[TestCase("0", "0")]
[TestCase("!2", "2!")]
[TestCase("--1", "-(-1)")]
[TestCase("1+sin(3)", "1 + sin(3)")]
[TestCase("1+sin 3", "1 + sin(3)")]
public void ParseMathExpression(string input, string expected)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand Down Expand Up @@ -56,8 +59,9 @@ public void ParseMathExpression(string input, string expected)
[TestCase("1+")]
[TestCase("1.")]
[TestCase("1..1")]
[TestCase("--1")]
[TestCase("-*1")]
[TestCase("-+1")]
[TestCase("+-1")]
[TestCase("{")]
[TestCase("}")]
[TestCase("asd")]
Expand All @@ -67,7 +71,7 @@ public void ParseMathExpression(string input, string expected)
[TestCase("1 + 2 1")]
public void ParseBadExpression_Exception(string input)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand All @@ -84,7 +88,7 @@ public void ParseExpression_CustomOperators(string input, string expected)
context.RegisterBinary("pow", (a, b) => a);
context.RegisterUnary("rand", (a) => a);

Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, context);

IExpression result = parser.Parse();
Expand All @@ -100,7 +104,7 @@ public void ParseExpression_CustomOperators_Exception(string expected)
{
MathContext context = new MathContext();

Tokenizer tokenizer = new Tokenizer(expected);
Tokenizer tokenizer = new Tokenizer(expected, _context);
Parser parser = new Parser(tokenizer, context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand All @@ -119,7 +123,7 @@ public void ParseExpression_CustomOperators_Exception(string expected)
[TestCase("{a13}", "a13")]
public void ParseVariableExpression(string expected, string name)
{
Tokenizer tokenizer = new Tokenizer(expected);
Tokenizer tokenizer = new Tokenizer(expected, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand All @@ -139,7 +143,7 @@ public void ParseVariableExpression(string expected, string name)
[TestCase("{-a}")]
public void ParseVariableExpression_Exception(string expected)
{
Tokenizer tokenizer = new Tokenizer(expected);
Tokenizer tokenizer = new Tokenizer(expected, _context);
Parser parser = new Parser(tokenizer, _context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand All @@ -159,7 +163,7 @@ public void ParseVariableExpression_Exception(string expected)
[TestCase("1 / 2 / 3", "1 / 2 / 3")]
public void ParseBinaryExpression(string input, string expected)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand All @@ -183,7 +187,7 @@ public void ParseBinaryExpression(string input, string expected)
[TestCase("sqrt{a}", "sqrt({a})")]
public void ParseUnaryExpression(string input, string expected)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand All @@ -199,7 +203,7 @@ public void ParseUnaryExpression(string input, string expected)
[TestCase("+5")]
public void ParseUnaryExpression_Exception(string input)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand All @@ -213,7 +217,7 @@ public void ParseUnaryExpression_Exception(string input)
[TestCase("9999999", "9999999")]
public void ParseConstantExpression(string input, string expected)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand All @@ -231,7 +235,7 @@ public void ParseConstantExpression(string input, string expected)
[TestCase("9.01+")]
public void ParseConstantExpression_Exception(string expected)
{
Tokenizer tokenizer = new Tokenizer(expected);
Tokenizer tokenizer = new Tokenizer(expected, _context);
Parser parser = new Parser(tokenizer, _context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand All @@ -250,7 +254,7 @@ public void ParseConstantExpression_Exception(string expected)
[TestCase("((5 - 2) + ((-1 + 2) * 3))", "5 - 2 + (-1 + 2) * 3")]
public void ParseGroupingExpression(string input, string expected)
{
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);
Parser parser = new Parser(tokenizer, _context);

IExpression result = parser.Parse();
Expand All @@ -271,7 +275,7 @@ public void ParseGroupingExpression(string input, string expected)
[TestCase("({a} + (1 + 2)")]
public void ParseGroupingExpression_Fail(string expected)
{
Tokenizer tokenizer = new Tokenizer(expected);
Tokenizer tokenizer = new Tokenizer(expected, _context);
Parser parser = new Parser(tokenizer, _context);

MathException exception = Assert.Throws<MathException>(() => parser.Parse());
Expand Down
26 changes: 19 additions & 7 deletions StringMath.Tests/TokenizerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,25 @@ namespace StringMath.Tests
[TestFixture]
internal class TokenizerTests
{
private IMathContext _context;

[OneTimeSetUp]
public void Setup()
{
_context = MathContext.Default;
}

[Test]
[TestCase("-1 * 3.5", new[] { TokenType.Operator, TokenType.Number, TokenType.Operator, TokenType.Number })]
[TestCase("2 pow 3", new[] { TokenType.Number, TokenType.Operator, TokenType.Number })]
[TestCase("{a} + 2", new[] { TokenType.Identifier, TokenType.Operator, TokenType.Number })]
[TestCase("(-1) + 2", new[] { TokenType.OpenParen, TokenType.Operator, TokenType.Number, TokenType.CloseParen, TokenType.Operator, TokenType.Number })]
[TestCase("5!", new[] { TokenType.Number, TokenType.Exclamation })]
[TestCase("1+sin(3)", new[] { TokenType.Number, TokenType.Operator, TokenType.Operator, TokenType.OpenParen, TokenType.Number, TokenType.CloseParen })]
[TestCase("1+sin 3", new[] { TokenType.Number, TokenType.Operator, TokenType.Operator, TokenType.Number })]
public void ReadToken(string input, TokenType[] expected)
{
IEnumerable<TokenType> actualTokens = input.ReadAllTokens()
IEnumerable<TokenType> actualTokens = input.ReadAllTokens(_context)
.Where(token => token.Type != TokenType.EndOfCode)
.Select(t => t.Type);
Assert.That(actualTokens, Is.EquivalentTo(expected));
Expand All @@ -29,7 +39,7 @@ public void ReadToken(string input, TokenType[] expected)
public void ReadToken_IgnoresWhitespace(string input)
{
// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act
Token token1 = tokenizer.ReadToken();
Expand All @@ -53,7 +63,7 @@ public void ReadToken_IgnoresWhitespace(string input)
public void ReadIdentifier(string input)
{
// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act
Token token = tokenizer.ReadToken();
Expand All @@ -75,7 +85,7 @@ public void ReadIdentifier(string input)
public void ReadIdentifier_Exception(string input)
{
// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act & Assert
MathException exception = Assert.Throws<MathException>(() => tokenizer.ReadToken());
Expand All @@ -91,8 +101,10 @@ public void ReadIdentifier_Exception(string input)
[TestCase("a@a")]
public void ReadOperator(string input)
{
_context.RegisterBinary("**", (a, b) => a * b, Precedence.Multiplication);

// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act
Token token = tokenizer.ReadToken();
Expand All @@ -113,7 +125,7 @@ public void ReadOperator(string input)
public void ReadNumber(string input)
{
// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act
Token token = tokenizer.ReadToken();
Expand All @@ -133,7 +145,7 @@ public void ReadNumber(string input)
public void ReadNumber_Exception(string input)
{
// Arrange
Tokenizer tokenizer = new Tokenizer(input);
Tokenizer tokenizer = new Tokenizer(input, _context);

// Act & Assert
MathException exception = Assert.Throws<MathException>(() => tokenizer.ReadToken());
Expand Down
2 changes: 1 addition & 1 deletion StringMath/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public static IExpression Parse(this string text, IMathContext context)
{
text.EnsureNotNull(nameof(text));

Tokenizer tokenizer = new Tokenizer(text);
Tokenizer tokenizer = new Tokenizer(text, context);
Parser parser = new Parser(tokenizer, context);
return parser.Parse();
}
Expand Down
49 changes: 23 additions & 26 deletions StringMath/MathContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ public sealed class MathContext : IMathContext
private readonly Dictionary<string, Func<double, double, double>> _binaryEvaluators = new Dictionary<string, Func<double, double, double>>(StringComparer.Ordinal);
private readonly Dictionary<string, Func<double, double>> _unaryEvaluators = new Dictionary<string, Func<double, double>>(StringComparer.Ordinal);
private readonly Dictionary<string, Precedence> _binaryPrecedence = new Dictionary<string, Precedence>(StringComparer.Ordinal);
private readonly HashSet<string> _operators = new HashSet<string>(StringComparer.Ordinal);

/// <summary>The global instance used by <see cref="MathExpr.AddOperator(string, Func{double, double})"/> methods.</summary>
public static readonly IMathContext Default = new MathContext();
Expand All @@ -28,25 +27,25 @@ static MathContext()
Default.RegisterBinary("*", (a, b) => a * b, Precedence.Multiplication);
Default.RegisterBinary("/", (a, b) => a / b, Precedence.Multiplication);
Default.RegisterBinary("%", (a, b) => a % b, Precedence.Multiplication);
Default.RegisterBinary("^", (a, b) => Math.Pow(a, b), Precedence.Power);
Default.RegisterBinary("log", (a, b) => Math.Log(a, b), Precedence.Logarithmic);
Default.RegisterBinary("max", (a, b) => Math.Max(a, b), Precedence.UserDefined);
Default.RegisterBinary("min", (a, b) => Math.Min(a, b), Precedence.UserDefined);
Default.RegisterBinary("^", Math.Pow, Precedence.Power);
Default.RegisterBinary("log", Math.Log, Precedence.Logarithmic);
Default.RegisterBinary("max", Math.Max, Precedence.UserDefined);
Default.RegisterBinary("min", Math.Min, Precedence.UserDefined);

Default.RegisterUnary("-", a => -a);
Default.RegisterUnary("!", a => ComputeFactorial(a));
Default.RegisterUnary("sqrt", a => Math.Sqrt(a));
Default.RegisterUnary("sin", a => Math.Sin(a));
Default.RegisterUnary("asin", a => Math.Asin(a));
Default.RegisterUnary("cos", a => Math.Cos(a));
Default.RegisterUnary("acos", a => Math.Acos(a));
Default.RegisterUnary("tan", a => Math.Tan(a));
Default.RegisterUnary("atan", a => Math.Atan(a));
Default.RegisterUnary("ceil", a => Math.Ceiling(a));
Default.RegisterUnary("floor", a => Math.Floor(a));
Default.RegisterUnary("round", a => Math.Round(a));
Default.RegisterUnary("exp", a => Math.Exp(a));
Default.RegisterUnary("abs", a => Math.Abs(a));
Default.RegisterUnary("!", ComputeFactorial);
Default.RegisterUnary("sqrt", Math.Sqrt);
Default.RegisterUnary("sin", Math.Sin);
Default.RegisterUnary("asin", Math.Asin);
Default.RegisterUnary("cos", Math.Cos);
Default.RegisterUnary("acos", Math.Acos);
Default.RegisterUnary("tan", Math.Tan);
Default.RegisterUnary("atan", Math.Atan);
Default.RegisterUnary("ceil", Math.Ceiling);
Default.RegisterUnary("floor", Math.Floor);
Default.RegisterUnary("round", Math.Round);
Default.RegisterUnary("exp", Math.Exp);
Default.RegisterUnary("abs", Math.Abs);
Default.RegisterUnary("rad", a => rad * a);
Default.RegisterUnary("deg", a => deg * a);
}
Expand All @@ -70,8 +69,8 @@ public bool IsBinary(string operatorName)
/// <inheritdoc />
public Precedence GetBinaryPrecedence(string operatorName)
{
return _binaryPrecedence.ContainsKey(operatorName)
? _binaryPrecedence[operatorName]
return _binaryPrecedence.TryGetValue(operatorName, out var value)
? value
: Parent?.GetBinaryPrecedence(operatorName)
?? throw MathException.MissingBinaryOperator(operatorName);
}
Expand All @@ -84,7 +83,6 @@ public void RegisterBinary(string operatorName, Func<double, double, double> ope

_binaryEvaluators[operatorName] = operation;
_binaryPrecedence[operatorName] = precedence ?? Precedence.UserDefined;
_operators.Add(operatorName);
}

/// <inheritdoc />
Expand All @@ -94,14 +92,13 @@ public void RegisterUnary(string operatorName, Func<double, double> operation)
operation.EnsureNotNull(nameof(operation));

_unaryEvaluators[operatorName] = operation;
_operators.Add(operatorName);
}

/// <inheritdoc />
public double EvaluateBinary(string op, double a, double b)
{
double result = _binaryEvaluators.ContainsKey(op)
? _binaryEvaluators[op](a, b)
double result = _binaryEvaluators.TryGetValue(op, out var value)
? value(a, b)
: Parent?.EvaluateBinary(op, a, b)
?? throw MathException.MissingBinaryOperator(op);

Expand All @@ -111,8 +108,8 @@ public double EvaluateBinary(string op, double a, double b)
/// <inheritdoc />
public double EvaluateUnary(string op, double a)
{
double result = _unaryEvaluators.ContainsKey(op)
? _unaryEvaluators[op](a)
double result = _unaryEvaluators.TryGetValue(op, out var value)
? value(a)
: Parent?.EvaluateUnary(op, a)
?? throw MathException.MissingUnaryOperator(op);

Expand Down
Loading
Loading