diff --git a/ClosedXML/ClosedXML.csproj b/ClosedXML/ClosedXML.csproj index c7da769..d4073e5 100644 --- a/ClosedXML/ClosedXML.csproj +++ b/ClosedXML/ClosedXML.csproj @@ -64,7 +64,7 @@ - + diff --git a/ClosedXML/Excel/CalcEngine/CalcEngine.cs b/ClosedXML/Excel/CalcEngine/CalcEngine.cs index b946fa2..9ba60b2 100644 --- a/ClosedXML/Excel/CalcEngine/CalcEngine.cs +++ b/ClosedXML/Excel/CalcEngine/CalcEngine.cs @@ -9,8 +9,7 @@ using ClosedXML.Excel.CalcEngine; using ClosedXML.Excel.CalcEngine.Functions; -namespace ClosedXML.Excel.CalcEngine -{ +namespace ClosedXML.Excel.CalcEngine { /// /// CalcEngine parses strings and returns Expression objects that can /// be evaluated. @@ -21,19 +20,18 @@ /// 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 + internal class CalcEngine { + //--------------------------------------------------------------------------- + #region ** fields - // members - string _expr; // expression being parsed - int _len; // length of the expression being parsed - int _ptr; // current pointer into expression - string _idChars; // valid characters in identifiers (besides alpha and digits) - Token _token; // current token being parsed + // members + string _expr; // expression being parsed + int _len; // length of the expression being parsed + 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 _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 @@ -46,8 +44,7 @@ //--------------------------------------------------------------------------- #region ** ctor - public CalcEngine() - { + public CalcEngine() { CultureInfo = CultureInfo.InvariantCulture; _tkTbl = GetSymbolTable(); _fnTbl = GetFunctionTable(); @@ -58,48 +55,44 @@ //this.Test(); #endif } - + #endregion //--------------------------------------------------------------------------- - #region ** object model + #region ** object model - /// - /// Parses a string into an . - /// - /// String to parse. + /// + /// 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; + public Expression Parse(string expression) { + // initialize + _expr = expression; + _len = _expr.Length; + _ptr = 0; // skip leading equals sign - if (_len > 0 && _expr[0] == '=') - { + if (_len > 0 && _expr[0] == '=') { _ptr++; } - // parse the expression - var expr = ParseExpression(); + // parse the expression + var expr = ParseExpression(); - // check for errors - if (_token.ID != TKID.END) - { + // check for errors + if (_token.ID != TKID.END) { Throw(); - } + } // optimize expression - if (_optimize) - { + if (_optimize) { expr = expr.Optimize(); } // done - return expr; - } + return expr; + } /// /// Evaluates a string. /// @@ -111,25 +104,21 @@ /// method and then using the Expression.Evaluate method to evaluate /// the parsed expression. /// - public object Evaluate(string expression) - { + public object Evaluate(string expression) { var x = //Parse(expression); _cache != null ? _cache[expression] : Parse(expression); - return x.Evaluate(); - } + return x.Evaluate(); + } /// /// Gets or sets whether the calc engine should keep a cache with parsed /// expressions. /// - public bool CacheExpressions - { + public bool CacheExpressions { get { return _cache != null; } - set - { - if (value != CacheExpressions) - { + set { + if (value != CacheExpressions) { _cache = value ? new ExpressionCache(this) : null; @@ -140,8 +129,7 @@ /// Gets or sets whether the calc engine should optimize expressions when /// they are parsed. /// - public bool OptimizeExpressions - { + public bool OptimizeExpressions { get { return _optimize; } set { _optimize = value; } } @@ -154,8 +142,7 @@ /// additional valid characters such as ':' or '!' (used in Excel range references /// for example). /// - public string IdentifierChars - { + public string IdentifierChars { get { return _idChars; } set { _idChars = value; } } @@ -166,8 +153,7 @@ /// Minimum parameter count. /// Maximum parameter count. /// Delegate that evaluates the function. - public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn) - { + public void RegisterFunction(string functionName, int parmMin, int parmMax, CalcEngineFunction fn) { _fnTbl.Add(functionName, new FunctionDefinition(parmMin, parmMax, fn)); } /// @@ -176,8 +162,7 @@ /// Function name. /// Parameter count. /// Delegate that evaluates the function. - public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn) - { + public void RegisterFunction(string functionName, int parmCount, CalcEngineFunction fn) { RegisterFunction(functionName, parmCount, parmCount, fn); } /// @@ -189,8 +174,7 @@ /// range objects based on identifiers that cannot be enumerated at design time /// (such as "AB12", "A1:AB12", etc.) /// - public virtual object GetExternalObject(string identifier) - { + public virtual object GetExternalObject(string identifier) { return null; } /// @@ -201,33 +185,28 @@ /// 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 - { + public virtual object DataContext { get { return _dataContext; } set { _dataContext = value; } } /// /// Gets the dictionary that contains function definitions. /// - public Dictionary Functions - { + public Dictionary Functions { get { return _fnTbl; } } /// /// Gets the dictionary that contains simple variables (not in the DataContext). /// - public Dictionary Variables - { + public Dictionary Variables { get { return _vars; } } /// /// Gets or sets the to use when parsing numbers and dates. /// - public CultureInfo CultureInfo - { + public CultureInfo CultureInfo { get { return _ci; } - set - { + set { _ci = value; var nf = _ci.NumberFormat; _decimal = nf.NumberDecimalSeparator[0]; @@ -236,16 +215,14 @@ } } - #endregion + #endregion //--------------------------------------------------------------------------- #region ** token/keyword tables // build/get static token table - Dictionary GetSymbolTable() - { - if (_tkTbl == null) - { + Dictionary GetSymbolTable() { + if (_tkTbl == null) { _tkTbl = new Dictionary(); AddToken('&', TKID.CONCAT, TKTYPE.ADDSUB); AddToken('+', TKID.ADD, TKTYPE.ADDSUB); @@ -263,29 +240,26 @@ 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) - { + 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) - { + Dictionary GetFunctionTable() { + if (_fnTbl == null) { // create table _fnTbl = new Dictionary(StringComparer.InvariantCultureIgnoreCase); // register built-in functions (and constants) - Is.Register(this); + Information.Register(this); Logical.Register(this); Lookup.Register(this); MathTrig.Register(this); @@ -299,87 +273,74 @@ #endregion //--------------------------------------------------------------------------- - #region ** private stuff + #region ** private stuff - Expression ParseExpression() - { - GetToken(); - return ParseCompare(); - } - Expression ParseCompare() - { - var x = ParseAddSub(); - while (_token.Type == TKTYPE.COMPARE) - { - var t = _token; - GetToken(); - var exprArg = ParseAddSub(); - x = new BinaryExpression(t, x, exprArg); - } - return x; - } - Expression ParseAddSub() - { - var x = ParseMulDiv(); - while (_token.Type == TKTYPE.ADDSUB) - { - var t = _token; - GetToken(); - var exprArg = ParseMulDiv(); - x = new BinaryExpression(t, x, exprArg); - } - return x; + Expression ParseExpression() { + GetToken(); + return ParseCompare(); } - Expression ParseMulDiv() - { - var x = ParsePower(); - while (_token.Type == TKTYPE.MULDIV) - { - var t = _token; - GetToken(); - var a = ParsePower(); - x = new BinaryExpression(t, x, a); - } - return x; + Expression ParseCompare() { + var x = ParseAddSub(); + while (_token.Type == TKTYPE.COMPARE) { + var t = _token; + GetToken(); + var exprArg = ParseAddSub(); + x = new BinaryExpression(t, x, exprArg); + } + return x; } - Expression ParsePower() - { - var x = ParseUnary(); - while (_token.Type == TKTYPE.POWER) - { - var t = _token; - GetToken(); - var a = ParseUnary(); - x = new BinaryExpression(t, x, a); - } - return x; - } - Expression ParseUnary() - { - // unary plus and minus - if (_token.ID == TKID.ADD || _token.ID == TKID.SUB) - { - var t = _token; - GetToken(); + Expression ParseAddSub() { + var x = ParseMulDiv(); + while (_token.Type == TKTYPE.ADDSUB) { + var t = _token; + GetToken(); + var exprArg = ParseMulDiv(); + x = new BinaryExpression(t, x, exprArg); + } + return x; + } + Expression ParseMulDiv() { + var x = ParsePower(); + while (_token.Type == TKTYPE.MULDIV) { + var t = _token; + GetToken(); + var a = ParsePower(); + x = new BinaryExpression(t, x, a); + } + return x; + } + Expression ParsePower() { + var x = ParseUnary(); + while (_token.Type == TKTYPE.POWER) { + var t = _token; + GetToken(); + var a = ParseUnary(); + x = new BinaryExpression(t, x, a); + } + return x; + } + Expression ParseUnary() { + // unary plus and minus + if (_token.ID == TKID.ADD || _token.ID == TKID.SUB) { + var t = _token; + GetToken(); var a = ParseAtom(); return new UnaryExpression(t, a); - } + } - // not unary, return atom - return ParseAtom(); - } - Expression ParseAtom() - { + // not unary, return atom + return ParseAtom(); + } + Expression ParseAtom() { string id; Expression x = null; FunctionDefinition fnDef = null; - switch (_token.Type) - { - // literals - case TKTYPE.LITERAL: - x = new Expression(_token); - break; + switch (_token.Type) { + // literals + case TKTYPE.LITERAL: + x = new Expression(_token); + break; // identifiers case TKTYPE.IDENTIFIER: @@ -388,16 +349,13 @@ id = (string)_token.Value; // look for functions - if (_fnTbl.TryGetValue(id, out fnDef)) - { + if (_fnTbl.TryGetValue(id, out fnDef)) { var p = GetParameters(); var pCnt = p == null ? 0 : p.Count; - if (fnDef.ParmMin != -1 && pCnt < fnDef.ParmMin) - { + if (fnDef.ParmMin != -1 && pCnt < fnDef.ParmMin) { Throw("Too few parameters."); } - if (fnDef.ParmMax != -1 && pCnt > fnDef.ParmMax) - { + if (fnDef.ParmMax != -1 && pCnt > fnDef.ParmMax) { Throw("Too many parameters."); } x = new FunctionExpression(fnDef, p); @@ -405,26 +363,22 @@ } // look for simple variables (much faster than binding!) - if (_vars.ContainsKey(id)) - { + if (_vars.ContainsKey(id)) { x = new VariableExpression(_vars, id); break; } // look for external objects var xObj = GetExternalObject(id); - if (xObj != null) - { + if (xObj != null) { x = new XObjectExpression(xObj); break; } // look for bindings - if (DataContext != null) - { + if (DataContext != null) { var list = new List(); - for (var t = _token; t != null; t = GetMember()) - { + for (var t = _token; t != null; t = GetMember()) { list.Add(new BindingInfo((string)t.Value, GetParameters())); } x = new BindingExpression(this, list, _ci); @@ -433,172 +387,148 @@ Throw("Unexpected identifier"); break; - // sub-expressions - case TKTYPE.GROUP: + // sub-expressions + case TKTYPE.GROUP: // anything other than opening parenthesis is illegal here - if (_token.ID != TKID.OPEN) - { + if (_token.ID != TKID.OPEN) { Throw("Expression expected."); } - // get expression - GetToken(); - x = ParseCompare(); + // get expression + GetToken(); + x = ParseCompare(); - // check that the parenthesis was closed - if (_token.ID != TKID.CLOSE) - { - Throw("Unbalanced parenthesis."); - } + // check that the parenthesis was closed + if (_token.ID != TKID.CLOSE) { + Throw("Unbalanced parenthesis."); + } - break; - } + break; + } // make sure we got something... - if (x == null) - { + if (x == null) { Throw(); } - // done - GetToken(); - return x; - } + // done + GetToken(); + return x; + } - #endregion + #endregion - //--------------------------------------------------------------------------- - #region ** parser + //--------------------------------------------------------------------------- + #region ** parser - void GetToken() - { - // eat white space - while (_ptr < _len && _expr[_ptr] <= ' ') - { - _ptr++; - } + void GetToken() { + // eat white space + while (_ptr < _len && _expr[_ptr] <= ' ') { + _ptr++; + } - // are we done? - if (_ptr >= _len) - { + // are we done? + if (_ptr >= _len) { _token = new Token(null, TKID.END, TKTYPE.GROUP); - return; - } + return; + } - // prepare to parse + // prepare to parse int i; - var c = _expr[_ptr]; + var c = _expr[_ptr]; - // operators - // this gets called a lot, so it's pretty optimized. - // note that operators must start with non-letter/digit characters. + // 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 isDigit = c >= '0' && c <= '9'; - if (!isLetter && !isDigit) - { - // if this is a number starting with a decimal, don't parse as operator + 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; bool isNumber = c == _decimal && nxt >= '0' && nxt <= '9'; - if (!isNumber) - { + if (!isNumber) { // look up localized list separator - if (c == _listSep) - { + if (c == _listSep) { _token = new Token(c, TKID.COMMA, TKTYPE.GROUP); _ptr++; return; } - + // look up single-char tokens on table Token tk; - if (_tkTbl.TryGetValue(c, out tk)) - { - // save token we found - _token = tk; - _ptr++; + 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 (_tkTbl.TryGetValue(_expr.Substring(_ptr - 1, 2), out tk)) - { - _token = tk; - _ptr++; - } - } + // look for double-char tokens (special case) + if (_ptr < _len && (c == '>' || c == '<')) { + if (_tkTbl.TryGetValue(_expr.Substring(_ptr - 1, 2), out tk)) { + _token = tk; + _ptr++; + } + } // found token on the table - return; - } - } - } + return; + } + } + } - // parse numbers - if (isDigit || c == _decimal) - { - var sci = false; + // 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; - for (i = 0; i + _ptr < _len; i++) - { - c = _expr[_ptr + i]; + for (i = 0; i + _ptr < _len; i++) { + c = _expr[_ptr + i]; // digits always OK - if (c >= '0' && c <= '9') - { + if (c >= '0' && c <= '9') { val = val * 10 + (c - '0'); - if (div > -1) - { + if (div > -1) { div *= 10; } continue; } - // one decimal is OK - if (c == _decimal && div < 0) - { - div = 1; - continue; - } - - // scientific notation? - if ((c == 'E' || c == 'e') && !sci) - { - sci = true; - c = _expr[_ptr + i + 1]; - if (c == '+' || c == '-') i++; - continue; - } + // one decimal is OK + if (c == _decimal && div < 0) { + div = 1; + continue; + } + + // scientific notation? + if ((c == 'E' || c == 'e') && !sci) { + sci = true; + c = _expr[_ptr + i + 1]; + if (c == '+' || c == '-') i++; + continue; + } // percentage? - if (c == _percent) - { + if (c == _percent) { pct = true; i++; break; } - // end of literal - break; - } + // end of literal + break; + } // end of number, get value - if (!sci) - { + if (!sci) { // much faster than ParseDouble - if (div > 1) - { + if (div > 1) { val /= div; } - if (pct) - { + if (pct) { val /= 100.0; } - } - else - { + } else { var lit = _expr.Substring(_ptr, i); val = ParseDouble(lit, _ci); } @@ -609,71 +539,62 @@ // advance pointer and return _ptr += i; return; - } + } - // parse strings - if (c == '\"') - { - // look for end quote, skip double quotes - for (i = 1; i + _ptr < _len; i++) - { - c = _expr[_ptr + i]; - if (c != '\"') continue; - char cNext = i + _ptr < _len - 1 ? _expr[_ptr + i + 1]: ' '; - if (cNext != '\"') break; - i++; - } + // parse strings + if (c == '\"') { + // look for end quote, skip double quotes + for (i = 1; i + _ptr < _len; i++) { + c = _expr[_ptr + i]; + if (c != '\"') continue; + char cNext = i + _ptr < _len - 1 ? _expr[_ptr + i + 1] : ' '; + if (cNext != '\"') break; + i++; + } - // check that we got the end of the string - if (c != '\"') - { - Throw("Can't find final quote."); - } + // check that we got the end of the string + if (c != '\"') { + Throw("Can't find final quote."); + } - // end of string - var lit = _expr.Substring(_ptr + 1, i - 1); - _ptr += i + 1; + // end of string + var lit = _expr.Substring(_ptr + 1, i - 1); + _ptr += i + 1; _token = new Token(lit.Replace("\"\"", "\""), TKID.ATOM, TKTYPE.LITERAL); - return; - } + return; + } - // parse dates (review) - if (c == '#') - { - // look for end # - for (i = 1; i + _ptr < _len; i++) - { - c = _expr[_ptr + i]; - if (c == '#') break; - } + // parse dates (review) + if (c == '#') { + // look for end # + for (i = 1; i + _ptr < _len; i++) { + c = _expr[_ptr + i]; + if (c == '#') break; + } - // check that we got the end of the date - if (c != '#') - { - Throw("Can't find final date delimiter ('#')."); - } + // check that we got the end of the date + if (c != '#') { + Throw("Can't find final date delimiter ('#')."); + } - // end of date - var lit = _expr.Substring(_ptr + 1, i - 1); - _ptr += i + 1; + // end of date + var lit = _expr.Substring(_ptr + 1, i - 1); + _ptr += i + 1; _token = new Token(DateTime.Parse(lit, _ci), TKID.ATOM, TKTYPE.LITERAL); - return; - } + return; + } // identifiers (functions, objects) must start with alpha or underscore - if (!isLetter && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) - { + 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++) - { + 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)) - { + if (!isLetter && !isDigit && c != '_' && (_idChars == null || _idChars.IndexOf(c) < 0)) { break; } } @@ -682,67 +603,59 @@ 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]) - { + } + 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 (, - // restore state and bail if it's not - var pos = _ptr; - var tk = _token; - GetToken(); - if (_token.ID != TKID.OPEN) - { + { + // check whether next token is a (, + // restore state and bail if it's not + var pos = _ptr; + var tk = _token; + GetToken(); + 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; } - _ptr = pos; - // get Parameters until we reach the end of the list + // check for empty Parameter list + pos = _ptr; + GetToken(); + if (_token.ID == TKID.CLOSE) { + return null; + } + _ptr = pos; + + // get Parameters until we reach the end of the list var parms = new List(); - var expr = ParseExpression(); - parms.Add(expr); - while (_token.ID == TKID.COMMA) - { - expr = ParseExpression(); - parms.Add(expr); - } + var expr = ParseExpression(); + parms.Add(expr); + while (_token.ID == TKID.COMMA) { + expr = ParseExpression(); + parms.Add(expr); + } - // make sure the list was closed correctly - if (_token.ID != TKID.CLOSE) - { + // make sure the list was closed correctly + if (_token.ID != TKID.CLOSE) { Throw(); - } + } - // done - return parms; - } - Token GetMember() - { + // 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) - { + if (_token.ID != TKID.PERIOD) { _ptr = pos; _token = tk; return null; @@ -750,29 +663,26 @@ // skip member token GetToken(); - if (_token.Type != TKTYPE.IDENTIFIER) - { + if (_token.Type != TKTYPE.IDENTIFIER) { Throw("Identifier expected"); } return _token; } - #endregion + #endregion - //--------------------------------------------------------------------------- - #region ** static helpers + //--------------------------------------------------------------------------- + #region ** static helpers - static void Throw() - { + static void Throw() { Throw("Syntax error."); } - static void Throw(string msg) - { + static void Throw(string msg) { throw new Exception(msg); } #endregion - } + } /// /// Delegate that represents CalcEngine functions. diff --git a/ClosedXML/Excel/CalcEngine/Expression.cs b/ClosedXML/Excel/CalcEngine/Expression.cs index 3e2c9bc..831db51 100644 --- a/ClosedXML/Excel/CalcEngine/Expression.cs +++ b/ClosedXML/Excel/CalcEngine/Expression.cs @@ -97,7 +97,7 @@ } // handle nulls - if (v == null || v is String) + if (v == null || v is string) { return 0; } diff --git a/ClosedXML/Excel/CalcEngine/Functions/Information.cs b/ClosedXML/Excel/CalcEngine/Functions/Information.cs new file mode 100644 index 0000000..2af09f9 --- /dev/null +++ b/ClosedXML/Excel/CalcEngine/Functions/Information.cs @@ -0,0 +1,193 @@ +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class Information + { + public static void Register(CalcEngine ce) + { + //TODO: Add documentation + ce.RegisterFunction("ERRORTYPE",1,ErrorType); + ce.RegisterFunction("ISBLANK", 1,int.MaxValue, IsBlank); + ce.RegisterFunction("ISERR",1, int.MaxValue, IsErr); + ce.RegisterFunction("ISERROR",1, int.MaxValue, IsError); + ce.RegisterFunction("ISEVEN",1, IsEven); + ce.RegisterFunction("ISLOGICAL",1,int.MaxValue,IsLogical); + ce.RegisterFunction("ISNA",1, int.MaxValue, IsNa); + ce.RegisterFunction("ISNONTEXT",1, int.MaxValue, IsNonText); + ce.RegisterFunction("ISNUMBER",1, int.MaxValue, IsNumber); + ce.RegisterFunction("ISODD",1,IsOdd); + ce.RegisterFunction("ISREF",1, int.MaxValue, IsRef); + ce.RegisterFunction("ISTEXT", 1, int.MaxValue, IsText); + ce.RegisterFunction("N",1,N); + ce.RegisterFunction("NA",0,NA); + ce.RegisterFunction("TYPE",1,Type); + } + + static object ErrorType(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsBlank(List p) + { + var v = (string) p[0]; + var isBlank = string.IsNullOrEmpty(v); + + + if (isBlank && p.Count > 1) { + var sublist = p.GetRange(1, p.Count); + isBlank = (bool)IsBlank(sublist); + } + + return isBlank; + } + + //TODO: Support for Error Values + static object IsErr(List p) + { + //TODO: Write Code + throw new NotSupportedException(); + } + + static object IsError(List p) + { + //TODO: Write Code + throw new NotSupportedException(); + } + + static object IsEven(List p) + { + var v = p[0].Evaluate(); + if (v is double) + { + return Math.Abs((double) v%2) < 1; + } + //TODO: Error Exceptions + throw new ArgumentException("Expression doesn't evaluate to double"); + } + + static object IsLogical(List p) + { + var v = p[0].Evaluate(); + var isLogical = v is bool; + + if (isLogical && p.Count > 1) + { + var sublist = p.GetRange(1, p.Count); + isLogical = (bool) IsLogical(sublist); + } + + return isLogical; + } + + static object IsNa(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsNonText(List p) + { + return !(bool) IsText(p); + } + + static object IsNumber(List p) + { + var v = p[0].Evaluate(); + + var isNumber = v is double; //Normal number formatting + if (!isNumber) + { + isNumber = v is DateTime; //Handle DateTime Format + } + if (!isNumber) + { + //Handle Number Styles + try + { + var stringValue = (string) v; + double.Parse(stringValue.TrimEnd('%', ' '), NumberStyles.Any); + isNumber = true; + } + catch (Exception) + { + isNumber = false; + } + } + + if (isNumber && p.Count > 1) + { + var sublist = p.GetRange(1, p.Count); + isNumber = (bool)IsNumber(sublist); + } + + return isNumber; + } + + static object IsOdd(List p) + { + return !(bool) IsEven(p); + } + + static object IsRef(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsText(List p) + { + //Evaluate Expressions + var isText = !(bool) IsBlank(p); + if (isText) + { + isText = !(bool) IsNumber(p); + } + if (isText) + { + isText = !(bool) IsLogical(p); + } + return isText; + } + + static object N(List p) + { + return (double) p[0]; + } + + static object NA(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object Type(List p) + { + if ((bool) IsNumber(p)) + { + return 1; + } + if ((bool) IsText(p)) + { + return 2; + } + if ((bool) IsLogical(p)) + { + return 4; + } + if ((bool) IsError(p)) + { + return 16; + } + if(p.Count > 1) + { + return 64; + } + return null; + } + } +} \ No newline at end of file diff --git a/ClosedXML/Excel/CalcEngine/Functions/Is.cs b/ClosedXML/Excel/CalcEngine/Functions/Is.cs deleted file mode 100644 index 544f92e..0000000 --- a/ClosedXML/Excel/CalcEngine/Functions/Is.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Diagnostics; -using System.Collections.Generic; -using System.Text; - -namespace ClosedXML.Excel.CalcEngine -{ - internal static class Is - { - public static void Register(CalcEngine ce) - { - ce.RegisterFunction("ISBLANK", 1, IsBlank); - } - - static object IsBlank(List p) - { - var v = (string)p[0]; - return String.IsNullOrEmpty(v); - } - } -} diff --git a/ClosedXML/Excel/CalcEngine/Functions/Logical.cs b/ClosedXML/Excel/CalcEngine/Functions/Logical.cs index 1b6695e..18d2a9d 100644 --- a/ClosedXML/Excel/CalcEngine/Functions/Logical.cs +++ b/ClosedXML/Excel/CalcEngine/Functions/Logical.cs @@ -1,7 +1,5 @@ using System; -using System.Diagnostics; using System.Collections.Generic; -using System.Text; namespace ClosedXML.Excel.CalcEngine { @@ -15,6 +13,7 @@ ce.RegisterFunction("IF", 2, 3, If); ce.RegisterFunction("TRUE", 0, True); ce.RegisterFunction("FALSE", 0, False); + ce.RegisterFunction("IFERROR",2,IfError); } static object And(List p) @@ -22,41 +21,55 @@ var b = true; foreach (var v in p) { - b = b && (bool)v; + b = b && v; } return b; } + static object Or(List p) { var b = false; foreach (var v in p) { - b = b || (bool)v; + b = b || v; } return b; } + static object Not(List p) { - return !(bool)p[0]; + return !p[0]; } + static object If(List p) { - if ((bool)p[0] ) + if (p[0]) { return p[1].Evaluate(); } - else - { - return p.Count > 2 ? p[2].Evaluate() : false; - } + return p.Count > 2 ? p[2].Evaluate() : false; } + static object True(List p) { return true; } + static object False(List p) { return false; } + + static object IfError(List p) + { + try + { + return p[0].Evaluate(); + } + catch (ArgumentException) + { + return p[1].Evaluate(); + } + } } -} +} \ No newline at end of file diff --git a/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj b/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj index 56c7353..7cd14ba 100644 --- a/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj +++ b/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj @@ -112,8 +112,8 @@ Excel\CalcEngine\Functions\DateAndTime.cs - - Excel\CalcEngine\Functions\Is.cs + + Excel\CalcEngine\Functions\Information.cs Excel\CalcEngine\Functions\Logical.cs diff --git a/ClosedXML_Net3.5/Excel/CalcEngine/Functions/Information.cs b/ClosedXML_Net3.5/Excel/CalcEngine/Functions/Information.cs new file mode 100644 index 0000000..dcb1340 --- /dev/null +++ b/ClosedXML_Net3.5/Excel/CalcEngine/Functions/Information.cs @@ -0,0 +1,152 @@ +using System; +using System.Collections.Generic; +using System.Drawing.Design; + +namespace ClosedXML.Excel.CalcEngine.Functions +{ + internal static class Information + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("ERRORTYPE",1,ErrorType); + ce.RegisterFunction("ISBLANK", 1,int.MaxValue, IsBlank); + ce.RegisterFunction("ISERR",1, int.MaxValue, IsErr); + ce.RegisterFunction("ISERROR",1, int.MaxValue, IsError); + ce.RegisterFunction("ISEVEN",1, IsEven); + ce.RegisterFunction("ISLOGICAL",1,int.MaxValue,IsLogical); + ce.RegisterFunction("ISNA",1, int.MaxValue, IsNa); + ce.RegisterFunction("ISNONTEXT",1, int.MaxValue, IsNonText); + ce.RegisterFunction("ISNUMBER",1, int.MaxValue, IsNumber); + ce.RegisterFunction("ISODD",1,IsOdd); + ce.RegisterFunction("ISREF",1, int.MaxValue, IsRef); + ce.RegisterFunction("ISTEXT",1, int.MaxValue, IsText); + ce.RegisterFunction("N",1,N); + ce.RegisterFunction("NA",0,NA); + ce.RegisterFunction("TYPE",1,Type); + } + + static object ErrorType(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsBlank(List p) + { + var v = (string) p[0].Evaluate(); + var isBlank = string.IsNullOrEmpty(v); + p.RemoveAt(0); + + if (isBlank && p.Count > 0) { + isBlank = (bool)IsBlank(p); + } + + return isBlank; + } + + //TODO: Support for Error Values + static object IsErr(List p) + { + //TODO: Write Code + throw new NotSupportedException(); + } + + static object IsError(List p) + { + //TODO: Write Code + throw new NotSupportedException(); + } + + static object IsEven(List p) + { + var v = p[0].Evaluate(); + if (v is double) + { + return Math.Abs((double) v%2) < 0; + } + throw new ArgumentException("Expression doesn't evaluate to double"); + } + + static object IsLogical(List p) + { + var v = p[0].Evaluate(); + var isLogical = v is bool; + p.RemoveAt(0); + + if (isLogical && p.Count > 0) + { + isLogical = (bool) IsLogical(p); + } + + return isLogical; + } + + static object IsNa(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsNonText(List p) + { + return !(bool) IsText(p); + } + + static object IsNumber(List p) + { + var v = p[0].Evaluate(); + var isNumber = v is double; + p.RemoveAt(0); + + if (isNumber && p.Count > 0) { + isNumber = (bool)IsNumber(p); + } + + return isNumber; + } + + static object IsOdd(List p) + { + return !(bool) IsEven(p); + } + + static object IsRef(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object IsText(List p) + { + //Evaluate Expressions + var isText = !(bool) IsBlank(p); + if (isText) + { + isText = !(bool) IsNumber(p); + } + if (isText) + { + isText = !(bool) IsLogical(p); + } + return isText; + } + + static object N(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object NA(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + + static object Type(List p) + { + //TODO: Write Code + throw new NotSupportedException();; + } + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/ClosedXML_Tests.csproj b/ClosedXML_Tests/ClosedXML_Tests.csproj index aba9560..7ebc711 100644 --- a/ClosedXML_Tests/ClosedXML_Tests.csproj +++ b/ClosedXML_Tests/ClosedXML_Tests.csproj @@ -70,7 +70,7 @@ - + diff --git a/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs b/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs new file mode 100644 index 0000000..2bb46a3 --- /dev/null +++ b/ClosedXML_Tests/Excel/CalcEngine/InformationTests.cs @@ -0,0 +1,331 @@ +using System; +using System.Runtime.InteropServices; +using ClosedXML.Excel; +using NUnit.Framework; + +namespace ClosedXML_Tests.Excel.CalcEngine +{ + + [TestFixture] + public class InformationTests + { + + #region IsBlank Tests + [Test] + public void IsBlank_Single_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + var actual = ws.Evaluate("=IsBlank(A1)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsBlank_Single_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = " "; + var actual = ws.Evaluate("=IsBlank(A1)"); + Assert.AreEqual(false, actual); + } + + [Test] + public void IsBlank_MultipleAllEmpty_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(true,actual); + + } + + [Test] + public void IsBlank_MultipleAllFill_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "1"; + ws.Cell("A2").Value = "1"; + ws.Cell("A3").Value = "1"; + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(false,actual); + } + + [Test] + public void IsBlank_MultipleMixedFill_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "1"; + ws.Cell("A3").Value = "1"; + var actual = ws.Evaluate("=IsBlank(A1:A3)"); + Assert.AreEqual(false, actual); + } + #endregion + + #region IsEven Tests + + [Test] + public void IsEven_Single_False() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 1.2; + ws.Cell("A3").Value = 3; + + var actual = ws.Evaluate("=IsEven(A1)"); + Assert.AreEqual(false,actual); + + actual = ws.Evaluate("=IsEven(A2)"); + Assert.AreEqual(false, actual); + + actual = ws.Evaluate("=IsEven(A3)"); + Assert.AreEqual(false, actual); + } + + [Test] + public void IsEven_Single_True() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 4; + ws.Cell("A2").Value = 0.2; + ws.Cell("A3").Value = 12.2; + + var actual = ws.Evaluate("=IsEven(A1)"); + Assert.AreEqual(true, actual); + + actual = ws.Evaluate("=IsEven(A2)"); + Assert.AreEqual(true, actual); + + actual = ws.Evaluate("=IsEven(A3)"); + Assert.AreEqual(true, actual); + } + #endregion + + #region IsLogical Tests + + [Test] + public void IsLogical_Simple_True() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = true; + + var actual = ws.Evaluate("=IsLogical(A1)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsLogical_Simpe_False() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 123; + + var actual = ws.Evaluate("=IsLogical(A1)"); + Assert.AreEqual(false, actual); + } + #endregion + + #region IsNotText Tests + + [Test] + public void IsNotText_Simple_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Coma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = true; //Bool Value + ws.Cell("A6").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsNonText(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A3)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A4)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A5)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNonText(A6)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsNotText_Simple_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; + var actual = ws.Evaluate("=IsNonText(A1)"); + Assert.AreEqual(false, actual); + } + + #endregion + + #region IsNumber Tests + + [Test] + public void IsNumber_Simple_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Coma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsNumber(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A3)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A4)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsNumber(A5)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsNumber_Simple_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; //String Value + ws.Cell("A2").Value = true; //Bool Value + + var actual = ws.Evaluate("=IsNumber(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsNumber(A2)"); + Assert.AreEqual(false, actual); + } + #endregion + + #region IsOdd Test + + [Test] + public void IsOdd_Simple_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 1; + ws.Cell("A2").Value = 1.2; + ws.Cell("A3").Value = 3; + + var actual = ws.Evaluate("=IsOdd(A1)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsOdd(A2)"); + Assert.AreEqual(true, actual); + actual = ws.Evaluate("=IsOdd(A3)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsOdd_Simple_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = 4; + ws.Cell("A2").Value = 0.2; + ws.Cell("A3").Value = 12.2; + + var actual = ws.Evaluate("=IsOdd(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsOdd(A2)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsOdd(A3)"); + Assert.AreEqual(false, actual); + } + #endregion + + #region IsText Tests + [Test] + public void IsText_Simple_true() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + + ws.Cell("A1").Value = "asd"; + + var actual = ws.Evaluate("=IsText(A1)"); + Assert.AreEqual(true, actual); + } + + [Test] + public void IsText_Simple_false() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "123"; //Double Value + ws.Cell("A2").Value = DateTime.Now; //Date Value + ws.Cell("A3").Value = "12,235.5"; //Coma Formatting + ws.Cell("A4").Value = "$12,235.5"; //Currency Value + ws.Cell("A5").Value = true; //Bool Value + ws.Cell("A6").Value = "12%"; //Percentage Value + + var actual = ws.Evaluate("=IsText(A1)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A2)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A3)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A4)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A5)"); + Assert.AreEqual(false, actual); + actual = ws.Evaluate("=IsText(A6)"); + Assert.AreEqual(false, actual); + } + #endregion + + #region N Tests + + [Test] + public void N_Date_SerialNumber() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + var testedDate = DateTime.Now; + ws.Cell("A1").Value = testedDate; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(testedDate.ToOADate(),actual); + } + + [Test] + public void N_Number_Number() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + var testedValue = 123; + ws.Cell("A1").Value = testedValue; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(testedValue, actual); + } + + [Test] + public void N_String_Zero() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = "asd"; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(0, actual); + } + + [Test] + public void N_True_One() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = true; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(1, actual); + } + + [Test] + public void N_False_Zero() + { + var ws = new XLWorkbook().AddWorksheet("Sheet"); + ws.Cell("A1").Value = false; + var actual = ws.Evaluate("=N(A1)"); + Assert.AreEqual(0, actual); + } + #endregion + + + } +} \ No newline at end of file diff --git a/ClosedXML_Tests/Excel/CalcEngine/IsTests.cs b/ClosedXML_Tests/Excel/CalcEngine/IsTests.cs deleted file mode 100644 index 2d98c68..0000000 --- a/ClosedXML_Tests/Excel/CalcEngine/IsTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using ClosedXML.Excel; -using NUnit.Framework; - -namespace ClosedXML_Tests.Excel.CalcEngine -{ - - [TestFixture] - public class IsTests - { - [Test] - public void IsBlank_true() - { - var ws = new XLWorkbook().AddWorksheet("Sheet"); - var actual = ws.Evaluate("=IsBlank(A1)"); - Assert.AreEqual(true, actual); - } - - [Test] - public void IsBlank_false() - { - var ws = new XLWorkbook().AddWorksheet("Sheet"); - ws.Cell("A1").Value = " "; - var actual = ws.Evaluate("=IsBlank(A1)"); - Assert.AreEqual(false, actual); - } - } -} \ No newline at end of file