diff --git a/ClosedXML/Excel/CalcEngine/CalcEngine.cs b/ClosedXML/Excel/CalcEngine/CalcEngine.cs index f510498..b11fb46 100644 --- a/ClosedXML/Excel/CalcEngine/CalcEngine.cs +++ b/ClosedXML/Excel/CalcEngine/CalcEngine.cs @@ -85,10 +85,10 @@ var expr = ParseExpression(); // check for errors - if (_token.ID != TKID.END) - { - Throw(); - } + if (_token.ID == TKID.OPEN) + Throw("Unknown function: " + expr.LastParseItem); + else if (_token.ID != TKID.END) + Throw("Expected end of expression"); // optimize expression if (_optimize) @@ -113,10 +113,9 @@ /// public object Evaluate(string expression) { - var x = //Parse(expression); - _cache != null - ? _cache[expression] - : Parse(expression); + var x = _cache != null + ? _cache[expression] + : Parse(expression); return x.Evaluate(); } @@ -713,7 +712,7 @@ if (isEnclosed && disallowedSymbols.Contains(c)) break; - var allowedSymbols = new List() { '_' }; + var allowedSymbols = new List() { '_', '.' }; if (!isLetter && !isDigit && !(isEnclosed || allowedSymbols.Contains(c)) @@ -771,10 +770,10 @@ } // make sure the list was closed correctly - if (_token.ID != TKID.CLOSE) - { - Throw(); - } + if (_token.ID == TKID.OPEN) + Throw("Unknown function: " + expr.LastParseItem); + else if (_token.ID != TKID.CLOSE) + Throw("Syntax error: expected ')'"); // done return parms; diff --git a/ClosedXML/Excel/CalcEngine/Expression.cs b/ClosedXML/Excel/CalcEngine/Expression.cs index dcac3c0..9dfdf28 100644 --- a/ClosedXML/Excel/CalcEngine/Expression.cs +++ b/ClosedXML/Excel/CalcEngine/Expression.cs @@ -1,11 +1,17 @@ using System; -using System.Threading; using System.Collections; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Threading; namespace ClosedXML.Excel.CalcEngine { + internal abstract class ExpressionBase + { + public abstract string LastParseItem { get; } + } + /// /// Base class that represents parsed expressions. /// @@ -16,34 +22,39 @@ /// object val = expr.Evaluate(); /// /// - internal class Expression : IComparable + internal class Expression : ExpressionBase, IComparable { //--------------------------------------------------------------------------- + #region ** fields - internal Token _token; + internal readonly Token _token; - #endregion + #endregion ** fields //--------------------------------------------------------------------------- + #region ** ctors internal Expression() { _token = new Token(null, TKID.ATOM, TKTYPE.IDENTIFIER); } + internal Expression(object value) { _token = new Token(value, TKID.ATOM, TKTYPE.LITERAL); } + internal Expression(Token tk) { _token = tk; } - #endregion + #endregion ** ctors //--------------------------------------------------------------------------- + #region ** object model public virtual object Evaluate() @@ -54,14 +65,16 @@ } return _token.Value; } + public virtual Expression Optimize() { return this; } - #endregion + #endregion ** object model //--------------------------------------------------------------------------- + #region ** implicit converters public static implicit operator string(Expression x) @@ -69,6 +82,7 @@ var v = x.Evaluate(); return v == null ? string.Empty : v.ToString(); } + public static implicit operator double(Expression x) { // evaluate @@ -102,6 +116,7 @@ CultureInfo _ci = Thread.CurrentThread.CurrentCulture; return (double)Convert.ChangeType(v, typeof(double), _ci); } + public static implicit operator bool(Expression x) { // evaluate @@ -128,6 +143,7 @@ // handle everything else return (double)x == 0 ? false : true; } + public static implicit operator DateTime(Expression x) { // evaluate @@ -150,9 +166,10 @@ return (DateTime)Convert.ChangeType(v, typeof(DateTime), _ci); } - #endregion + #endregion ** implicit converters //--------------------------------------------------------------------------- + #region ** IComparable public int CompareTo(Expression other) @@ -197,15 +214,27 @@ return c1.CompareTo(c2); } - #endregion + #endregion ** IComparable + + //--------------------------------------------------------------------------- + + #region ** ExpressionBase + + public override string LastParseItem + { + get { return _token?.Value?.ToString() ?? "Unknown value"; } + } + + #endregion ** ExpressionBase } + /// /// Unary expression, e.g. +123 /// - class UnaryExpression : Expression + internal class UnaryExpression : Expression { // ** fields - Expression _expr; + private Expression _expr; // ** ctor public UnaryExpression(Token tk, Expression expr) : base(tk) @@ -220,11 +249,13 @@ { case TKID.ADD: return +(double)_expr; + case TKID.SUB: return -(double)_expr; } throw new ArgumentException("Bad expression."); } + public override Expression Optimize() { _expr = _expr.Optimize(); @@ -232,20 +263,27 @@ ? new Expression(this.Evaluate()) : this; } + + public override string LastParseItem + { + get { return _expr.LastParseItem; } + } } + /// /// Binary expression, e.g. 1+2 /// - class BinaryExpression : Expression + internal class BinaryExpression : Expression { // ** fields - Expression _lft; - Expression _rgt; + private Expression _lft; + + private Expression _rgt; // ** ctor public BinaryExpression(Token tk, Expression exprLeft, Expression exprRight) : base(tk) { - _lft = exprLeft; + _lft = exprLeft; _rgt = exprRight; } @@ -272,18 +310,25 @@ { case TKID.CONCAT: return (string)_lft + (string)_rgt; + case TKID.ADD: return (double)_lft + (double)_rgt; + case TKID.SUB: return (double)_lft - (double)_rgt; + case TKID.MUL: return (double)_lft * (double)_rgt; + case TKID.DIV: return (double)_lft / (double)_rgt; + case TKID.DIVINT: return (double)(int)((double)_lft / (double)_rgt); + case TKID.MOD: return (double)(int)((double)_lft % (double)_rgt); + case TKID.POWER: var a = (double)_lft; var b = (double)_rgt; @@ -297,6 +342,7 @@ } throw new ArgumentException("Bad expression."); } + public override Expression Optimize() { _lft = _lft.Optimize(); @@ -305,20 +351,27 @@ ? new Expression(this.Evaluate()) : this; } + + public override string LastParseItem + { + get { return _rgt.LastParseItem; } + } } + /// /// Function call expression, e.g. sin(0.5) /// - class FunctionExpression : Expression + internal class FunctionExpression : Expression { // ** fields - FunctionDefinition _fn; - List _parms; + private readonly FunctionDefinition _fn; + + private readonly List _parms; // ** ctor internal FunctionExpression() - { - } + { } + public FunctionExpression(FunctionDefinition function, List parms) { _fn = function; @@ -330,6 +383,7 @@ { return _fn.Function(_parms); } + public override Expression Optimize() { bool allLits = true; @@ -349,33 +403,46 @@ ? new Expression(this.Evaluate()) : this; } + + public override string LastParseItem + { + get { return _parms.Last().LastParseItem; } + } } + /// /// Simple variable reference. /// - class VariableExpression : Expression + internal class VariableExpression : Expression { - Dictionary _dct; - string _name; + private readonly Dictionary _dct; + private readonly string _name; public VariableExpression(Dictionary dct, string name) { _dct = dct; _name = name; } + public override object Evaluate() { return _dct[_name]; } + + public override string LastParseItem + { + get { return _name; } + } } + /// /// Expression that represents an external object. /// - class XObjectExpression : + internal class XObjectExpression : Expression, IEnumerable { - object _value; + private readonly object _value; // ** ctor internal XObjectExpression(object value) @@ -398,18 +465,29 @@ // return raw object return _value; } + public IEnumerator GetEnumerator() { return (_value as IEnumerable).GetEnumerator(); } + + public override string LastParseItem + { + get { return Value.ToString(); } + } } /// /// Expression that represents an omitted parameter. /// - class EmptyValueExpression : Expression + internal class EmptyValueExpression : Expression { internal EmptyValueExpression() { } + + public override string LastParseItem + { + get { return ""; } + } } /// diff --git a/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs index 5a25661..2ea565c 100644 --- a/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs +++ b/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs @@ -30,7 +30,7 @@ ce.RegisterFunction("FACT", 1, Fact); ce.RegisterFunction("FACTDOUBLE", 1, FactDouble); ce.RegisterFunction("FLOOR", 1, 2, Floor); - //ce.RegisterFunction("FLOOR.MATH", 1, 3, FloorMath); + ce.RegisterFunction("FLOOR.MATH", 1, 3, FloorMath); ce.RegisterFunction("GCD", 1, 255, Gcd); ce.RegisterFunction("INT", 1, Int); ce.RegisterFunction("LCM", 1, 255, Lcm); @@ -139,6 +139,23 @@ return Math.Floor(number / significance) * significance; } + private static object FloorMath(List p) + { + double number = p[0]; + double significance = 1; + if (p.Count > 1) significance = p[1]; + + double mode = 0; + if (p.Count > 2) mode = p[2]; + + if (number >= 0) + return Math.Floor(number / Math.Abs(significance)) * Math.Abs(significance); + else if (mode >= 0) + return Math.Floor(number / Math.Abs(significance)) * Math.Abs(significance); + else + return -Math.Floor(-number / Math.Abs(significance)) * Math.Abs(significance); + } + private static object Int(List p) { return (int)((double)p[0]); diff --git a/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs b/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs index 6d30f1f..1bdbf76 100644 --- a/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs +++ b/ClosedXML_Tests/Excel/CalcEngine/MathTrigTests.cs @@ -7,6 +7,8 @@ [TestFixture] public class MathTrigTests { + private readonly double tolerance = 1e-10; + [Test] public void Floor() { @@ -43,23 +45,44 @@ Assert.AreEqual(-4, actual); } - //[Test] + [Test] // Functions have to support a period first before we can implement this public void FloorMath() { - Object actual; + double actual; - actual = XLWorkbook.EvaluateExpr(@"FLOOR.MATH(24.3, 5)"); - Assert.AreEqual(20, actual); + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(24.3, 5)"); + Assert.AreEqual(20, actual, tolerance); - actual = XLWorkbook.EvaluateExpr(@"FLOOR.MATH(6.7)"); - Assert.AreEqual(6, actual); + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(6.7)"); + Assert.AreEqual(6, actual, tolerance); - actual = XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-8.1, 2)"); - Assert.AreEqual(-10, actual); + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-8.1, 2)"); + Assert.AreEqual(-10, actual, tolerance); - actual = XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-5.5, 2, -1)"); - Assert.AreEqual(-4, actual); + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(5.5, 2.1, 0)"); + Assert.AreEqual(4.2, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(5.5, -2.1, 0)"); + Assert.AreEqual(4.2, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(5.5, 2.1, -1)"); + Assert.AreEqual(4.2, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(5.5, -2.1, -1)"); + Assert.AreEqual(4.2, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-5.5, 2.1, 0)"); + Assert.AreEqual(-6.3, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-5.5, -2.1, 0)"); + Assert.AreEqual(-6.3, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-5.5, 2.1, -1)"); + Assert.AreEqual(-4.2, actual, tolerance); + + actual = (double)XLWorkbook.EvaluateExpr(@"FLOOR.MATH(-5.5, -2.1, -1)"); + Assert.AreEqual(-4.2, actual, tolerance); } } }