Newer
Older
ClosedXML / ClosedXML / Excel / CalcEngine / CalcEngineHelpers.cs
@Francois Botha Francois Botha on 18 Apr 2018 5 KB Use inline out parameters provided by C# 7.2
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;

namespace ClosedXML.Excel.CalcEngine
{
    internal class CalcEngineHelpers
    {
        private static Lazy<Dictionary<string, Tuple<string, string>>> patternReplacements =
            new Lazy<Dictionary<string, Tuple<string, string>>>(() =>
            {
                var patternReplacements = new Dictionary<string, Tuple<string, string>>();
                // key: the literal string to match
                // value: a tuple: first item: the search pattern, second item: the replacement
                patternReplacements.Add(@"~~", new Tuple<string, string>(@"~~", "~"));
                patternReplacements.Add(@"~*", new Tuple<string, string>(@"~\*", @"\*"));
                patternReplacements.Add(@"~?", new Tuple<string, string>(@"~\?", @"\?"));
                patternReplacements.Add(@"?", new Tuple<string, string>(@"\?", ".?"));
                patternReplacements.Add(@"*", new Tuple<string, string>(@"\*", ".*"));

                return patternReplacements;
            });

        internal 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)
            {
                if (value is Double)
                    return (double)value == (double)criteria;
                return Double.TryParse(value.ToString(), out double dValue) && dValue == (double)criteria;
            }

            // convert criteria to string
            var cs = criteria as string;
            if (cs != null)
            {
                if (value is string && (value as string).Trim() == "")
                    return cs == "";

                if (cs == "")
                    return cs.Equals(value);

                // if criteria is an expression (e.g. ">20"), use calc engine
                if ((cs[0] == '=' && cs.IndexOfAny(new[] { '*', '?' }) < 0)
                    || cs[0] == '<'
                    || cs[0] == '>')
                {
                    // build expression
                    var expression = string.Format("{0}{1}", value, cs);

                    // add quotes if necessary
                    var pattern = @"([\w\s]+)(\W+)(\w+)";
                    var m = Regex.Match(expression, pattern);
                    if (m.Groups.Count == 4
                        && (!double.TryParse(m.Groups[1].Value, out double 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.IndexOfAny(new[] { '*', '?' }) > -1)
                {
                    if (cs[0] == '=') cs = cs.Substring(1);

                    var pattern = Regex.Replace(
                        cs,
                        "(" + String.Join(
                                "|",
                                patternReplacements.Value.Values.Select(t => t.Item1))
                        + ")",
                        m => patternReplacements.Value[m.Value].Item2);
                    pattern = $"^{pattern}$";

                    return Regex.IsMatch(value.ToString(), pattern, RegexOptions.IgnoreCase);
                }

                // straight string comparison
                return string.Equals(value.ToString(), cs, StringComparison.OrdinalIgnoreCase);
            }

            // should never get here?
            Debug.Assert(false, "failed to evaluate criteria in SumIf");
            return false;
        }

        internal static bool ValueIsBlank(object value)
        {
            return
                value == null ||
                value is string && ((string)value).Length == 0;
        }

        /// <summary>
        /// Get total count of cells in the specified range without initalizing them all
        /// (which might cause serious performance issues on column-wide calculations).
        /// </summary>
        /// <param name="rangeExpression">Expression referring to the cell range.</param>
        /// <returns>Total number of cells in the range.</returns>
        internal static long GetTotalCellsCount(XObjectExpression rangeExpression)
        {
            var range = ((rangeExpression)?.Value as CellRangeReference)?.Range;
            if (range == null)
                return 0;
            return (long)(range.LastColumn().ColumnNumber() - range.FirstColumn().ColumnNumber() + 1) *
                   (long)(range.LastRow().RowNumber() - range.FirstRow().RowNumber() + 1);
        }
    }
}