diff --git a/ClosedXML/Excel/CalcEngine/CalcEngine.cs b/ClosedXML/Excel/CalcEngine/CalcEngine.cs index 1b18b15..9ba60b2 100644 --- a/ClosedXML/Excel/CalcEngine/CalcEngine.cs +++ b/ClosedXML/Excel/CalcEngine/CalcEngine.cs @@ -1,279 +1,279 @@ -using System; -using System.Reflection; -using System.Collections.Generic; -using System.ComponentModel; -using System.Globalization; -using System.Diagnostics; -using System.Text; -using System.Text.RegularExpressions; -using ClosedXML.Excel.CalcEngine; -using ClosedXML.Excel.CalcEngine.Functions; - -namespace ClosedXML.Excel.CalcEngine { - /// - /// CalcEngine parses strings and returns Expression objects that can - /// be evaluated. - /// - /// - /// This class has three extensibility points: - /// Use the DataContext property to add an object's properties to the engine scope. - /// Use the RegisterFunction method to define custom functions. - /// Override the GetExternalObject method to add arbitrary variables to the engine scope. - /// +using System; +using System.Reflection; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.Diagnostics; +using System.Text; +using System.Text.RegularExpressions; +using ClosedXML.Excel.CalcEngine; +using ClosedXML.Excel.CalcEngine.Functions; + +namespace ClosedXML.Excel.CalcEngine { + /// + /// CalcEngine parses strings and returns Expression objects that can + /// be evaluated. + /// + /// + /// This class has three extensibility points: + /// Use the DataContext property to add an object's properties to the engine scope. + /// Use the RegisterFunction method to define custom functions. + /// Override the GetExternalObject method to add arbitrary variables to the engine scope. + /// internal class CalcEngine { //--------------------------------------------------------------------------- - #region ** fields + #region ** fields // members string _expr; // expression being parsed int _len; // length of the expression being parsed - int _ptr; // current pointer into expression + int _ptr; // current pointer into expression string _idChars; // valid characters in identifiers (besides alpha and digits) - Token _token; // current token being parsed - Dictionary _tkTbl; // table with tokens (+, -, etc) - Dictionary _fnTbl; // table with constants and functions (pi, sin, etc) - Dictionary _vars; // table with variables - object _dataContext; // object with properties - bool _optimize; // optimize expressions when parsing - ExpressionCache _cache; // cache with parsed expressions - CultureInfo _ci; // culture info used to parse numbers/dates - char _decimal, _listSep, _percent; // localized decimal separator, list separator, percent sign - - #endregion - - //--------------------------------------------------------------------------- - #region ** ctor - - public CalcEngine() { - CultureInfo = CultureInfo.InvariantCulture; - _tkTbl = GetSymbolTable(); - _fnTbl = GetFunctionTable(); - _vars = new Dictionary(StringComparer.OrdinalIgnoreCase); - _cache = new ExpressionCache(this); - _optimize = true; -#if DEBUG - //this.Test(); -#endif - } + Token _token; // current token being parsed + Dictionary _tkTbl; // table with tokens (+, -, etc) + Dictionary _fnTbl; // table with constants and functions (pi, sin, etc) + Dictionary _vars; // table with variables + object _dataContext; // object with properties + bool _optimize; // optimize expressions when parsing + ExpressionCache _cache; // cache with parsed expressions + CultureInfo _ci; // culture info used to parse numbers/dates + char _decimal, _listSep, _percent; // localized decimal separator, list separator, percent sign - #endregion + #endregion //--------------------------------------------------------------------------- - #region ** object model + #region ** ctor - /// - /// Parses a string into an . - /// - /// String to parse. - /// An object that can be evaluated. + public CalcEngine() { + CultureInfo = CultureInfo.InvariantCulture; + _tkTbl = GetSymbolTable(); + _fnTbl = GetFunctionTable(); + _vars = new Dictionary(StringComparer.OrdinalIgnoreCase); + _cache = new ExpressionCache(this); + _optimize = true; +#if DEBUG + //this.Test(); +#endif + } + + #endregion + + //--------------------------------------------------------------------------- + #region ** object model + + /// + /// Parses a string into an . + /// + /// String to parse. + /// An object that can be evaluated. public Expression Parse(string expression) { // initialize _expr = expression; _len = _expr.Length; - _ptr = 0; - - // skip leading equals sign - if (_len > 0 && _expr[0] == '=') { - _ptr++; + _ptr = 0; + + // skip leading equals sign + if (_len > 0 && _expr[0] == '=') { + _ptr++; } // parse the expression var expr = ParseExpression(); // check for errors - if (_token.ID != TKID.END) { + if (_token.ID != TKID.END) { Throw(); - } - - // optimize expression - if (_optimize) { - expr = expr.Optimize(); + } + + // optimize expression + if (_optimize) { + expr = expr.Optimize(); } // done return expr; - } - /// - /// Evaluates a string. - /// - /// Expression to evaluate. - /// The value of the expression. - /// - /// If you are going to evaluate the same expression several times, - /// it is more efficient to parse it only once using the - /// method and then using the Expression.Evaluate method to evaluate - /// the parsed expression. - /// - public object Evaluate(string expression) { - var x = //Parse(expression); - _cache != null - ? _cache[expression] + } + /// + /// Evaluates a string. + /// + /// Expression to evaluate. + /// The value of the expression. + /// + /// If you are going to evaluate the same expression several times, + /// it is more efficient to parse it only once using the + /// method and then using the Expression.Evaluate method to evaluate + /// the parsed expression. + /// + public object Evaluate(string expression) { + var x = //Parse(expression); + _cache != null + ? _cache[expression] : Parse(expression); return x.Evaluate(); - } - /// - /// Gets or sets whether the calc engine should keep a cache with parsed - /// expressions. - /// - public bool CacheExpressions { - get { return _cache != null; } - set { - if (value != CacheExpressions) { - _cache = value - ? new ExpressionCache(this) - : null; - } - } - } - /// - /// Gets or sets whether the calc engine should optimize expressions when - /// they are parsed. - /// - public bool OptimizeExpressions { - get { return _optimize; } - set { _optimize = value; } - } - /// - /// Gets or sets a string that specifies special characters that are valid for identifiers. - /// - /// - /// Identifiers must start with a letter or an underscore, which may be followed by - /// additional letters, underscores, or digits. This string allows you to specify - /// additional valid characters such as ':' or '!' (used in Excel range references - /// for example). - /// - public string IdentifierChars { - get { return _idChars; } - set { _idChars = value; } - } - /// - /// Registers a function that can be evaluated by this . - /// - /// Function name. - /// Minimum parameter count. - /// Maximum parameter count. - /// Delegate that evaluates the function. - public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn) { - _fnTbl.Add(functionName, new FunctionDefinition(parmMin, parmMax, fn)); - } - /// - /// Registers a function that can be evaluated by this . - /// - /// Function name. - /// Parameter count. - /// Delegate that evaluates the function. - public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn) { - RegisterFunction(functionName, parmCount, parmCount, fn); - } - /// - /// Gets an external object based on an identifier. - /// - /// - /// This method is useful when the engine needs to create objects dynamically. - /// For example, a spreadsheet calc engine would use this method to dynamically create cell - /// range objects based on identifiers that cannot be enumerated at design time - /// (such as "AB12", "A1:AB12", etc.) - /// - public virtual object GetExternalObject(string identifier) { - return null; - } - /// - /// Gets or sets the DataContext for this . - /// - /// - /// Once a DataContext is set, all public properties of the object become available - /// to the CalcEngine, including sub-properties such as "Address.Street". These may - /// be used with expressions just like any other constant. - /// - public virtual object DataContext { - get { return _dataContext; } - set { _dataContext = value; } - } - /// - /// Gets the dictionary that contains function definitions. - /// - public Dictionary Functions { - get { return _fnTbl; } - } - /// - /// Gets the dictionary that contains simple variables (not in the DataContext). - /// - public Dictionary Variables { - get { return _vars; } - } - /// - /// Gets or sets the to use when parsing numbers and dates. - /// - public CultureInfo CultureInfo { - get { return _ci; } - set { - _ci = value; - var nf = _ci.NumberFormat; - _decimal = nf.NumberDecimalSeparator[0]; - _percent = nf.PercentSymbol[0]; - _listSep = _ci.TextInfo.ListSeparator[0]; - } + } + /// + /// Gets or sets whether the calc engine should keep a cache with parsed + /// expressions. + /// + public bool CacheExpressions { + get { return _cache != null; } + set { + if (value != CacheExpressions) { + _cache = value + ? new ExpressionCache(this) + : null; + } + } + } + /// + /// Gets or sets whether the calc engine should optimize expressions when + /// they are parsed. + /// + public bool OptimizeExpressions { + get { return _optimize; } + set { _optimize = value; } + } + /// + /// Gets or sets a string that specifies special characters that are valid for identifiers. + /// + /// + /// Identifiers must start with a letter or an underscore, which may be followed by + /// additional letters, underscores, or digits. This string allows you to specify + /// additional valid characters such as ':' or '!' (used in Excel range references + /// for example). + /// + public string IdentifierChars { + get { return _idChars; } + set { _idChars = value; } + } + /// + /// Registers a function that can be evaluated by this . + /// + /// Function name. + /// Minimum parameter count. + /// Maximum parameter count. + /// Delegate that evaluates the function. + public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn) { + _fnTbl.Add(functionName, new FunctionDefinition(parmMin, parmMax, fn)); + } + /// + /// Registers a function that can be evaluated by this . + /// + /// Function name. + /// Parameter count. + /// Delegate that evaluates the function. + public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn) { + RegisterFunction(functionName, parmCount, parmCount, fn); + } + /// + /// Gets an external object based on an identifier. + /// + /// + /// This method is useful when the engine needs to create objects dynamically. + /// For example, a spreadsheet calc engine would use this method to dynamically create cell + /// range objects based on identifiers that cannot be enumerated at design time + /// (such as "AB12", "A1:AB12", etc.) + /// + public virtual object GetExternalObject(string identifier) { + return null; + } + /// + /// Gets or sets the DataContext for this . + /// + /// + /// Once a DataContext is set, all public properties of the object become available + /// to the CalcEngine, including sub-properties such as "Address.Street". These may + /// be used with expressions just like any other constant. + /// + public virtual object DataContext { + get { return _dataContext; } + set { _dataContext = value; } + } + /// + /// Gets the dictionary that contains function definitions. + /// + public Dictionary Functions { + get { return _fnTbl; } + } + /// + /// Gets the dictionary that contains simple variables (not in the DataContext). + /// + public Dictionary Variables { + get { return _vars; } + } + /// + /// Gets or sets the to use when parsing numbers and dates. + /// + public CultureInfo CultureInfo { + get { return _ci; } + set { + _ci = value; + var nf = _ci.NumberFormat; + _decimal = nf.NumberDecimalSeparator[0]; + _percent = nf.PercentSymbol[0]; + _listSep = _ci.TextInfo.ListSeparator[0]; + } } - #endregion + #endregion //--------------------------------------------------------------------------- - #region ** token/keyword tables + #region ** token/keyword tables // build/get static token table - Dictionary GetSymbolTable() { - if (_tkTbl == null) { - _tkTbl = new Dictionary(); - AddToken('&', TKID.CONCAT, TKTYPE.ADDSUB); - AddToken('+', TKID.ADD, TKTYPE.ADDSUB); - AddToken('-', TKID.SUB, TKTYPE.ADDSUB); - AddToken('(', TKID.OPEN, TKTYPE.GROUP); - AddToken(')', TKID.CLOSE, TKTYPE.GROUP); - AddToken('*', TKID.MUL, TKTYPE.MULDIV); - AddToken('.', TKID.PERIOD, TKTYPE.GROUP); - AddToken('/', TKID.DIV, TKTYPE.MULDIV); - AddToken('\\', TKID.DIVINT, TKTYPE.MULDIV); - AddToken('=', TKID.EQ, TKTYPE.COMPARE); - AddToken('>', TKID.GT, TKTYPE.COMPARE); - AddToken('<', TKID.LT, TKTYPE.COMPARE); - AddToken('^', TKID.POWER, TKTYPE.POWER); - AddToken("<>", TKID.NE, TKTYPE.COMPARE); - AddToken(">=", TKID.GE, TKTYPE.COMPARE); + Dictionary GetSymbolTable() { + if (_tkTbl == null) { + _tkTbl = new Dictionary(); + AddToken('&', TKID.CONCAT, TKTYPE.ADDSUB); + AddToken('+', TKID.ADD, TKTYPE.ADDSUB); + AddToken('-', TKID.SUB, TKTYPE.ADDSUB); + AddToken('(', TKID.OPEN, TKTYPE.GROUP); + AddToken(')', TKID.CLOSE, TKTYPE.GROUP); + AddToken('*', TKID.MUL, TKTYPE.MULDIV); + AddToken('.', TKID.PERIOD, TKTYPE.GROUP); + AddToken('/', TKID.DIV, TKTYPE.MULDIV); + AddToken('\\', TKID.DIVINT, TKTYPE.MULDIV); + AddToken('=', TKID.EQ, TKTYPE.COMPARE); + AddToken('>', TKID.GT, TKTYPE.COMPARE); + AddToken('<', TKID.LT, TKTYPE.COMPARE); + AddToken('^', TKID.POWER, TKTYPE.POWER); + AddToken("<>", TKID.NE, TKTYPE.COMPARE); + AddToken(">=", TKID.GE, TKTYPE.COMPARE); AddToken("<=", TKID.LE, TKTYPE.COMPARE); // list separator is localized, not necessarily a comma // so it can't be on the static table //AddToken(',', TKID.COMMA, TKTYPE.GROUP); - } - return _tkTbl; - } - void AddToken(object symbol, TKID id, TKTYPE type) { - var token = new Token(symbol, id, type); - _tkTbl.Add(symbol, token); - } - - // build/get static keyword table - Dictionary GetFunctionTable() { - if (_fnTbl == null) { - // create table - _fnTbl = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - // register built-in functions (and constants) - Information.Register(this); - Logical.Register(this); - Lookup.Register(this); - MathTrig.Register(this); - Text.Register(this); - Statistical.Register(this); - DateAndTime.Register(this); - } - return _fnTbl; + } + return _tkTbl; + } + void AddToken(object symbol, TKID id, TKTYPE type) { + var token = new Token(symbol, id, type); + _tkTbl.Add(symbol, token); } - #endregion + // build/get static keyword table + Dictionary GetFunctionTable() { + if (_fnTbl == null) { + // create table + _fnTbl = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + // register built-in functions (and constants) + Information.Register(this); + Logical.Register(this); + Lookup.Register(this); + MathTrig.Register(this); + Text.Register(this); + Statistical.Register(this); + DateAndTime.Register(this); + } + return _fnTbl; + } + + #endregion //--------------------------------------------------------------------------- - #region ** private stuff + #region ** private stuff Expression ParseExpression() { GetToken(); @@ -297,7 +297,7 @@ var exprArg = ParseMulDiv(); x = new BinaryExpression(t, x, exprArg); } - return x; + return x; } Expression ParseMulDiv() { var x = ParsePower(); @@ -307,7 +307,7 @@ var a = ParsePower(); x = new BinaryExpression(t, x, a); } - return x; + return x; } Expression ParsePower() { var x = ParseUnary(); @@ -323,76 +323,76 @@ // unary plus and minus if (_token.ID == TKID.ADD || _token.ID == TKID.SUB) { var t = _token; - GetToken(); - var a = ParseAtom(); + GetToken(); + var a = ParseAtom(); return new UnaryExpression(t, a); } // not unary, return atom return ParseAtom(); } - Expression ParseAtom() { - string id; - Expression x = null; + Expression ParseAtom() { + string id; + Expression x = null; FunctionDefinition fnDef = null; switch (_token.Type) { // literals case TKTYPE.LITERAL: x = new Expression(_token); - break; - - // identifiers - case TKTYPE.IDENTIFIER: - - // get identifier - id = (string)_token.Value; - - // look for functions - if (_fnTbl.TryGetValue(id, out fnDef)) { - var p = GetParameters(); - var pCnt = p == null ? 0 : p.Count; - if (fnDef.ParmMin != -1 && pCnt < fnDef.ParmMin) { - Throw("Too few parameters."); - } - if (fnDef.ParmMax != -1 && pCnt > fnDef.ParmMax) { - Throw("Too many parameters."); - } - x = new FunctionExpression(fnDef, p); - break; - } - - // look for simple variables (much faster than binding!) - if (_vars.ContainsKey(id)) { - x = new VariableExpression(_vars, id); - break; - } - - // look for external objects - var xObj = GetExternalObject(id); - if (xObj != null) { - x = new XObjectExpression(xObj); - break; - } - - // look for bindings - if (DataContext != null) { - var list = new List(); - for (var t = _token; t != null; t = GetMember()) { - list.Add(new BindingInfo((string)t.Value, GetParameters())); - } - x = new BindingExpression(this, list, _ci); - break; - } - Throw("Unexpected identifier"); + break; + + // identifiers + case TKTYPE.IDENTIFIER: + + // get identifier + id = (string)_token.Value; + + // look for functions + if (_fnTbl.TryGetValue(id, out fnDef)) { + var p = GetParameters(); + var pCnt = p == null ? 0 : p.Count; + if (fnDef.ParmMin != -1 && pCnt < fnDef.ParmMin) { + Throw("Too few parameters."); + } + if (fnDef.ParmMax != -1 && pCnt > fnDef.ParmMax) { + Throw("Too many parameters."); + } + x = new FunctionExpression(fnDef, p); + break; + } + + // look for simple variables (much faster than binding!) + if (_vars.ContainsKey(id)) { + x = new VariableExpression(_vars, id); + break; + } + + // look for external objects + var xObj = GetExternalObject(id); + if (xObj != null) { + x = new XObjectExpression(xObj); + break; + } + + // look for bindings + if (DataContext != null) { + var list = new List(); + for (var t = _token; t != null; t = GetMember()) { + list.Add(new BindingInfo((string)t.Value, GetParameters())); + } + x = new BindingExpression(this, list, _ci); + break; + } + Throw("Unexpected identifier"); break; // sub-expressions case TKTYPE.GROUP: // anything other than opening parenthesis is illegal here - if (_token.ID != TKID.OPEN) { - Throw("Expression expected."); + if (_token.ID != TKID.OPEN) { + Throw("Expression expected."); } // get expression @@ -405,11 +405,11 @@ } break; - } - - // make sure we got something... - if (x == null) { - Throw(); + } + + // make sure we got something... + if (x == null) { + Throw(); } // done @@ -417,10 +417,10 @@ return x; } - #endregion + #endregion //--------------------------------------------------------------------------- - #region ** parser + #region ** parser void GetToken() { // eat white space @@ -429,7 +429,7 @@ } // are we done? - if (_ptr >= _len) { + if (_ptr >= _len) { _token = new Token(null, TKID.END, TKTYPE.GROUP); return; } @@ -441,29 +441,29 @@ // operators // this gets called a lot, so it's pretty optimized. // note that operators must start with non-letter/digit characters. - var isLetter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + var isLetter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); var isDigit = c >= '0' && c <= '9'; if (!isLetter && !isDigit) { // if this is a number starting with a decimal, don't parse as operator - var nxt = _ptr + 1 < _len ? _expr[_ptr + 1] : 0; + var nxt = _ptr + 1 < _len ? _expr[_ptr + 1] : 0; bool isNumber = c == _decimal && nxt >= '0' && nxt <= '9'; - if (!isNumber) { - // look up localized list separator - if (c == _listSep) { - _token = new Token(c, TKID.COMMA, TKTYPE.GROUP); - _ptr++; - return; + if (!isNumber) { + // look up localized list separator + if (c == _listSep) { + _token = new Token(c, TKID.COMMA, TKTYPE.GROUP); + _ptr++; + return; } // look up single-char tokens on table - Token tk; + Token tk; if (_tkTbl.TryGetValue(c, out tk)) { // save token we found _token = tk; _ptr++; // look for double-char tokens (special case) - if (_ptr < _len && (c == '>' || c == '<')) { + if (_ptr < _len && (c == '>' || c == '<')) { if (_tkTbl.TryGetValue(_expr.Substring(_ptr - 1, 2), out tk)) { _token = tk; _ptr++; @@ -478,20 +478,20 @@ // parse numbers if (isDigit || c == _decimal) { - var sci = false; - var pct = false; - var div = -1.0; // use double, not int (this may get really big) - var val = 0.0; + var sci = false; + var pct = false; + var div = -1.0; // use double, not int (this may get really big) + var val = 0.0; for (i = 0; i + _ptr < _len; i++) { - c = _expr[_ptr + i]; - - // digits always OK - if (c >= '0' && c <= '9') { - val = val * 10 + (c - '0'); - if (div > -1) { - div *= 10; - } - continue; + c = _expr[_ptr + i]; + + // digits always OK + if (c >= '0' && c <= '9') { + val = val * 10 + (c - '0'); + if (div > -1) { + div *= 10; + } + continue; } // one decimal is OK @@ -506,38 +506,38 @@ c = _expr[_ptr + i + 1]; if (c == '+' || c == '-') i++; continue; - } - - // percentage? - if (c == _percent) { - pct = true; - i++; - break; + } + + // percentage? + if (c == _percent) { + pct = true; + i++; + break; } // end of literal break; - } - - // end of number, get value - if (!sci) { - // much faster than ParseDouble - if (div > 1) { - val /= div; - } - if (pct) { - val /= 100.0; - } - } else { - var lit = _expr.Substring(_ptr, i); - val = ParseDouble(lit, _ci); - } - - // build token - _token = new Token(val, TKID.ATOM, TKTYPE.LITERAL); - - // advance pointer and return - _ptr += i; + } + + // end of number, get value + if (!sci) { + // much faster than ParseDouble + if (div > 1) { + val /= div; + } + if (pct) { + val /= 100.0; + } + } else { + var lit = _expr.Substring(_ptr, i); + val = ParseDouble(lit, _ci); + } + + // build token + _token = new Token(val, TKID.ATOM, TKTYPE.LITERAL); + + // advance pointer and return + _ptr += i; return; } @@ -559,7 +559,7 @@ // end of string var lit = _expr.Substring(_ptr + 1, i - 1); - _ptr += i + 1; + _ptr += i + 1; _token = new Token(lit.Replace("\"\"", "\""), TKID.ATOM, TKTYPE.LITERAL); return; } @@ -579,38 +579,38 @@ // end of date var lit = _expr.Substring(_ptr + 1, i - 1); - _ptr += i + 1; + _ptr += i + 1; _token = new Token(DateTime.Parse(lit, _ci), TKID.ATOM, TKTYPE.LITERAL); return; - } - - // identifiers (functions, objects) must start with alpha or underscore - if (!isLetter && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) { - Throw("Identifier expected."); - } - - // and must contain only letters/digits/_idChars - for (i = 1; i + _ptr < _len; i++) { - c = _expr[_ptr + i]; - isLetter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); - isDigit = c >= '0' && c <= '9'; - if (!isLetter && !isDigit && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) { - break; - } - } - - // got identifier - var id = _expr.Substring(_ptr, i); - _ptr += i; + } + + // identifiers (functions, objects) must start with alpha or underscore + if (!isLetter && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) { + Throw("Identifier expected."); + } + + // and must contain only letters/digits/_idChars + for (i = 1; i + _ptr < _len; i++) { + c = _expr[_ptr + i]; + isLetter = (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + isDigit = c >= '0' && c <= '9'; + if (!isLetter && !isDigit && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) { + break; + } + } + + // got identifier + var id = _expr.Substring(_ptr, i); + _ptr += i; _token = new Token(id, TKID.ATOM, TKTYPE.IDENTIFIER); - } - static double ParseDouble(string str, CultureInfo ci) { - if (str.Length > 0 && str[str.Length - 1] == ci.NumberFormat.PercentSymbol[0]) { - str = str.Substring(0, str.Length - 1); - return double.Parse(str, NumberStyles.Any, ci) / 100.0; - } - return double.Parse(str, NumberStyles.Any, ci); - } + } + static double ParseDouble(string str, CultureInfo ci) { + if (str.Length > 0 && str[str.Length - 1] == ci.NumberFormat.PercentSymbol[0]) { + str = str.Substring(0, str.Length - 1); + return double.Parse(str, NumberStyles.Any, ci) / 100.0; + } + return double.Parse(str, NumberStyles.Any, ci); + } List GetParameters() // e.g. myfun(a, b, c+2) { // check whether next token is a (, @@ -618,17 +618,17 @@ var pos = _ptr; var tk = _token; GetToken(); - if (_token.ID != TKID.OPEN) { - _ptr = pos; + if (_token.ID != TKID.OPEN) { + _ptr = pos; _token = tk; return null; } // check for empty Parameter list pos = _ptr; - GetToken(); - if (_token.ID == TKID.CLOSE) { - return null; + GetToken(); + if (_token.ID == TKID.CLOSE) { + return null; } _ptr = pos; @@ -642,53 +642,53 @@ } // make sure the list was closed correctly - if (_token.ID != TKID.CLOSE) { + if (_token.ID != TKID.CLOSE) { Throw(); } // done return parms; - } - Token GetMember() { - // check whether next token is a MEMBER token ('.'), - // restore state and bail if it's not - var pos = _ptr; - var tk = _token; - GetToken(); - if (_token.ID != TKID.PERIOD) { - _ptr = pos; - _token = tk; - return null; - } - - // skip member token - GetToken(); - if (_token.Type != TKTYPE.IDENTIFIER) { - Throw("Identifier expected"); - } - return _token; + } + Token GetMember() { + // check whether next token is a MEMBER token ('.'), + // restore state and bail if it's not + var pos = _ptr; + var tk = _token; + GetToken(); + if (_token.ID != TKID.PERIOD) { + _ptr = pos; + _token = tk; + return null; + } + + // skip member token + GetToken(); + if (_token.Type != TKTYPE.IDENTIFIER) { + Throw("Identifier expected"); + } + return _token; } - #endregion + #endregion //--------------------------------------------------------------------------- - #region ** static helpers + #region ** static helpers - static void Throw() { - Throw("Syntax error."); - } - static void Throw(string msg) { - throw new Exception(msg); + static void Throw() { + Throw("Syntax error."); + } + static void Throw(string msg) { + throw new Exception(msg); } - #endregion - } - - /// - /// Delegate that represents CalcEngine functions. - /// - /// List of objects that represent the - /// parameters to be used in the function call. - /// The function result. - internal delegate object CalcEngineFunction(List parms); -} + #endregion + } + + /// + /// Delegate that represents CalcEngine functions. + /// + /// List of objects that represent the + /// parameters to be used in the function call. + /// The function result. + internal delegate object CalcEngineFunction(List parms); +} diff --git a/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs b/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs index e794f99..013950b 100644 --- a/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs +++ b/ClosedXML_Tests/Excel/CalcEngine/TextTests.cs @@ -270,7 +270,7 @@ public void Proper_Value() { Object actual = XLWorkbook.EvaluateExpr(@"Proper(""my name is francois botha"")"); - Assert.AreEqual("My Name Information Francois Botha", actual); + Assert.AreEqual("My Name Is Francois Botha", actual); } [Test]