diff --git a/ClosedXML/ClosedXML/ClosedXML/ClosedXML.csproj b/ClosedXML/ClosedXML/ClosedXML/ClosedXML.csproj index ad72f79..6a45326 100644 --- a/ClosedXML/ClosedXML/ClosedXML/ClosedXML.csproj +++ b/ClosedXML/ClosedXML/ClosedXML/ClosedXML.csproj @@ -140,6 +140,17 @@ + + + + + + + + + + + diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/CalcEngine.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/CalcEngine.cs new file mode 100644 index 0000000..22c24db --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/CalcEngine.cs @@ -0,0 +1,780 @@ +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; + +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. + /// + public partial 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 + 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 + } + + #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++; + } + + // parse the expression + var expr = ParseExpression(); + + // check for errors + if (_token.ID != TKID.END) + { + Throw(); + } + + // 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] + : 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]; + } + } + + #endregion + + //--------------------------------------------------------------------------- + #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); + 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) + Logical.Register(this); + MathTrig.Register(this); + Text.Register(this); + Statistical.Register(this); + } + return _fnTbl; + } + + #endregion + + //--------------------------------------------------------------------------- + #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 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() + { + 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; + + // sub-expressions + case TKTYPE.GROUP: + + // anything other than opening parenthesis is illegal here + if (_token.ID != TKID.OPEN) + { + Throw("Expression expected."); + } + + // get expression + GetToken(); + x = ParseCompare(); + + // check that the parenthesis was closed + if (_token.ID != TKID.CLOSE) + { + Throw("Unbalanced parenthesis."); + } + + break; + } + + // make sure we got something... + if (x == null) + { + Throw(); + } + + // done + GetToken(); + return x; + } + + #endregion + + //--------------------------------------------------------------------------- + #region ** parser + + void GetToken() + { + // eat white space + while (_ptr < _len && _expr[_ptr] <= ' ') + { + _ptr++; + } + + // are we done? + if (_ptr >= _len) + { + _token = new Token(null, TKID.END, TKTYPE.GROUP); + return; + } + + // prepare to parse + int i; + 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. + 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; + 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; + } + + // look up single-char tokens on table + 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 (_tkTbl.TryGetValue(_expr.Substring(_ptr - 1, 2), out tk)) + { + _token = tk; + _ptr++; + } + } + + // found token on the table + return; + } + } + } + + // 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]; + + // digits always OK + if (c >= '0' && c <= '9') + { + val = val * 10 + (c - '0'); + 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; + } + + // 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; + 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++; + } + + // 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; + _token = new Token(lit.Replace("\"\"", "\""), TKID.ATOM, TKTYPE.LITERAL); + return; + } + + // 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 ('#')."); + } + + // 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; + } + + // 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); + } + 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) + { + _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 + var parms = new List(); + 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) + { + 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; + } + + #endregion + + //--------------------------------------------------------------------------- + #region ** static helpers + + 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. + public delegate object CalcEngineFunction(List parms); +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Expression.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Expression.cs new file mode 100644 index 0000000..1b1732e --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Expression.cs @@ -0,0 +1,504 @@ +using System; +using System.Text; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Reflection; +using ClosedXML.Excel.CalcEngine; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Base class that represents parsed expressions. + /// + /// + /// For example: + /// + /// Expression expr = scriptEngine.Parse(strExpression); + /// object val = expr.Evaluate(); + /// + /// + public class Expression : IComparable + { + //--------------------------------------------------------------------------- + #region ** fields + + internal Token _token; + static CultureInfo _ci = CultureInfo.InvariantCulture; + + #endregion + + //--------------------------------------------------------------------------- + #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 + + //--------------------------------------------------------------------------- + #region ** object model + + public virtual object Evaluate() + { + if (_token.Type != TKTYPE.LITERAL) + { + throw new ArgumentException("Bad expression."); + } + return _token.Value; + } + public virtual Expression Optimize() + { + return this; + } + + #endregion + + //--------------------------------------------------------------------------- + #region ** implicit converters + + public static implicit operator string(Expression x) + { + var v = x.Evaluate(); + return v == null ? string.Empty : v.ToString(); + } + public static implicit operator double(Expression x) + { + // evaluate + var v = x.Evaluate(); + + // handle doubles + if (v is double) + { + return (double)v; + } + + // handle booleans + if (v is bool) + { + return (bool)v ? 1 : 0; + } + + // handle dates + if (v is DateTime) + { + return ((DateTime)v).ToOADate(); + } + + // handle nulls + if (v == null) + { + return 0; + } + + // handle everything else + return (double)Convert.ChangeType(v, typeof(double), _ci); + } + public static implicit operator bool(Expression x) + { + // evaluate + var v = x.Evaluate(); + + // handle booleans + if (v is bool) + { + return (bool)v; + } + + // handle nulls + if (v == null) + { + return false; + } + + // handle doubles + if (v is double) + { + return (double)v == 0 ? false : true; + } + + // handle everything else + return (double)x == 0 ? false : true; + } + public static implicit operator DateTime(Expression x) + { + // evaluate + var v = x.Evaluate(); + + // handle dates + if (v is DateTime) + { + return (DateTime)v; + } + + // handle doubles + if (v is double) + { + return DateTime.FromOADate((double)x); + } + + // handle everything else + return (DateTime)Convert.ChangeType(v, typeof(DateTime), _ci); + } + + #endregion + + //--------------------------------------------------------------------------- + #region ** IComparable + + public int CompareTo(Expression other) + { + // get both values + var c1 = this.Evaluate() as IComparable; + var c2 = other.Evaluate() as IComparable; + + // handle nulls + if (c1 == null && c2 == null) + { + return 0; + } + if (c2 == null) + { + return -1; + } + if (c1 == null) + { + return +1; + } + + // make sure types are the same + if (c1.GetType() != c2.GetType()) + { + c2 = Convert.ChangeType(c2, c1.GetType()) as IComparable; + } + + // compare + return c1.CompareTo(c2); + } + + #endregion + } + /// + /// Unary expression, e.g. +123 + /// + class UnaryExpression : Expression + { + // ** fields + Expression _expr; + + // ** ctor + public UnaryExpression(Token tk, Expression expr) : base(tk) + { + _expr = expr; + } + + // ** object model + override public object Evaluate() + { + switch (_token.ID) + { + case TKID.ADD: + return +(double)_expr; + case TKID.SUB: + return -(double)_expr; + } + throw new ArgumentException("Bad expression."); + } + public override Expression Optimize() + { + _expr = _expr.Optimize(); + return _expr._token.Type == TKTYPE.LITERAL + ? new Expression(this.Evaluate()) + : this; + } + } + /// + /// Binary expression, e.g. 1+2 + /// + class BinaryExpression : Expression + { + // ** fields + Expression _lft; + Expression _rgt; + + // ** ctor + public BinaryExpression(Token tk, Expression exprLeft, Expression exprRight) : base(tk) + { + _lft = exprLeft; + _rgt = exprRight; + } + + // ** object model + override public object Evaluate() + { + // handle comparisons + if (_token.Type == TKTYPE.COMPARE) + { + var cmp = _lft.CompareTo(_rgt); + switch (_token.ID) + { + case TKID.GT: return cmp > 0; + case TKID.LT: return cmp < 0; + case TKID.GE: return cmp >= 0; + case TKID.LE: return cmp <= 0; + case TKID.EQ: return cmp == 0; + case TKID.NE: return cmp != 0; + } + } + + // handle everything else + switch (_token.ID) + { + 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; + if (b == 0.0) return 1.0; + if (b == 0.5) return Math.Sqrt(a); + if (b == 1.0) return a; + if (b == 2.0) return a * a; + if (b == 3.0) return a * a * a; + if (b == 4.0) return a * a * a * a; + return Math.Pow((double)_lft, (double)_rgt); + } + throw new ArgumentException("Bad expression."); + } + public override Expression Optimize() + { + _lft = _lft.Optimize(); + _rgt = _rgt.Optimize(); + return _lft._token.Type == TKTYPE.LITERAL && _rgt._token.Type == TKTYPE.LITERAL + ? new Expression(this.Evaluate()) + : this; + } + } + /// + /// Function call expression, e.g. sin(0.5) + /// + class FunctionExpression : Expression + { + // ** fields + FunctionDefinition _fn; + List _parms; + + // ** ctor + internal FunctionExpression() + { + } + public FunctionExpression(FunctionDefinition function, List parms) + { + _fn = function; + _parms = parms; + } + + // ** object model + override public object Evaluate() + { + return _fn.Function(_parms); + } + public override Expression Optimize() + { + bool allLits = true; + if (_parms != null) + { + for (int i = 0; i < _parms.Count; i++) + { + var p = _parms[i].Optimize(); + _parms[i] = p; + if (p._token.Type != TKTYPE.LITERAL) + { + allLits = false; + } + } + } + return allLits + ? new Expression(this.Evaluate()) + : this; + } + } + /// + /// Simple variable reference. + /// + class VariableExpression : Expression + { + Dictionary _dct; + string _name; + + public VariableExpression(Dictionary dct, string name) + { + _dct = dct; + _name = name; + } + public override object Evaluate() + { + return _dct[_name]; + } + } + /// + /// Expression based on an object's properties. + /// + class BindingExpression : Expression + { + CalcEngine _ce; + CultureInfo _ci; + List _bindingPath; + + // ** ctor + internal BindingExpression(CalcEngine engine, List bindingPath, CultureInfo ci) + { + _ce = engine; + _bindingPath = bindingPath; + _ci = ci; + } + + // ** object model + override public object Evaluate() + { + return GetValue(_ce.DataContext); + } + + // ** implementation + object GetValue(object obj) + { + const BindingFlags bf = + BindingFlags.IgnoreCase | + BindingFlags.Instance | + BindingFlags.Public | + BindingFlags.Static; + + if (obj != null) + { + foreach (var bi in _bindingPath) + { + // get property + if (bi.PropertyInfo == null) + { + bi.PropertyInfo = obj.GetType().GetProperty(bi.Name, bf); + } + + // get object + try + { + obj = bi.PropertyInfo.GetValue(obj, null); + } + catch + { + // REVIEW: is this needed? + System.Diagnostics.Debug.Assert(false, "shouldn't happen!"); + bi.PropertyInfo = obj.GetType().GetProperty(bi.Name, bf); + bi.PropertyInfoItem = null; + obj = bi.PropertyInfo.GetValue(obj, null); + } + + // handle indexers (lists and dictionaries) + if (bi.Parms != null && bi.Parms.Count > 0) + { + // get indexer property (always called "Item") + if (bi.PropertyInfoItem == null) + { + bi.PropertyInfoItem = obj.GetType().GetProperty("Item", bf); + } + + // get indexer parameters + var pip = bi.PropertyInfoItem.GetIndexParameters(); + var list = new List(); + for (int i = 0; i < pip.Length; i++) + { + var pv = bi.Parms[i].Evaluate(); + pv = Convert.ChangeType(pv, pip[i].ParameterType, _ci); + list.Add(pv); + } + + // get value + obj = bi.PropertyInfoItem.GetValue(obj, list.ToArray()); + } + } + } + + // all done + return obj; + } + } + /// + /// Helper used for building BindingExpression objects. + /// + class BindingInfo + { + public BindingInfo(string member, List parms) + { + Name = member; + Parms = parms; + } + public string Name { get; set; } + public PropertyInfo PropertyInfo { get; set; } + public PropertyInfo PropertyInfoItem { get; set; } + public List Parms { get; set; } + } + /// + /// Expression that represents an external object. + /// + class XObjectExpression : + Expression, + IEnumerable + { + object _value; + + // ** ctor + internal XObjectExpression(object value) + { + _value = value; + } + + // ** object model + public override object Evaluate() + { + // use IValueObject if available + var iv = _value as IValueObject; + if (iv != null) + { + return iv.GetValue(); + } + + // return raw object + return _value; + } + public IEnumerator GetEnumerator() + { + var ie = _value as IEnumerable; + return ie != null ? ie.GetEnumerator() : null; + } + } + /// + /// Interface supported by external objects that have to return a value + /// other than themselves (e.g. a cell range object should return the + /// cell content instead of the range itself). + /// + public interface IValueObject + { + object GetValue(); + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/ExpressionCache.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/ExpressionCache.cs new file mode 100644 index 0000000..3c7fd00 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/ExpressionCache.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Caches expressions based on their string representation. + /// This saves parsing time. + /// + /// + /// Uses weak references to avoid accumulating unused expressions. + /// + class ExpressionCache + { + Dictionary _dct; + CalcEngine _ce; + int _hitCount; + + public ExpressionCache(CalcEngine ce) + { + _ce = ce; + _dct = new Dictionary(); + } + + // gets the parsed version of a string expression + public Expression this[string expression] + { + get + { + Expression x; + WeakReference wr; + if (_dct.TryGetValue(expression, out wr) && wr.IsAlive) + { + x = wr.Target as Expression; + } + else + { + // remove all dead references from dictionary + if (wr != null && _dct.Count > 100 && _hitCount++ > 100) + { + RemoveDeadReferences(); + _hitCount = 0; + } + + // store this expression + x = _ce.Parse(expression); + _dct[expression] = new WeakReference(x); + } + return x; + } + } + + // remove all dead references from the cache + void RemoveDeadReferences() + { + for (bool done = false; !done; ) + { + done = true; + foreach (var k in _dct.Keys) + { + if (!_dct[k].IsAlive) + { + _dct.Remove(k); + done = false; + break; + } + } + } + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs new file mode 100644 index 0000000..6438ce8 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/FunctionDefinition.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Function definition class (keeps function name, parameter counts, and delegate). + /// + public class FunctionDefinition + { + // ** fields + public int ParmMin, ParmMax; + public CalcEngineFunction Function; + + // ** ctor + public FunctionDefinition(int parmMin, int parmMax, CalcEngineFunction function) + { + ParmMin = parmMin; + ParmMax = parmMax; + Function = function; + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Logical.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Logical.cs new file mode 100644 index 0000000..c2cd087 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Logical.cs @@ -0,0 +1,57 @@ +using System; +using System.Diagnostics; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + static class Logical + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("AND", 1, int.MaxValue, And); + ce.RegisterFunction("OR", 1, int.MaxValue, Or); + ce.RegisterFunction("NOT", 1, Not); + ce.RegisterFunction("IF", 3, If); + ce.RegisterFunction("TRUE", 0, True); + ce.RegisterFunction("FALSE", 0, False); + } + + static object And(List p) + { + var b = true; + foreach (var v in p) + { + b = b && (bool)v; + } + return b; + } + static object Or(List p) + { + var b = false; + foreach (var v in p) + { + b = b || (bool)v; + } + return b; + } + static object Not(List p) + { + return !(bool)p[0]; + } + static object If(List p) + { + return (bool)p[0] + ? p[1].Evaluate() + : p[2].Evaluate(); + } + static object True(List p) + { + return true; + } + static object False(List p) + { + return false; + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs new file mode 100644 index 0000000..0e8b804 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/MathTrig.cs @@ -0,0 +1,280 @@ +using System; +using System.Diagnostics; +using System.Collections; +using System.Collections.Generic; +using System.Text; +using System.Text.RegularExpressions; + +namespace ClosedXML.Excel.CalcEngine +{ + static class MathTrig + { + public static void Register(CalcEngine ce) + { + ce.RegisterFunction("ABS", 1, Abs); + ce.RegisterFunction("ACOS", 1, Acos); + //ce.RegisterFunction("ACOSH", Acosh, 1); + ce.RegisterFunction("ASIN", 1, Asin); + //ce.RegisterFunction("ASINH", Asinh, 1); + ce.RegisterFunction("ATAN", 1, Atan); + ce.RegisterFunction("ATAN2", 2, Atan2); + //ce.RegisterFunction("ATANH", Atanh, 1); + ce.RegisterFunction("CEILING", 1, Ceiling); + //ce.RegisterFunction("COMBIN", Combin, 1); + ce.RegisterFunction("COS", 1, Cos); + ce.RegisterFunction("COSH", 1, Cosh); + //ce.RegisterFunction("DEGREES", Degrees, 1); + //ce.RegisterFunction("EVEN", Even, 1); + ce.RegisterFunction("EXP", 1, Exp); + //ce.RegisterFunction("FACT", Fact, 1); + //ce.RegisterFunction("FACTDOUBLE", FactDouble, 1); + ce.RegisterFunction("FLOOR", 1, Floor); + //ce.RegisterFunction("GCD", Gcd, 1); + ce.RegisterFunction("INT", 1, Int); + //ce.RegisterFunction("LCM", Lcm, 1); + ce.RegisterFunction("LN", 1, Ln); + ce.RegisterFunction("LOG", 1, 2, Log); + ce.RegisterFunction("LOG10", 1, Log10); + //ce.RegisterFunction("MDETERM", MDeterm, 1); + //ce.RegisterFunction("MINVERSE", MInverse, 1); + //ce.RegisterFunction("MMULT", MMult, 1); + //ce.RegisterFunction("MOD", Mod, 2); + //ce.RegisterFunction("MROUND", MRound, 1); + //ce.RegisterFunction("MULTINOMIAL", Multinomial, 1); + //ce.RegisterFunction("ODD", Odd, 1); + ce.RegisterFunction("PI", 0, Pi); + ce.RegisterFunction("POWER", 2, Power); + //ce.RegisterFunction("PRODUCT", Product, 1); + //ce.RegisterFunction("QUOTIENT", Quotient, 1); + //ce.RegisterFunction("RADIANS", Radians, 1); + ce.RegisterFunction("RAND", 0, Rand); + ce.RegisterFunction("RANDBETWEEN", 2, RandBetween); + //ce.RegisterFunction("ROMAN", Roman, 1); + //ce.RegisterFunction("ROUND", Round, 1); + //ce.RegisterFunction("ROUNDDOWN", RoundDown, 1); + //ce.RegisterFunction("ROUNDUP", RoundUp, 1); + //ce.RegisterFunction("SERIESSUM", SeriesSum, 1); + ce.RegisterFunction("SIGN", 1, Sign); + ce.RegisterFunction("SIN", 1, Sin); + ce.RegisterFunction("SINH", 1, Sinh); + ce.RegisterFunction("SQRT", 1, Sqrt); + //ce.RegisterFunction("SQRTPI", SqrtPi, 1); + //ce.RegisterFunction("SUBTOTAL", Subtotal, 1); + ce.RegisterFunction("SUM", 1, int.MaxValue, Sum); + ce.RegisterFunction("SUMIF", 2, 3, SumIf); + //ce.RegisterFunction("SUMPRODUCT", SumProduct, 1); + //ce.RegisterFunction("SUMSQ", SumSq, 1); + //ce.RegisterFunction("SUMX2MY2", SumX2MY2, 1); + //ce.RegisterFunction("SUMX2PY2", SumX2PY2, 1); + //ce.RegisterFunction("SUMXMY2", SumXMY2, 1); + ce.RegisterFunction("TAN", 1, Tan); + ce.RegisterFunction("TANH", 1, Tanh); + ce.RegisterFunction("TRUNC", 1, Trunc); + } + static object Abs(List p) + { + return Math.Abs((double)p[0]); + } + static object Acos(List p) + { + return Math.Acos((double)p[0]); + } + static object Asin(List p) + { + return Math.Asin((double)p[0]); + } + static object Atan(List p) + { + return Math.Atan((double)p[0]); + } + static object Atan2(List p) + { + return Math.Atan2((double)p[0], (double)p[1]); + } + static object Ceiling(List p) + { + return Math.Ceiling((double)p[0]); + } + static object Cos(List p) + { + return Math.Cos((double)p[0]); + } + static object Cosh(List p) + { + return Math.Cosh((double)p[0]); + } + static object Exp(List p) + { + return Math.Exp((double)p[0]); + } + static object Floor(List p) + { + return Math.Floor((double)p[0]); + } + static object Int(List p) + { + return (int)((double)p[0]); + } + static object Ln(List p) + { + return Math.Log((double)p[0]); + } + static object Log(List p) + { + var lbase = p.Count > 1 ? (double)p[1] : 10; + return Math.Log((double)p[0], lbase); + } + static object Log10(List p) + { + return Math.Log10((double)p[0]); + } + static object Pi(List p) + { + return Math.PI; + } + static object Power(List p) + { + return Math.Pow((double)p[0], (double)p[1]); + } + static Random _rnd = new Random(); + static object Rand(List p) + { + return _rnd.NextDouble(); + } + static object RandBetween(List p) + { + return _rnd.Next((int)(double)p[0], (int)(double)p[1]); + } + static object Sign(List p) + { + return Math.Sign((double)p[0]); + } + static object Sin(List p) + { + return Math.Sin((double)p[0]); + } + static object Sinh(List p) + { + return Math.Sinh((double)p[0]); + } + static object Sqrt(List p) + { + return Math.Sqrt((double)p[0]); + } + static object Sum(List p) + { + var tally = new Tally(); + foreach (Expression e in p) + { + tally.Add(e); + } + return tally.Sum(); + } + static object SumIf(List p) + { + // get parameters + IEnumerable range = p[0] as IEnumerable; + IEnumerable sumRange = p.Count < 3 ? range : p[2] as IEnumerable; + var criteria = p[1].Evaluate(); + + // build list of values in range and sumRange + var rangeValues = new List(); + foreach (var value in range) + { + rangeValues.Add(value); + } + var sumRangeValues = new List(); + foreach (var value in sumRange) + { + sumRangeValues.Add(value); + } + + // compute total + var ce = new CalcEngine(); + var tally = new Tally(); + for (int i = 0; i < Math.Min(rangeValues.Count, sumRangeValues.Count); i++) + { + if (ValueSatisfiesCriteria(rangeValues[i], criteria, ce)) + { + tally.AddValue(sumRangeValues[i]); + } + } + + // done + return tally.Sum(); + } + static bool ValueSatisfiesCriteria(object value, object criteria, CalcEngine ce) + { + // safety... + if (value == null) + { + return false; + } + + // if criteria is a number, straight comparison + if (criteria is double) + { + return (double)value == (double)criteria; + } + + // convert criteria to string + var cs = criteria as string; + if (!string.IsNullOrEmpty(cs)) + { + // if criteria is an expression (e.g. ">20"), use calc engine + if (cs[0] == '=' || cs[0] == '<' || cs[0] == '>') + { + // build expression + var expression = string.Format("{0}{1}", value, cs); + + // add quotes if necessary + var pattern = @"(\w+)(\W+)(\w+)"; + var m = Regex.Match(expression, pattern); + if (m.Groups.Count == 4) + { + double d; + if (!double.TryParse(m.Groups[1].Value, out d) || + !double.TryParse(m.Groups[3].Value, out d)) + { + expression = string.Format("\"{0}\"{1}\"{2}\"", + m.Groups[1].Value, + m.Groups[2].Value, + m.Groups[3].Value); + } + } + + // evaluate + return (bool)ce.Evaluate(expression); + } + + // if criteria is a regular expression, use regex + if (cs.IndexOf('*') > -1) + { + var pattern = cs.Replace(@"\", @"\\"); + pattern = pattern.Replace(".", @"\"); + pattern = pattern.Replace("*", ".*"); + return Regex.IsMatch(value.ToString(), pattern, RegexOptions.IgnoreCase); + } + + // straight string comparison + return string.Equals(value.ToString(), cs, StringComparison.OrdinalIgnoreCase); + } + + // should never get here? + System.Diagnostics.Debug.Assert(false, "failed to evaluate criteria in SumIf"); + return false; + } + static object Tan(List p) + { + return Math.Tan((double)p[0]); + } + static object Tanh(List p) + { + return Math.Tanh((double)p[0]); + } + static object Trunc(List p) + { + return (double)(int)((double)p[0]); + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs new file mode 100644 index 0000000..a6015bb --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Statistical.cs @@ -0,0 +1,220 @@ +using System; +using System.Net; +using System.Collections; +using System.Collections.Generic; + +namespace ClosedXML.Excel.CalcEngine +{ + static class Statistical + { + public static void Register(CalcEngine ce) + { + //ce.RegisterFunction("AVEDEV", AveDev, 1, int.MaxValue); + ce.RegisterFunction("AVERAGE", 1, int.MaxValue, Average); + ce.RegisterFunction("AVERAGEA", 1, int.MaxValue, AverageA); + //BETADIST Returns the beta cumulative distribution function + //BETAINV Returns the inverse of the cumulative distribution function for a specified beta distribution + //BINOMDIST Returns the individual term binomial distribution probability + //CHIDIST Returns the one-tailed probability of the chi-squared distribution + //CHIINV Returns the inverse of the one-tailed probability of the chi-squared distribution + //CHITEST Returns the test for independence + //CONFIDENCE Returns the confidence interval for a population mean + //CORREL Returns the correlation coefficient between two data sets + ce.RegisterFunction("COUNT", 1, int.MaxValue, Count); + ce.RegisterFunction("COUNTA", 1, int.MaxValue, CountA); + ce.RegisterFunction("COUNTBLANK", 1, int.MaxValue, CountBlank); + ce.RegisterFunction("COUNTIF", 2, CountIf); + //COVAR Returns covariance, the average of the products of paired deviations + //CRITBINOM Returns the smallest value for which the cumulative binomial distribution is less than or equal to a criterion value + //DEVSQ Returns the sum of squares of deviations + //EXPONDIST Returns the exponential distribution + //FDIST Returns the F probability distribution + //FINV Returns the inverse of the F probability distribution + //FISHER Returns the Fisher transformation + //FISHERINV Returns the inverse of the Fisher transformation + //FORECAST Returns a value along a linear trend + //FREQUENCY Returns a frequency distribution as a vertical array + //FTEST Returns the result of an F-test + //GAMMADIST Returns the gamma distribution + //GAMMAINV Returns the inverse of the gamma cumulative distribution + //GAMMALN Returns the natural logarithm of the gamma function, Γ(x) + //GEOMEAN Returns the geometric mean + //GROWTH Returns values along an exponential trend + //HARMEAN Returns the harmonic mean + //HYPGEOMDIST Returns the hypergeometric distribution + //INTERCEPT Returns the intercept of the linear regression line + //KURT Returns the kurtosis of a data set + //LARGE Returns the k-th largest value in a data set + //LINEST Returns the parameters of a linear trend + //LOGEST Returns the parameters of an exponential trend + //LOGINV Returns the inverse of the lognormal distribution + //LOGNORMDIST Returns the cumulative lognormal distribution + ce.RegisterFunction("MAX", 1, int.MaxValue, Max); + ce.RegisterFunction("MAXA", 1, int.MaxValue, MaxA); + //MEDIAN Returns the median of the given numbers + ce.RegisterFunction("MIN", 1, int.MaxValue, Min); + ce.RegisterFunction("MINA", 1, int.MaxValue, MinA); + //MODE Returns the most common value in a data set + //NEGBINOMDIST Returns the negative binomial distribution + //NORMDIST Returns the normal cumulative distribution + //NORMINV Returns the inverse of the normal cumulative distribution + //NORMSDIST Returns the standard normal cumulative distribution + //NORMSINV Returns the inverse of the standard normal cumulative distribution + //PEARSON Returns the Pearson product moment correlation coefficient + //PERCENTILE Returns the k-th percentile of values in a range + //PERCENTRANK Returns the percentage rank of a value in a data set + //PERMUT Returns the number of permutations for a given number of objects + //POISSON Returns the Poisson distribution + //PROB Returns the probability that values in a range are between two limits + //QUARTILE Returns the quartile of a data set + //RANK Returns the rank of a number in a list of numbers + //RSQ Returns the square of the Pearson product moment correlation coefficient + //SKEW Returns the skewness of a distribution + //SLOPE Returns the slope of the linear regression line + //SMALL Returns the k-th smallest value in a data set + //STANDARDIZE Returns a normalized value + ce.RegisterFunction("STDEV", 1, int.MaxValue, StDev); + ce.RegisterFunction("STDEVA", 1, int.MaxValue, StDevA); + ce.RegisterFunction("STDEVP", 1, int.MaxValue, StDevP); + ce.RegisterFunction("STDEVPA", 1, int.MaxValue, StDevPA); + //STEYX Returns the standard error of the predicted y-value for each x in the regression + //TDIST Returns the Student's t-distribution + //TINV Returns the inverse of the Student's t-distribution + //TREND Returns values along a linear trend + //TRIMMEAN Returns the mean of the interior of a data set + //TTEST Returns the probability associated with a Student's t-test + ce.RegisterFunction("VAR", 1, int.MaxValue, Var); + ce.RegisterFunction("VARA", 1, int.MaxValue, VarA); + ce.RegisterFunction("VARP", 1, int.MaxValue, VarP); + ce.RegisterFunction("VARPA", 1, int.MaxValue, VarPA); + //WEIBULL Returns the Weibull distribution + //ZTEST Returns the one-tailed probability-value of a z-test + } + + + + static object Average(List p) + { + return GetTally(p, true).Average(); + } + static object AverageA(List p) + { + return GetTally(p, false).Average(); + } + static object Count(List p) + { + return GetTally(p, true).Count(); + } + static object CountA(List p) + { + return GetTally(p, false).Count(); + } + static object CountBlank(List p) + { + var cnt = 0.0; + foreach (Expression e in p) + { + var ienum = e as IEnumerable; + if (ienum != null) + { + foreach (var value in ienum) + { + if (IsBlank(value)) + cnt++; + } + } + else + { + if (IsBlank(e.Evaluate())) + cnt++; + } + } + return cnt; + } + static bool IsBlank(object value) + { + return + value == null || + value is string && ((string)value).Length == 0; + } + static object CountIf(List p) + { + CalcEngine ce = new CalcEngine(); + var cnt = 0.0; + var ienum = p[0] as IEnumerable; + if (ienum != null) + { + var crit = (string)p[1].Evaluate(); + foreach (var value in ienum) + { + if (!IsBlank(value)) + { + var exp = string.Format("{0}{1}", value, crit); + if ((bool)ce.Evaluate(exp)) + cnt++; + } + } + } + return cnt; + } + static object Max(List p) + { + return GetTally(p, true).Max(); + } + static object MaxA(List p) + { + return GetTally(p, false).Max(); + } + static object Min(List p) + { + return GetTally(p, true).Min(); + } + static object MinA(List p) + { + return GetTally(p, false).Min(); + } + static object StDev(List p) + { + return GetTally(p, true).Std(); + } + static object StDevA(List p) + { + return GetTally(p, false).Std(); + } + static object StDevP(List p) + { + return GetTally(p, true).StdP(); + } + static object StDevPA(List p) + { + return GetTally(p, false).StdP(); + } + static object Var(List p) + { + return GetTally(p, true).Var(); + } + static object VarA(List p) + { + return GetTally(p, false).Var(); + } + static object VarP(List p) + { + return GetTally(p, true).VarP(); + } + static object VarPA(List p) + { + return GetTally(p, false).VarP(); + } + + // utility for tallying statistics + static Tally GetTally(List p, bool numbersOnly) + { + var tally = new Tally(numbersOnly); + foreach (Expression e in p) + { + tally.Add(e); + } + return tally; + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Tally.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Tally.cs new file mode 100644 index 0000000..74c3afd --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Tally.cs @@ -0,0 +1,109 @@ +using System; +using System.Net; +using System.Collections; + +namespace ClosedXML.Excel.CalcEngine +{ + class Tally + { + double _sum, _sum2, _cnt, _min, _max; + bool _numbersOnly; + + public Tally(bool numbersOnly) + { + _numbersOnly = numbersOnly; + } + public Tally() + { + } + + public void Add(Expression e) + { + // handle enumerables + var ienum = e as IEnumerable; + if (ienum != null) + { + foreach (var value in ienum) + { + AddValue(value); + } + return; + } + + // handle expressions + AddValue(e.Evaluate()); + } + public void AddValue(object value) + { + // conversions + if (!_numbersOnly) + { + // arguments that contain text evaluate as 0 (zero). + // empty text ("") evaluates as 0 (zero). + if (value == null || value is string) + { + value = 0; + } + // arguments that contain TRUE evaluate as 1; + // arguments that contain FALSE evaluate as 0 (zero). + if (value is bool) + { + value = (bool)value ? 1 : 0; + } + } + + // convert all numeric values to doubles + if (value != null) + { + var typeCode = Type.GetTypeCode(value.GetType()); + if (typeCode >= TypeCode.Char && typeCode <= TypeCode.Decimal) + { + value = Convert.ChangeType(value, typeof(double), System.Globalization.CultureInfo.CurrentCulture); + } + } + + // tally + if (value is double) + { + var dbl = (double)value; + _sum += dbl; + _sum2 += dbl * dbl; + _cnt++; + if (_cnt == 1 || dbl < _min) + { + _min = dbl; + } + if (_cnt == 1 || dbl > _max) + { + _max = dbl; + } + } + } + public double Count() { return _cnt; } + public double Sum() { return _sum; } + public double Average() { return _sum / _cnt; } + public double Min() { return _min; } + public double Max() { return _max; } + public double Range() { return _max - _min; } + public double VarP() + { + var avg = Average(); + return _cnt <= 1 ? 0 : _sum2 / _cnt - avg * avg; + } + public double StdP() + { + var avg = Average(); + return _cnt <= 1 ? 0 : Math.Sqrt(_sum2 / _cnt - avg * avg); + } + public double Var() + { + var avg = Average(); + return _cnt <= 1 ? 0 : (_sum2 / _cnt - avg * avg) * _cnt / (_cnt - 1); + } + public double Std() + { + var avg = Average(); + return _cnt <= 1 ? 0 : Math.Sqrt((_sum2 / _cnt - avg * avg) * _cnt / (_cnt - 1)); + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Text.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Text.cs new file mode 100644 index 0000000..ce80362 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Functions/Text.cs @@ -0,0 +1,192 @@ +using System; +using System.Diagnostics; +using System.Globalization; +using System.Collections.Generic; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + static class Text + { + public static void Register(CalcEngine ce) + { + //ce.RegisterFunction("ASC Changes full-width (double-byte) English letters or katakana within a character string to half-width (single-byte) characters + //ce.RegisterFunction("BAHTTEXT Converts a number to text, using the ß (baht) currency format + ce.RegisterFunction("CHAR", 1, _Char); // Returns the character specified by the code number + //ce.RegisterFunction("CLEAN Removes all nonprintable characters from text + ce.RegisterFunction("CODE", 1, Code); // Returns a numeric code for the first character in a text string + ce.RegisterFunction("CONCATENATE", 1, int.MaxValue, Concat); // Joins several text items into one text item + //ce.RegisterFunction("DOLLAR Converts a number to text, using the $ (dollar) currency format + //ce.RegisterFunction("EXACT Checks to see if two text values are identical + ce.RegisterFunction("FIND", 2, 3, Find); //Finds one text value within another (case-sensitive) + //ce.RegisterFunction("FIXED Formats a number as text with a fixed number of decimals + //ce.RegisterFunction("JIS Changes half-width (single-byte) English letters or katakana within a character string to full-width (double-byte) characters + ce.RegisterFunction("LEFT", 1, 2, Left); // LEFTB Returns the leftmost characters from a text value + ce.RegisterFunction("LEN", 1, Len); //, Returns the number of characters in a text string + ce.RegisterFunction("LOWER", 1, Lower); // Converts text to lowercase + ce.RegisterFunction("MID", 3, Mid); // Returns a specific number of characters from a text string starting at the position you specify + //ce.RegisterFunction("PHONETIC Extracts the phonetic (furigana) characters from a text string + ce.RegisterFunction("PROPER", 1, Proper); // Capitalizes the first letter in each word of a text value + ce.RegisterFunction("REPLACE", 4, Replace); // Replaces characters within text + ce.RegisterFunction("REPT", 2, Rept); // Repeats text a given number of times + ce.RegisterFunction("RIGHT", 1, 2, Right); // Returns the rightmost characters from a text value + ce.RegisterFunction("SEARCH", 2, Search); // Finds one text value within another (not case-sensitive) + ce.RegisterFunction("SUBSTITUTE", 3, 4, Substitute); // Substitutes new text for old text in a text string + ce.RegisterFunction("T", 1, T); // Converts its arguments to text + ce.RegisterFunction("TEXT", 2, _Text); // Formats a number and converts it to text + ce.RegisterFunction("TRIM", 1, Trim); // Removes spaces from text + ce.RegisterFunction("UPPER", 1, Upper); // Converts text to uppercase + ce.RegisterFunction("VALUE", 1, Value); // Converts a text argument to a number + } + + static object _Char(List p) + { + var c = (char)(int)p[0]; + return c.ToString(); + } + static object Code(List p) + { + var s = (string)p[0]; + return (int)s[0]; + } + static object Concat(List p) + { + var sb = new StringBuilder(); + foreach (var x in p) + { + sb.Append((string)x); + } + return sb.ToString(); + } + static object Find(List p) + { + return IndexOf(p, StringComparison.Ordinal); + } + static int IndexOf(List p, StringComparison cmp) + { + var srch = (string)p[0]; + var text = (string)p[1]; + var start = 0; + if (p.Count > 2) + { + start = (int)p[2] - 1; + } + var index = text.IndexOf(srch, start, cmp); + return index > -1 ? index + 1 : index; + } + static object Left(List p) + { + var n = 1; + if (p.Count > 1) + { + n = (int)p[1]; + } + return ((string)p[0]).Substring(0, n); + } + static object Len(List p) + { + return ((string)p[0]).Length; + } + static object Lower(List p) + { + return ((string)p[0]).ToLower(); + } + static object Mid(List p) + { + return ((string)p[0]).Substring((int)p[1] - 1, (int)p[2]); + } + static object Proper(List p) + { + var s = (string)p[0]; + return s.Substring(0, 1).ToUpper() + s.Substring(1).ToLower(); + } + static object Replace(List p) + { + // old start len new + var s = (string)p[0]; + var start = (int)p[1] - 1; + var len = (int)p[2]; + var rep = (string)p[3]; + + var sb = new StringBuilder(); + sb.Append(s.Substring(0, start)); + sb.Append(rep); + sb.Append(s.Substring(start + len)); + + return sb.ToString(); + } + static object Rept(List p) + { + var sb = new StringBuilder(); + var s = (string)p[0]; + for (int i = 0; i < (int)p[1]; i++) + { + sb.Append(s); + } + return sb.ToString(); + } + static object Right(List p) + { + var n = 1; + if (p.Count > 1) + { + n = (int)p[1]; + } + var s = (string)p[0]; + return s.Substring(s.Length - n); + } + static object Search(List p) + { + return IndexOf(p, StringComparison.OrdinalIgnoreCase); + } + static object Substitute(List p) + { + // get parameters + var text = (string)p[0]; + var oldText = (string)p[1]; + var newText = (string)p[2]; + + // if index not supplied, replace all + if (p.Count == 3) + { + return text.Replace(oldText, newText); + } + + // replace specific instance + int index = (int)p[3]; + if (index < 1) + { + throw new Exception("Invalid index in Substitute."); + } + int pos = text.IndexOf(oldText); + while (pos > -1 && index > 1) + { + pos = text.IndexOf(oldText, pos + 1); + index--; + } + return pos > -1 + ? text.Substring(0, pos) + newText + text.Substring(pos + oldText.Length) + : text; + } + static object T(List p) + { + return (string)p[0]; + } + static object _Text(List p) + { + return ((double)p[0]).ToString((string)p[1], CultureInfo.CurrentCulture); + } + static object Trim(List p) + { + return ((string)p[0]).Trim(); + } + static object Upper(List p) + { + return ((string)p[0]).ToUpper(); + } + static object Value(List p) + { + return double.Parse((string)p[0], NumberStyles.Any, CultureInfo.InvariantCulture); + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Token.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Token.cs new file mode 100644 index 0000000..2095806 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/Token.cs @@ -0,0 +1,47 @@ +namespace ClosedXML.Excel.CalcEngine +{ + /// + /// Represents a node in the expression tree. + /// + internal class Token + { + // ** fields + public TKID ID; + public TKTYPE Type; + public object Value; + + // ** ctor + public Token(object value, TKID id, TKTYPE type) + { + Value = value; + ID = id; + Type = type; + } + } + /// + /// Token types (used when building expressions, sequence defines operator priority) + /// + internal enum TKTYPE + { + COMPARE, // < > = <= >= + ADDSUB, // + - + MULDIV, // * / + POWER, // ^ + GROUP, // ( ) , . + LITERAL, // 123.32, "Hello", etc. + IDENTIFIER // functions, external objects, bindings + } + /// + /// Token ID (used when evaluating expressions) + /// + internal enum TKID + { + GT, LT, GE, LE, EQ, NE, // COMPARE + ADD, SUB, // ADDSUB + MUL, DIV, DIVINT, MOD, // MULDIV + POWER, // POWER + OPEN, CLOSE, END, COMMA, PERIOD, // GROUP + ATOM, // LITERAL, IDENTIFIER + CONCAT + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs new file mode 100644 index 0000000..cc78c47 --- /dev/null +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/CalcEngine/XLCalcEngine.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace ClosedXML.Excel.CalcEngine +{ + public class XLCalcEngine : CalcEngine + { + private IXLWorksheet _ws; + public XLCalcEngine(IXLWorksheet ws) + { + _ws = ws; + // parse multi-cell range references ($A2:B$4) + IdentifierChars = "$:!"; + } + + public override object GetExternalObject(string identifier) + { + //if (!XLHelper.IsValidA1Address(identifier)) return null; + //String wsName; + if (identifier.Contains("!")) + { + var wsName = identifier.Substring(0, identifier.IndexOf("!")); + return new CellRangeReference(_ws.Workbook.Worksheet(wsName).Range(identifier.Substring(identifier.IndexOf("!") + 1)), this); + } + return new CellRangeReference(_ws.Range(identifier), this); + } + + + } + + public class CellRangeReference : IValueObject, IEnumerable + { + private IXLRange _range; + private XLCalcEngine _ce; + public CellRangeReference(IXLRange range, XLCalcEngine ce) + { + _range = range; + _ce = ce; + } + + // ** IValueObject + public object GetValue() + { + return GetValue(_range.FirstCell()); + } + + // ** IEnumerable + public IEnumerator GetEnumerator() + { + return _range.Cells().Select(GetValue).GetEnumerator(); + } + + private Boolean _evaluating; + + // ** implementation + object GetValue(IXLCell cell) + { + if (_evaluating) + { + throw new Exception("Circular Reference"); + } + try + { + _evaluating = true; + var f = cell.FormulaA1; + if (XLHelper.IsNullOrWhiteSpace(f)) + return cell.Value; + else + return _ce.Evaluate(f); + + } + finally + { + _evaluating = false; + } + } + } +} diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/Cells/XLCell.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/Cells/XLCell.cs index 1d33b47..66ccf14 100644 --- a/ClosedXML/ClosedXML/ClosedXML/Excel/Cells/XLCell.cs +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/Cells/XLCell.cs @@ -404,7 +404,8 @@ && XLHelper.IsValidA1Address(cAddress) ) return _worksheet.Workbook.Worksheet(sName).Cell(cAddress).Value; - return fA1; + + return Worksheet.CalcEngine.Evaluate(fA1); } String cellValue = HasRichText ? _richText.ToString() : _cellValue; diff --git a/ClosedXML/ClosedXML/ClosedXML/Excel/XLWorksheet.cs b/ClosedXML/ClosedXML/ClosedXML/Excel/XLWorksheet.cs index 77f3cfd..4a2ce67 100644 --- a/ClosedXML/ClosedXML/ClosedXML/Excel/XLWorksheet.cs +++ b/ClosedXML/ClosedXML/ClosedXML/Excel/XLWorksheet.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using ClosedXML.Excel.CalcEngine; namespace ClosedXML.Excel { @@ -1410,5 +1411,11 @@ public IXLRanges SelectedRanges { get; internal set; } public IXLCell ActiveCell { get; set; } + + private XLCalcEngine _calcEngine; + public XLCalcEngine CalcEngine + { + get { return _calcEngine ?? (_calcEngine = new XLCalcEngine(this)); } + } } } \ No newline at end of file diff --git a/ClosedXML/ClosedXML/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj b/ClosedXML/ClosedXML/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj index 4e61e5d..27d3fd3 100644 --- a/ClosedXML/ClosedXML/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj +++ b/ClosedXML/ClosedXML/ClosedXML_Net3.5/ClosedXML_Net3.5.csproj @@ -162,6 +162,39 @@ Excel\AutoFilters\XLFilteredColumn.cs + + Excel\CalcEngine\CalcEngine.cs + + + Excel\CalcEngine\Expression.cs + + + Excel\CalcEngine\ExpressionCache.cs + + + Excel\CalcEngine\FunctionDefinition.cs + + + Excel\CalcEngine\Functions\Logical.cs + + + Excel\CalcEngine\Functions\MathTrig.cs + + + Excel\CalcEngine\Functions\Statistical.cs + + + Excel\CalcEngine\Functions\Tally.cs + + + Excel\CalcEngine\Functions\Text.cs + + + Excel\CalcEngine\Token.cs + + + Excel\CalcEngine\XLCalcEngine.cs + Excel\Cells\IXLCell.cs