From e10be70a5d182fdd3e7003661e23369035de950d Mon Sep 17 00:00:00 2001 From: uom Date: Thu, 13 Jan 2022 23:22:41 +0700 Subject: [PATCH 01/10] Imported iss29 from uom local --- .../Commons/EnumerableExtensions.cs | 67 +- NavfertyExcelAddIn/NavfertyExcelAddIn.csproj | 784 +++++++++--------- .../ParseNumerics/DecimalParser.cs | 231 +++--- .../ParseNumerics/NumericParseResult.cs | 61 ++ .../ParseNumerics/NumericParser.cs | 73 +- 5 files changed, 690 insertions(+), 526 deletions(-) create mode 100644 NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs diff --git a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs index 0cf5f08..6b7eb41 100644 --- a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs +++ b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs @@ -28,8 +28,8 @@ public static void ForEach(this IEnumerable source, Action action) } public static void ForEachCell(this Range range, Action action) - { - // TODO rewrite to use less read-write calls to interop (like Range.Value) + { + // TODO rewrite to use less read-write calls to interop (like Range.Value) (may be use try/finally with selection.Worksheet.EnableCalculation = false/true?; range.Cast().ForEach(action); } @@ -64,15 +64,15 @@ private static void ApplyToArea(Range range, Func transfor }; undoManager.PushUndoItem(undoItem); return; - } - - // minimize number of COM calls to excel + } + + // minimize number of COM calls to excel if (!(rangeValue is object[,] values)) return; - int upperI = values.GetUpperBound(0); // Rows - int upperJ = values.GetUpperBound(1); // Columns - + int upperI = values.GetUpperBound(0); // Rows + int upperJ = values.GetUpperBound(1); // Columns + var isChanged = false; var oldValues = (object[,])values.Clone(); @@ -87,7 +87,7 @@ private static void ApplyToArea(Range range, Func transfor if (value is TIn s) { var newValue = transform(s); - if ((object)newValue != value) // TODO check boxing time on million values + if ((object)newValue != value) // TODO check boxing time on million values { isChanged = true; values[i, j] = newValue; @@ -110,6 +110,55 @@ private static void ApplyToArea(Range range, Func transfor Width = upperJ }); } + } + + /// Allow acces to Range object from transform func + public static void ApplyForEachCellOfType2(this Range range, Func transform) + { + logger.Debug($"Apply transformation to range '{range.GetRelativeAddress()}' on worksheet '{range.Worksheet.Name}'"); + + undoManager.StartNewAction(range); + + foreach (Range area in range.Areas) + { + ApplyToArea2(area, transform); + } + } + + // TODO check boxing time on million values + /// Allow acces to Range object from transform func may be slower than Old + private static void ApplyToArea2(Range range, Func transform) + { + try { if (null == range || null == range.Cells) return; } catch { return; }//Just for Test cases + + foreach (Range cell in range.Cells) + { + var cellValue = cell.Value; + if (!(cellValue is null)) + { + + if (cellValue is TIn currentValue) + { + // TODO transform func may chabge format of cell, and we need to allow undo this, but set/restore cell formating has so weird api... + var newValue = transform(currentValue, cell); + if (!(newValue == null)) + { + if (!newValue.Equals(currentValue)) + { + cell.Value = newValue; + var undoItem = new UndoItem + { + OldValue = currentValue, + NewValue = newValue, + ColumnIndex = cell.Column, + RowIndex = cell.Row + }; + undoManager.PushUndoItem(undoItem); + } + } + } + } + } } } } diff --git a/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj b/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj index 61b7d7f..f5d91dc 100644 --- a/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj +++ b/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj @@ -1,5 +1,5 @@ - - + + - - Latest - enable - enable - - {BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - Debug - AnyCPU - {DEF56C38-95B1-4CFC-8B27-70FEA7A35D78} - Library - false - NavfertyExcelAddIn - NavfertyExcelAddIn - v4.8 - VSTO40 - true - опубликовать\ - https://www.navferty.ru/deploy/ - ru - $(Version) - true - true - 7 - days - True - NavfertyExcelAddIn - - - NavfertyExcelAddIn - - 3 - - - - - False - Microsoft .NET Framework 4.7.2 %28x86 и x64%29 - true - - - False - .NET Framework 3.5 SP1 - false - - - False - Среда выполнения Microsoft Visual Studio 2010 Tools for Office %28x86 и x64%29 - true - - - + --> + + Latest + enable + enable + {BAA0C2D2-18E2-41B9-852F-F413020CAA33};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + Debug + AnyCPU + {DEF56C38-95B1-4CFC-8B27-70FEA7A35D78} + Library + false + NavfertyExcelAddIn + NavfertyExcelAddIn + v4.8 + VSTO40 + true + опубликовать\ + https://www.navferty.ru/deploy/ + ru + $(Version) + true + true + 7 + days + True + NavfertyExcelAddIn + + + NavfertyExcelAddIn + + 3 + + + + + False + Microsoft .NET Framework 4.7.2 %28x86 и x64%29 + true + + + False + .NET Framework 3.5 SP1 + false + + + False + Среда выполнения Microsoft Visual Studio 2010 Tools for Office %28x86 и x64%29 + true + + + - Excel - + --> + Excel + - - true - full - false - bin\Debug\ - false - $(DefineConstants);DEBUG;TRACE - 4 - + --> + + true + full + false + bin\Debug\ + false + $(DefineConstants);DEBUG;TRACE + 4 + - - pdbonly - true - bin\Release\ - false - $(DefineConstants);TRACE - 4 - + --> + + pdbonly + true + bin\Release\ + false + $(DefineConstants);TRACE + 4 + - - - - ..\packages\Autofac.6.2.0\lib\netstandard2.0\Autofac.dll - - - ..\packages\Autofac.Extras.DynamicProxy.6.0.0\lib\netstandard2.0\Autofac.Extras.DynamicProxy.dll - - - ..\packages\Castle.Core.4.4.1\lib\net45\Castle.Core.dll - - - ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll - - - ..\packages\NLog.4.6.7\lib\net45\NLog.dll - - - - ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll - - - - - ..\packages\System.Diagnostics.DiagnosticSource.4.7.1\lib\net46\System.Diagnostics.DiagnosticSource.dll - - - - - - ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll - True - True - - - ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll - - - - ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll - - - ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll - - - - - ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll - - - - - - - - - - ..\packages\Microsoft.Xml.XMLGen.1.0.0\lib\XmlSampleGenerator.dll - - - - - False - - - False - - - False - - - False - - - False - - - - - True - - - - - False - True - - - False - True - - - False - - + --> + + + + ..\packages\Autofac.6.2.0\lib\netstandard2.0\Autofac.dll + + + ..\packages\Autofac.Extras.DynamicProxy.6.0.0\lib\netstandard2.0\Autofac.Extras.DynamicProxy.dll + + + ..\packages\Castle.Core.4.4.1\lib\net45\Castle.Core.dll + + + ..\packages\Microsoft.Bcl.AsyncInterfaces.1.1.0\lib\net461\Microsoft.Bcl.AsyncInterfaces.dll + + + ..\packages\NLog.4.6.7\lib\net45\NLog.dll + + + + ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll + + + + + ..\packages\System.Diagnostics.DiagnosticSource.4.7.1\lib\net46\System.Diagnostics.DiagnosticSource.dll + + + + + + ..\packages\System.IO.Compression.ZipFile.4.3.0\lib\net46\System.IO.Compression.ZipFile.dll + True + True + + + ..\packages\System.Memory.4.5.4\lib\net461\System.Memory.dll + + + + ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll + + + ..\packages\System.Runtime.CompilerServices.Unsafe.4.5.3\lib\net461\System.Runtime.CompilerServices.Unsafe.dll + + + + + ..\packages\System.Threading.Tasks.Extensions.4.5.4\lib\net461\System.Threading.Tasks.Extensions.dll + + + + + + + + + + ..\packages\Microsoft.Xml.XMLGen.1.0.0\lib\XmlSampleGenerator.dll + + + + + False + + + False + + + False + + + False + + + False + + + + + True + + + + + False + True + + + False + True + + + False + + - - - - - - - - - - - - - - - - - - - - RibbonSupertips.resx - True - True - - - - - - - - - - - - - - - - - - True - True - ValidationMessages.resx - - - - - - - - - - - - - - - - Form - - - InteractiveRangeReportForm.cs - - - - - - - - - - - - Code - - - True - True - RibbonIcons.resx - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Always - - - Designer - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - True - Settings.settings - True - - - - Code - - - ThisAddIn.cs - - - ThisAddIn.Designer.xml - - - - True - True - UIStrings.resx - - - True - True - RibbonLabels.resx - - - - - InteractiveRangeReportForm.cs - Designer - - - ResXFileCodeGenerator - RibbonSupertips.Designer.cs - Designer - - - Designer - - - ResXFileCodeGenerator - RibbonLabels.Designer.cs - Designer - - - Designer - - - ResXFileCodeGenerator - UIStrings.Designer.cs - Designer - - - - ResXFileCodeGenerator - ValidationMessages.Designer.cs - - - - - ResXFileCodeGenerator - RibbonIcons.Designer.cs - Designer - - - - - 10.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - true - NavfertyExcelAddIn_uom_TemporaryKey.pfx - 2A9C33F460B4D532AD2F603B76CA85F096C65728 - - - - - true - NavfertyExcelAddIn_TemporaryKey.pfx - 8A194674EFEB140748CD131659BF16EE09C2361A - - - - - - - - - - - - - - - - - - + --> + + + + + + + + + + + + + + + + + + + + + RibbonSupertips.resx + True + True + + + + + + + + + + + + + + + + + + True + True + ValidationMessages.resx + + + + + + + + + + + + + + + + Form + + + InteractiveRangeReportForm.cs + + + + + + + + + + + + Code + + + True + True + RibbonIcons.resx + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Designer + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + True + Settings.settings + True + + + + Code + + + ThisAddIn.cs + + + ThisAddIn.Designer.xml + + + + True + True + UIStrings.resx + + + True + True + RibbonLabels.resx + + + + + InteractiveRangeReportForm.cs + Designer + + + ResXFileCodeGenerator + RibbonSupertips.Designer.cs + Designer + + + Designer + + + ResXFileCodeGenerator + RibbonLabels.Designer.cs + Designer + + + Designer + + + ResXFileCodeGenerator + UIStrings.Designer.cs + Designer + + + + ResXFileCodeGenerator + ValidationMessages.Designer.cs + + + + + ResXFileCodeGenerator + RibbonIcons.Designer.cs + Designer + + + + + 10.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + true + NavfertyExcelAddIn_uom_TemporaryKey.pfx + 2A9C33F460B4D532AD2F603B76CA85F096C65728 + + + + + true + NavfertyExcelAddIn_TemporaryKey.pfx + 8A194674EFEB140748CD131659BF16EE09C2361A + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index 47089bc..4d89ac0 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -1,106 +1,135 @@ -using System.Globalization; -using System.Linq; -using System.Text.RegularExpressions; - -namespace NavfertyExcelAddIn.ParseNumerics +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace NavfertyExcelAddIn.ParseNumerics { - /// - /// Parser implemented in https://github.com/navferty/NumericParser - /// - public static class DecimalParser - { - private static readonly Regex SpacesPattern = new(@"\s"); - private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]*"); - private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); - - //private const string CS10_TEST = $"{{222"; - private static readonly string? fff = null; - - public static decimal? ParseDecimal(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } - - var v = SpacesPattern.Replace(value, match => string.Empty); - - if (ExponentPattern.IsMatch(v)) - { - return v.TryParseExponent(); - } - - if (!DecimalPattern.IsMatch(value)) - { - return null; - } - - if (v.Contains(",") && v.Contains(".")) - { - var last = v.LastIndexOfAny(new[] { ',', '.' }); - var c = v[last]; - return v.CountChars(c) == 1 - ? v.TryParse(c == '.' ? Format.Dot : Format.Comma) - : null; - } - - if (v.Contains(",")) - { - return v.CountChars(',') == 1 - ? v.TryParse(Format.Comma) - : v.TryParse(Format.Dot); - } - - if (v.Contains(".")) - { - return v.CountChars('.') == 1 - ? v.TryParse(Format.Dot) - : v.TryParse(Format.Comma); - } - - return v.TryParse(Format.Dot); - } - - private static int CountChars(this string value, char c) - { - return value.Count(x => x == c); - } - - private static decimal? TryParseExponent(this string value) - { - return decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal result) - ? result - : (decimal?)null; - } - - private static decimal? TryParse(this string value, Format info) - { - var formatInfo = (NumberFormatInfo)NumberFormatInfo.InvariantInfo.Clone(); - - if (info == Format.Comma) - { - formatInfo.CurrencyDecimalSeparator = ","; - formatInfo.CurrencyGroupSeparator = "."; - formatInfo.NumberDecimalSeparator = ","; - formatInfo.NumberGroupSeparator = "."; - } - else - { - formatInfo.CurrencyDecimalSeparator = "."; - formatInfo.CurrencyGroupSeparator = ","; + /// + /// Parser implemented in https://github.com/navferty/NumericParser + /// + public static class DecimalParser + { + private static readonly Regex SpacesPattern = new Regex(@"\s"); + private static readonly Regex DecimalPattern = new Regex(@"[\d\.\,\s]+"); + private static readonly Regex ExponentPattern = new Regex(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); + + public static NumericParseResult? ParseDecimal(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var v = SpacesPattern.Replace(value, match => string.Empty); + + if (ExponentPattern.IsMatch(v)) + { + return new NumericParseResult(v.TryParseExponent()); + } + + if (!DecimalPattern.IsMatch(value)) + { + return null; + } + + if (v.Contains(",") && v.Contains(".")) + { + var last = v.LastIndexOfAny(new[] { ',', '.' }); + var c = v[last]; + return v.CountChars(c) == 1 + ? v.TryParse(c == '.' ? Format.Dot : Format.Comma) + : (NumericParseResult?)null; + } + + if (v.Contains(",")) + { + return v.CountChars(',') == 1 + ? v.TryParse(Format.Comma) + : v.TryParse(Format.Dot); + } + + if (v.Contains(".")) + { + return v.CountChars('.') == 1 + ? v.TryParse(Format.Dot) + : v.TryParse(Format.Comma); + } + + return v.TryParse(Format.Dot); + } + + private static int CountChars(this string value, char c) + { + return value.Count(x => x == c); + } + + private static decimal? TryParseExponent(this string value) + { + return decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal result) + ? result + : (decimal?)null; + } + + private static Lazy _allCurrencySymbolsCacheLazy = new Lazy(() + => CultureInfo.GetCultures(CultureTypes.AllCultures) + .Select(ci => ci.NumberFormat.CurrencySymbol) + .Distinct() + .Where(cur => !string.IsNullOrWhiteSpace(cur)) + .ToArray()); + + private static NumericParseResult? TryParse(this string value, Format info) + { + var formatInfo = (NumberFormatInfo)NumberFormatInfo.InvariantInfo.Clone(); + + if (info == Format.Comma) + { + formatInfo.CurrencyDecimalSeparator = ","; + formatInfo.CurrencyGroupSeparator = "."; + formatInfo.NumberDecimalSeparator = ","; + formatInfo.NumberGroupSeparator = "."; + } + else + { + formatInfo.CurrencyDecimalSeparator = "."; + formatInfo.CurrencyGroupSeparator = ","; } // добавить тест-кейсов на формат валют //formatInfo.CurrencyNegativePattern = 8; //formatInfo.CurrencyPositivePattern = 3; - return decimal.TryParse(value, NumberStyles.Currency, formatInfo, out decimal result) - ? result - : (decimal?)null; - } - - private enum Format - { - Dot, - Comma - } - } -} + + var valueParsed = decimal.TryParse(value, NumberStyles.Currency, formatInfo, out decimal result); + if (valueParsed) return new NumericParseResult(result);//Parsed without our help + + //decimal.TryParse не может разобрать строку со значком любой валюты, кроме валюты текущей культуры, + //и символ валюты должен располагаться в правильном месте (как требуется в конкретной культуре)!!! + //Поэтому помогаем руками разобрать строку с произвольной валютой... + + //detect how many currency symbols contains source string... + var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur)).ToArray(); + if (currenciesInValue.Count() == 1)// TODO: Если строка содержит несколько разных символов валют, не преобразовываем, т.к. приоритет валют не ясен + { + var curSymb = currenciesInValue.First(); + //Remove found currencySymbol from source string + var valueWithoutCurrencySymbol = value.Replace(curSymb, string.Empty); + valueParsed = decimal.TryParse(valueWithoutCurrencySymbol, NumberStyles.Currency, formatInfo, out result); + if (valueParsed) + { + //System.Windows.Forms.MessageBox.Show($"Parsed value: '{value}, valueWithoutCurrencySymbol: {valueWithoutCurrencySymbol}', result: {result}, currency: {curSymb}"); + return new NumericParseResult(result, curSymb); + } + //It was not possible to parse the line, even after removing the currencySymbol, most likely this is not about money at all... + } + return (NumericParseResult?)null;//Not found any currency symbols, or found more than one, or even not number... + } + + private enum Format + { + Dot, + Comma + } + } +} diff --git a/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs b/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs new file mode 100644 index 0000000..e9d5272 --- /dev/null +++ b/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace NavfertyExcelAddIn.ParseNumerics +{ + public struct NumericParseResult + { + private static readonly string currencySymbolFromOSUserLocale = CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol; + private static readonly string currencySymbolRu = CultureInfo.GetCultureInfo("ru").NumberFormat.CurrencySymbol; + + public readonly decimal? ConvertedValue; + public readonly string Currency; + + public NumericParseResult(decimal? value, string curr = null) + { + ConvertedValue = value; + Currency = curr; + } + + public bool IsMoney + => !string.IsNullOrEmpty(Currency); + + public bool IsCurrencyFromCurrentCulture() + => (currencySymbolFromOSUserLocale == Currency); + + public bool IsCurrencyFromRU() + => (currencySymbolRu == Currency); + + /* + + public static bool operator ==(NumericParseResult obj1, NumericParseResult obj2) + { + if ((obj1 is null) && (obj2 is null)) return true; + if (obj1 is null) return false; + + return obj1.ConvertedValue == obj2.ConvertedValue + && obj1.Currency == obj2.Currency + && obj1.IsMoney == obj2.IsMoney; + } + + public static bool operator !=(NumericParseResult obj1, NumericParseResult obj2) + { + if ((obj1 is null) && (obj2 is null)) return false; + if (obj1 is null) return true; + + return !(obj1.ConvertedValue == obj2.ConvertedValue + && obj1.Currency == obj2.Currency + && obj1.IsMoney == obj2.IsMoney); + } + + public override bool Equals(object obj) + => !(obj is null) && (obj.GetType() == typeof(NumericParseResult) && (this == obj as NumericParseResult)); + + */ + + } +} diff --git a/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs b/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs index 4b539ca..108d573 100644 --- a/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs @@ -1,24 +1,49 @@ -using System; - -using Microsoft.Office.Interop.Excel; - -using NavfertyExcelAddIn.Commons; - -namespace NavfertyExcelAddIn.ParseNumerics -{ - public class NumericParser : INumericParser - { - public void Parse(Range selection) - { - selection.ApplyForEachCellOfType( - value => - { - var newValue = value.ParseDecimal(); - if (newValue.HasValue) - // Excel stores numerics as Double - return (object)Convert.ToDouble(newValue); - return (object)value; - }); - } - } -} +using System; +using System.Diagnostics; + +using Microsoft.Office.Interop.Excel; + +using NavfertyExcelAddIn.Commons; + +namespace NavfertyExcelAddIn.ParseNumerics +{ + public class NumericParser : INumericParser + { + public void Parse(Range selection) + { + //Формат положительных;Формат отрицательных;Формат нулей;Формат текста + const string EXCEL_CURRENCY_FORMAT_TEMPLATE_RUS = @"_-* #,##0.00 {CUR}_-;-* #,##0.00 {CUR}_-;_-* ""-""?? {CUR}_-;_-@_-"; + const string EXCEL_CURRENCY_FORMAT_TEMPLATE_LAT = @"_-{CUR}* # ##0.00_-;-{CUR}* # ##0.00_-;_-{CUR}* ""-""??_-;_-@_-"; + const string CURRENCY_TEMPLATE = @"{CUR}"; + + bool autoCalcEnabled = false; + try { autoCalcEnabled = selection.Worksheet.EnableCalculation; } catch { } + if (autoCalcEnabled) selection.Worksheet.EnableCalculation = false; + try + { + + selection.ApplyForEachCellOfType2( + (value, cell) => + { + var pdResult = value.ParseDecimal(); + if (!pdResult.HasValue || !pdResult.Value.ConvertedValue.HasValue) + return (object)value; + + var npr = pdResult.Value; + //Parsed Ok... + if (pdResult.Value.IsMoney) + { + string currencyFormat = npr.IsCurrencyFromRU() ? EXCEL_CURRENCY_FORMAT_TEMPLATE_RUS : EXCEL_CURRENCY_FORMAT_TEMPLATE_LAT; + string curSymFmt = @"[$" + npr.Currency + @"]"; + cell.NumberFormat = currencyFormat.Replace(CURRENCY_TEMPLATE, curSymFmt); + } + return (object)Convert.ToDouble(npr.ConvertedValue.Value);// Excel stores numerics as Double + }); + } + finally + { + if (autoCalcEnabled) selection.Worksheet.EnableCalculation = autoCalcEnabled;//Restart sheet autorecalc + } + } + } +} From 264de6dda73fa0c842922e0ded4dda8b385f28a5 Mon Sep 17 00:00:00 2001 From: uom Date: Fri, 14 Jan 2022 00:14:32 +0700 Subject: [PATCH 02/10] iss 29 otimization 1 --- .../Commons/EnumerableExtensions.cs | 76 ++++++++++--------- .../Commons/StringExtensions.cs | 18 +++-- .../ParseNumerics/DecimalParser.cs | 36 ++++----- .../ParseNumerics/NumericParseResult.cs | 6 +- 4 files changed, 74 insertions(+), 62 deletions(-) diff --git a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs index 6b7eb41..b89a251 100644 --- a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs +++ b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs @@ -1,7 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; - +using System.Runtime.CompilerServices; + using Autofac; using Microsoft.Office.Interop.Excel; @@ -19,6 +20,7 @@ public static class EnumerableExtensions private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ForEach(this IEnumerable source, Action action) { foreach (T element in source) @@ -27,12 +29,14 @@ public static void ForEach(this IEnumerable source, Action action) } } - public static void ForEachCell(this Range range, Action action) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void ForEachCell(this Range? range, Action action) { // TODO rewrite to use less read-write calls to interop (like Range.Value) (may be use try/finally with selection.Worksheet.EnableCalculation = false/true?; - range.Cast().ForEach(action); + range?.Cast().ForEach(action); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ApplyForEachCellOfType(this Range range, Func transform) { logger.Debug($"Apply transformation to range '{range.GetRelativeAddress()}' on worksheet '{range.Worksheet.Name}'"); @@ -45,6 +49,7 @@ public static void ApplyForEachCellOfType(this Range range, Func(Range range, Func transform) { var rangeValue = range.Value; @@ -113,6 +118,7 @@ private static void ApplyToArea(Range range, Func transfor } /// Allow acces to Range object from transform func + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void ApplyForEachCellOfType2(this Range range, Func transform) { logger.Debug($"Apply transformation to range '{range.GetRelativeAddress()}' on worksheet '{range.Worksheet.Name}'"); @@ -127,38 +133,36 @@ public static void ApplyForEachCellOfType2(this Range range, FuncAllow acces to Range object from transform func may be slower than Old - private static void ApplyToArea2(Range range, Func transform) - { - try { if (null == range || null == range.Cells) return; } catch { return; }//Just for Test cases + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void ApplyToArea2(Range area, Func transform) + { + //try { if (null == range || null == range.Cells) return; } catch { return; }//TODO: Just for Test cases, remove catch and modify tests (range.Cells) + area?.Cells?.ForEachCell(cell => + { + { + var cellValue = cell.Value; + if ((cellValue is null) || (cellValue is not TIn currentValue)) return; - foreach (Range cell in range.Cells) - { - var cellValue = cell.Value; - if (!(cellValue is null)) - { + // TODO transform func may change format of cell, and we need to allow undo this, but set/restore cell formating has so weird api... + var newValue = transform(currentValue, cell); + if (newValue == null) return; + + if (!newValue.Equals(currentValue)) + { + cell.Value = newValue; + var undoItem = new UndoItem + { + OldValue = currentValue, + NewValue = newValue, + ColumnIndex = cell.Column, + RowIndex = cell.Row + }; + undoManager.PushUndoItem(undoItem); + } + } + }); + + } + } +} - if (cellValue is TIn currentValue) - { - // TODO transform func may chabge format of cell, and we need to allow undo this, but set/restore cell formating has so weird api... - var newValue = transform(currentValue, cell); - if (!(newValue == null)) - { - if (!newValue.Equals(currentValue)) - { - cell.Value = newValue; - var undoItem = new UndoItem - { - OldValue = currentValue, - NewValue = newValue, - ColumnIndex = cell.Column, - RowIndex = cell.Row - }; - undoManager.PushUndoItem(undoItem); - } - } - } - } - } - } - } -} diff --git a/NavfertyExcelAddIn/Commons/StringExtensions.cs b/NavfertyExcelAddIn/Commons/StringExtensions.cs index 3a577c4..081d20f 100644 --- a/NavfertyExcelAddIn/Commons/StringExtensions.cs +++ b/NavfertyExcelAddIn/Commons/StringExtensions.cs @@ -1,4 +1,6 @@ -using System.Text.RegularExpressions; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text.RegularExpressions; using NLog; @@ -9,12 +11,13 @@ public static class StringExtensions private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); private static readonly Regex spacesRegex = new Regex("\\s+", RegexOptions.None); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static string TrimSpaces(this string value) { if (string.IsNullOrWhiteSpace(value)) - return null; - - // replace any single or multiple space chars with single space + return null; + + // replace any single or multiple space chars with single space var newValue = spacesRegex.Replace(value, " "); newValue = string.IsNullOrEmpty(newValue) @@ -22,6 +25,11 @@ public static string TrimSpaces(this string value) : newValue.Trim(); return newValue; - } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CountChars(this string value, char c) + => value.Count(x => x == c); + } } diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index 4d89ac0..af88756 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -6,6 +6,8 @@ using System.Text; using System.Text.RegularExpressions; +using NavfertyExcelAddIn.Commons; + namespace NavfertyExcelAddIn.ParseNumerics { /// @@ -13,9 +15,9 @@ namespace NavfertyExcelAddIn.ParseNumerics /// public static class DecimalParser { - private static readonly Regex SpacesPattern = new Regex(@"\s"); - private static readonly Regex DecimalPattern = new Regex(@"[\d\.\,\s]+"); - private static readonly Regex ExponentPattern = new Regex(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); + private static readonly Regex SpacesPattern = new(@"\s"); + private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); + private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); public static NumericParseResult? ParseDecimal(this string value) { @@ -33,16 +35,17 @@ public static class DecimalParser if (!DecimalPattern.IsMatch(value)) { - return null; + return null;//Doesn't look like a number at all. } + //Determine the decimal separator . or , if (v.Contains(",") && v.Contains(".")) { var last = v.LastIndexOfAny(new[] { ',', '.' }); var c = v[last]; return v.CountChars(c) == 1 ? v.TryParse(c == '.' ? Format.Dot : Format.Comma) - : (NumericParseResult?)null; + : null; } if (v.Contains(",")) @@ -62,19 +65,16 @@ public static class DecimalParser return v.TryParse(Format.Dot); } - private static int CountChars(this string value, char c) - { - return value.Count(x => x == c); - } - private static decimal? TryParseExponent(this string value) + + private static double? TryParseExponent(this string value) { - return decimal.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal result) + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) ? result - : (decimal?)null; + : null; } - private static Lazy _allCurrencySymbolsCacheLazy = new Lazy(() + private static Lazy _allCurrencySymbolsCacheLazy = new(() => CultureInfo.GetCultures(CultureTypes.AllCultures) .Select(ci => ci.NumberFormat.CurrencySymbol) .Distinct() @@ -101,7 +101,7 @@ private static int CountChars(this string value, char c) //formatInfo.CurrencyNegativePattern = 8; //formatInfo.CurrencyPositivePattern = 3; - var valueParsed = decimal.TryParse(value, NumberStyles.Currency, formatInfo, out decimal result); + var valueParsed = double.TryParse(value, NumberStyles.Currency, formatInfo, out double result); if (valueParsed) return new NumericParseResult(result);//Parsed without our help //decimal.TryParse не может разобрать строку со значком любой валюты, кроме валюты текущей культуры, @@ -109,21 +109,21 @@ private static int CountChars(this string value, char c) //Поэтому помогаем руками разобрать строку с произвольной валютой... //detect how many currency symbols contains source string... - var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur)).ToArray(); + var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur));//.ToArray(); if (currenciesInValue.Count() == 1)// TODO: Если строка содержит несколько разных символов валют, не преобразовываем, т.к. приоритет валют не ясен { var curSymb = currenciesInValue.First(); //Remove found currencySymbol from source string var valueWithoutCurrencySymbol = value.Replace(curSymb, string.Empty); - valueParsed = decimal.TryParse(valueWithoutCurrencySymbol, NumberStyles.Currency, formatInfo, out result); + valueParsed = double.TryParse(valueWithoutCurrencySymbol, NumberStyles.Currency, formatInfo, out result); if (valueParsed) { - //System.Windows.Forms.MessageBox.Show($"Parsed value: '{value}, valueWithoutCurrencySymbol: {valueWithoutCurrencySymbol}', result: {result}, currency: {curSymb}"); + //Debug.WriteLine($"Parsed value: '{value}, valueWithoutCurrencySymbol: {valueWithoutCurrencySymbol}', result: {result}, currency: {curSymb}"); return new NumericParseResult(result, curSymb); } //It was not possible to parse the line, even after removing the currencySymbol, most likely this is not about money at all... } - return (NumericParseResult?)null;//Not found any currency symbols, or found more than one, or even not number... + return null;//Not found any currency symbols, or found more than one, or even not number... } private enum Format diff --git a/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs b/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs index e9d5272..7ea89dd 100644 --- a/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs +++ b/NavfertyExcelAddIn/ParseNumerics/NumericParseResult.cs @@ -12,10 +12,10 @@ public struct NumericParseResult private static readonly string currencySymbolFromOSUserLocale = CultureInfo.CurrentCulture.NumberFormat.CurrencySymbol; private static readonly string currencySymbolRu = CultureInfo.GetCultureInfo("ru").NumberFormat.CurrencySymbol; - public readonly decimal? ConvertedValue; - public readonly string Currency; + public readonly double? ConvertedValue = null; + public readonly string? Currency = null; - public NumericParseResult(decimal? value, string curr = null) + public NumericParseResult(double? value, string? curr = null) { ConvertedValue = value; Currency = curr; From acf4b23deeaaed48a31e59aa65246417ebe44e9b Mon Sep 17 00:00:00 2001 From: uom Date: Fri, 14 Jan 2022 00:55:58 +0700 Subject: [PATCH 03/10] iss 29 otimization and nullSafening --- .../Commons/EnumerableExtensions.cs | 95 ++++++++++--------- .../ParseNumerics/NumericParser.cs | 10 ++ 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs index b89a251..e70a145 100644 --- a/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs +++ b/NavfertyExcelAddIn/Commons/EnumerableExtensions.cs @@ -18,45 +18,55 @@ public static class EnumerableExtensions private static readonly UndoManager undoManager = NavfertyRibbon.Container.Resolve(); - private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); - + private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); + + /// null-safe ForEach [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ForEach(this IEnumerable source, Action action) + public static void ForEach(this IEnumerable? source, Action? action) { - foreach (T element in source) + foreach (T element in source.OrEmptyIfNull()) { - action(element); + action?.Invoke(element); } - } - + } + + /// Null-safe plug for loops + /// If source != null, return source. If source == null, returns empty Enumerable, without null reference exception + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static IEnumerable OrEmptyIfNull(this IEnumerable source) + => source ?? Enumerable.Empty(); + + /// null-safe ForEachCell [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ForEachCell(this Range? range, Action action) + public static void ForEachCell(this Range? range, Action? action) { // TODO rewrite to use less read-write calls to interop (like Range.Value) (may be use try/finally with selection.Worksheet.EnableCalculation = false/true?; range?.Cast().ForEach(action); - } - + } + + /// null-safe ApplyForEachCellOfType [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ApplyForEachCellOfType(this Range range, Func transform) + public static void ApplyForEachCellOfType(this Range? range, Func? transform) { + if (range == null || transform == null) return; logger.Debug($"Apply transformation to range '{range.GetRelativeAddress()}' on worksheet '{range.Worksheet.Name}'"); - undoManager.StartNewAction(range); foreach (Range area in range.Areas) { ApplyToArea(area, transform); } - } - + } + + /// null-safe ApplyToArea [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ApplyToArea(Range range, Func transform) + private static void ApplyToArea(Range? range, Func transform) { - var rangeValue = range.Value; + var rangeValue = range?.Value; if (rangeValue is null) return; - if (rangeValue is TIn currentValue) + if (rangeValue is TIn currentValue)//single cell { var newValue = transform(currentValue); range.Value = newValue; @@ -73,8 +83,10 @@ private static void ApplyToArea(Range range, Func transfor // minimize number of COM calls to excel if (!(rangeValue is object[,] values)) - return; - + return; + + //area of cells + int upperI = values.GetUpperBound(0); // Rows int upperJ = values.GetUpperBound(1); // Columns @@ -117,10 +129,12 @@ private static void ApplyToArea(Range range, Func transfor } } - /// Allow acces to Range object from transform func + /// null-safe ApplyForEachCellOfType, allow acces to Range object from transform func [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void ApplyForEachCellOfType2(this Range range, Func transform) + public static void ApplyForEachCellOfType2(this Range? range, Func? transform) { + if (range == null || transform == null) return; + logger.Debug($"Apply transformation to range '{range.GetRelativeAddress()}' on worksheet '{range.Worksheet.Name}'"); undoManager.StartNewAction(range); @@ -132,36 +146,29 @@ public static void ApplyForEachCellOfType2(this Range range, FuncAllow acces to Range object from transform func may be slower than Old + /// null-safe ApplyToArea, allow acces to Range object from transform func may be slower than Old [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static void ApplyToArea2(Range area, Func transform) + private static void ApplyToArea2(Range? area, Func transform) { //try { if (null == range || null == range.Cells) return; } catch { return; }//TODO: Just for Test cases, remove catch and modify tests (range.Cells) area?.Cells?.ForEachCell(cell => { - { - var cellValue = cell.Value; - if ((cellValue is null) || (cellValue is not TIn currentValue)) return; - - // TODO transform func may change format of cell, and we need to allow undo this, but set/restore cell formating has so weird api... - var newValue = transform(currentValue, cell); - if (newValue == null) return; + var cellValue = cell.Value; + if ((cellValue is null) || (cellValue is not TIn currentValue)) return; - if (!newValue.Equals(currentValue)) - { - cell.Value = newValue; - var undoItem = new UndoItem - { - OldValue = currentValue, - NewValue = newValue, - ColumnIndex = cell.Column, - RowIndex = cell.Row - }; - undoManager.PushUndoItem(undoItem); - } - } + // TODO transform func may change format of cell, and we need to allow undo this, but set/restore cell formating has so weird api... + var newValue = transform(currentValue, cell); + if (null == newValue || newValue.Equals(currentValue)) return;//value did not changed or not parsed + cell.Value = newValue; + var undoItem = new UndoItem + { + OldValue = currentValue, + NewValue = newValue, + ColumnIndex = cell.Column, + RowIndex = cell.Row + }; + undoManager.PushUndoItem(undoItem); }); - } } } diff --git a/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs b/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs index 108d573..145cc63 100644 --- a/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/NumericParser.cs @@ -19,6 +19,11 @@ public void Parse(Range selection) bool autoCalcEnabled = false; try { autoCalcEnabled = selection.Worksheet.EnableCalculation; } catch { } if (autoCalcEnabled) selection.Worksheet.EnableCalculation = false; + +#if DEBUG + var sw = new Stopwatch(); + sw.Start(); +#endif try { @@ -42,6 +47,11 @@ public void Parse(Range selection) } finally { +#if DEBUG + sw.Stop(); + Debug.WriteLine($"NumericParser.Parse() {sw.Elapsed.TotalMilliseconds}ms"); +#endif + if (autoCalcEnabled) selection.Worksheet.EnableCalculation = autoCalcEnabled;//Restart sheet autorecalc } } From 4364c15c5fc935dd415e078ab451296a156943c0 Mon Sep 17 00:00:00 2001 From: uom Date: Fri, 14 Jan 2022 01:28:33 +0700 Subject: [PATCH 04/10] iss 29 add unittest to DecimalParserTests --- .../ParseNumerics/DecimalParserTests.cs | 78 +++++++++++++------ .../ParseNumerics/NumericParserTests.cs | 16 +++- 2 files changed, 68 insertions(+), 26 deletions(-) diff --git a/NavfertyExcelAddIn.UnitTests/ParseNumerics/DecimalParserTests.cs b/NavfertyExcelAddIn.UnitTests/ParseNumerics/DecimalParserTests.cs index 939df4e..67ad989 100644 --- a/NavfertyExcelAddIn.UnitTests/ParseNumerics/DecimalParserTests.cs +++ b/NavfertyExcelAddIn.UnitTests/ParseNumerics/DecimalParserTests.cs @@ -9,27 +9,45 @@ namespace NavfertyExcelAddIn.UnitTests.ParseNumerics [TestClass] public class DecimalParserTests { - [DataRow("0", 0)] - [DataRow("123", 123)] - [DataRow("1.1", 1.1)] - [DataRow("1,1", 1.1)] - [DataRow("1 000", 1000)] - [DataRow("12 00 00 . 12", 120000.12)] - [DataRow("0.1", 0.1)] - [DataRow(".123", 0.123)] - [DataRow("10.000.000", 10000000)] - [DataRow("10.000,12", 10000.12)] - [DataRow("12,345.12", 12345.12)] - [DataRow("12,345,000", 12345000)] - [DataRow("1E6", 1000000)] - [DataRow("1.2E3", 1200)] - [DataRow("-1.3E-5", -0.000013)] + [DataRow("0", 0, null)] + [DataRow("123", 123, null)] + [DataRow("1.1", 1.1, null)] + [DataRow("1,1", 1.1, null)] + [DataRow("1 000", 1000, null)] + [DataRow("12 00 00 . 12", 120000.12, null)] + [DataRow("0.1", 0.1, null)] + [DataRow(".123", 0.123, null)] + [DataRow("10.000.000", 10000000, null)] + [DataRow("10.000,12", 10000.12, null)] + [DataRow("12,345.12", 12345.12, null)] + [DataRow("12,345,000", 12345000, null)] + [DataRow("1E6", 1000000, null)] + [DataRow("1.2E3", 1200, null)] + [DataRow("-1.3E-5", -0.000013, null)] + [DataRow("$5666", 5666, "$")] + [DataRow("5666$", 5666, "$")] + [DataRow("$56.66", 56.66, "$")] + [DataRow("56.66$", 56.66, "$")] + [DataRow("$56,66", 56.66, "$")] + [DataRow("56,66$", 56.66, "$")] + [DataRow("₽5666", 5666, "₽")] + [DataRow("5666₽", 5666, "₽")] + [DataRow("₽56.66", 56.66, "₽")] + [DataRow("56.66₽", 56.66, "₽")] + [DataRow("₽56,66", 56.66, "₽")] + [DataRow("56,66₽", 56.66, "₽")] + [DataRow("£5666", 5666, "£")] + [DataRow("5666£", 5666, "£")] + [DataRow("£56.66", 56.66, "£")] + [DataRow("56.66£", 56.66, "£")] + [DataRow("£56.66", 56.66, "£")] + [DataRow("56.66£", 56.66, "£")] [TestMethod] - public void ParseDecimal_Success(string sourceValue, double targetDoubleValue) - { - // DataRow can't into decimals - they are not primitive type - var targetValue = Convert.ToDecimal(targetDoubleValue); - + public void ParseDecimal_Success(string sourceValue, double targetDoubleValue, string currencySymbol) + { + var targetValue = new NumericParseResult(targetDoubleValue, currencySymbol); + // DataRow can't into decimals - they are not primitive type + //var targetValue = Convert.ToDecimal(targetDoubleValue); Assert.AreEqual(targetValue, sourceValue.ParseDecimal()); } @@ -37,9 +55,25 @@ public void ParseDecimal_Success(string sourceValue, double targetDoubleValue) public void ParseDecimal_MissingValue() { var input = "no value"; - - Assert.AreEqual(null, input.ParseDecimal()); + Assert.AreEqual(null, input.ParseDecimal()); } + [TestMethod] + public void ParseDecimal_UnknownCurrency() + { + var input = "56.66%"; + Assert.AreEqual(null, input.ParseDecimal()); + } + + [TestMethod] + public void ParseDecimal_MultipleCurrencyInOneString() + { + Assert.AreEqual(null, "56.66£₽".ParseDecimal()); + Assert.AreEqual(null, "56.66₽£".ParseDecimal()); + Assert.AreEqual(null, "₽£56.66".ParseDecimal()); + } + + + } } diff --git a/NavfertyExcelAddIn.UnitTests/ParseNumerics/NumericParserTests.cs b/NavfertyExcelAddIn.UnitTests/ParseNumerics/NumericParserTests.cs index dfdfa30..7665f6e 100644 --- a/NavfertyExcelAddIn.UnitTests/ParseNumerics/NumericParserTests.cs +++ b/NavfertyExcelAddIn.UnitTests/ParseNumerics/NumericParserTests.cs @@ -25,7 +25,7 @@ public void BeforeEachTest() } [TestMethod] - public void ParsedSuccessfully() // TODO naming + public void ParsedSuccessfully() // TODO naming { var selection = new Mock(MockBehavior.Strict); var values = new object[,] { { 1, "1", "abc" }, { "123.123", "321 , 321", null } }; @@ -46,9 +46,17 @@ public void ParsedSuccessfully() // TODO naming ws.Setup(x => x.Parent).Returns(wb.Object); var areas = new Mock(MockBehavior.Strict); - areas.Setup(x => x.GetEnumerator()).Returns(new[] { selection.Object }.GetEnumerator()); - selection.Setup(x => x.Areas).Returns(areas.Object); - + areas.Setup(x => x.GetEnumerator()).Returns(new[] { selection.Object }.GetEnumerator()); + selection.Setup(x => x.Areas).Returns(areas.Object); + + + var cells = new Mock(MockBehavior.Default); + cells.Setup(x => x.GetEnumerator()).Returns(new[] { selection.Object }.GetEnumerator()); + //selection.SetupSet(x => x.get_Cells = It.Is(z => VerifyParsed(z))); + //selection.Setup(x => x.Cells).Returns(cells.Object.GetEnumerator); + + //areas.Setup(x => x.ce .Cells).Returns(cells.Object); + NumericParser.Parse(selection.Object); } From a9d5f5b11d24fd6c94d6e315b89cb6e405c8b756 Mon Sep 17 00:00:00 2001 From: uom Date: Sat, 15 Jan 2022 06:04:28 +0700 Subject: [PATCH 05/10] iss29 comments to English --- NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index af88756..e4e58fc 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -104,13 +104,12 @@ public static class DecimalParser var valueParsed = double.TryParse(value, NumberStyles.Currency, formatInfo, out double result); if (valueParsed) return new NumericParseResult(result);//Parsed without our help - //decimal.TryParse не может разобрать строку со значком любой валюты, кроме валюты текущей культуры, - //и символ валюты должен располагаться в правильном месте (как требуется в конкретной культуре)!!! - //Поэтому помогаем руками разобрать строку с произвольной валютой... + //.TryParse cannot parse a string with a CurrencySymbol and number format not from the current user culture + //Therefore, we help to parse the string with an arbitrary currency manualy... - //detect how many currency symbols contains source string... + //detect how many different currency symbols contains source string. var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur));//.ToArray(); - if (currenciesInValue.Count() == 1)// TODO: Если строка содержит несколько разных символов валют, не преобразовываем, т.к. приоритет валют не ясен + if (currenciesInValue.Count() == 1)// TODO: If the string contains several different currency symbols, do not convert, because currency priority is not clear { var curSymb = currenciesInValue.First(); //Remove found currencySymbol from source string From 86aab27d95be59605c7b318f1f63f843d99168da Mon Sep 17 00:00:00 2001 From: uom Date: Mon, 17 Jan 2022 02:39:31 +0700 Subject: [PATCH 06/10] add optimized RemoveSpaces String Extensions --- NavfertyExcelAddIn/Commons/StringExtensions.cs | 12 +++++++++++- NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs | 6 ++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/NavfertyExcelAddIn/Commons/StringExtensions.cs b/NavfertyExcelAddIn/Commons/StringExtensions.cs index 081d20f..a33e0ff 100644 --- a/NavfertyExcelAddIn/Commons/StringExtensions.cs +++ b/NavfertyExcelAddIn/Commons/StringExtensions.cs @@ -29,7 +29,17 @@ public static string TrimSpaces(this string value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CountChars(this string value, char c) - => value.Count(x => x == c); + => value.Count(x => x == c); + + /// Removes only ASCII Space char (0x32) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string RemoveSpacesFast(this string source) + => source.Replace(" ", string.Empty); + + /// Removes all Unicode character which is categorized as white space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string RemoveSpacesEx(this string source) + => string.Concat(source.Where(c => !char.IsWhiteSpace(c))); } } diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index e4e58fc..8e2393e 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -15,7 +15,7 @@ namespace NavfertyExcelAddIn.ParseNumerics /// public static class DecimalParser { - private static readonly Regex SpacesPattern = new(@"\s"); + //private static readonly Regex SpacesPattern = new(@"\s"); private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); @@ -26,7 +26,7 @@ public static class DecimalParser return null; } - var v = SpacesPattern.Replace(value, match => string.Empty); + var v = value.RemoveSpacesEx(); if (ExponentPattern.IsMatch(v)) { @@ -65,8 +65,6 @@ public static class DecimalParser return v.TryParse(Format.Dot); } - - private static double? TryParseExponent(this string value) { return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) From df90a71fb370ac29a323fc402036324307129a29 Mon Sep 17 00:00:00 2001 From: uom Date: Mon, 17 Jan 2022 02:39:31 +0700 Subject: [PATCH 07/10] add optimized RemoveSpaces String Extensions --- NavfertyExcelAddIn/Commons/StringExtensions.cs | 12 +++++++++++- NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs | 5 +---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/NavfertyExcelAddIn/Commons/StringExtensions.cs b/NavfertyExcelAddIn/Commons/StringExtensions.cs index 081d20f..a33e0ff 100644 --- a/NavfertyExcelAddIn/Commons/StringExtensions.cs +++ b/NavfertyExcelAddIn/Commons/StringExtensions.cs @@ -29,7 +29,17 @@ public static string TrimSpaces(this string value) [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int CountChars(this string value, char c) - => value.Count(x => x == c); + => value.Count(x => x == c); + + /// Removes only ASCII Space char (0x32) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string RemoveSpacesFast(this string source) + => source.Replace(" ", string.Empty); + + /// Removes all Unicode character which is categorized as white space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static string RemoveSpacesEx(this string source) + => string.Concat(source.Where(c => !char.IsWhiteSpace(c))); } } diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index e4e58fc..00fd334 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -15,7 +15,6 @@ namespace NavfertyExcelAddIn.ParseNumerics /// public static class DecimalParser { - private static readonly Regex SpacesPattern = new(@"\s"); private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); @@ -26,7 +25,7 @@ public static class DecimalParser return null; } - var v = SpacesPattern.Replace(value, match => string.Empty); + var v = value.RemoveSpacesEx(); if (ExponentPattern.IsMatch(v)) { @@ -65,8 +64,6 @@ public static class DecimalParser return v.TryParse(Format.Dot); } - - private static double? TryParseExponent(this string value) { return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) From c3b9b87d192af1fee40ed4bba8176b8dfb5b5dd4 Mon Sep 17 00:00:00 2001 From: uom Date: Sun, 6 Feb 2022 18:57:18 +0700 Subject: [PATCH 08/10] fix merge errors --- NavfertyExcelAddIn/NavfertyExcelAddIn.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj b/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj index be280f2..3269ff1 100644 --- a/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj +++ b/NavfertyExcelAddIn/NavfertyExcelAddIn.csproj @@ -240,6 +240,7 @@ + From 623f1ef91664941c46df10279021d637a97343fb Mon Sep 17 00:00:00 2001 From: uom Date: Sun, 6 Feb 2022 19:03:01 +0700 Subject: [PATCH 09/10] fixed RemoveSpacesEx lost after merging --- .../Extensions/StringExtensions.cs | 26 +- .../ParseNumerics/DecimalParser.cs | 243 +++++++++--------- 2 files changed, 138 insertions(+), 131 deletions(-) diff --git a/Navferty.Common/Extensions/StringExtensions.cs b/Navferty.Common/Extensions/StringExtensions.cs index d63c277..5b14629 100644 --- a/Navferty.Common/Extensions/StringExtensions.cs +++ b/Navferty.Common/Extensions/StringExtensions.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using System.Text.RegularExpressions; @@ -6,6 +7,7 @@ namespace Navferty.Common { + [DebuggerStepThrough] public static class StringExtensions { //private static readonly ILogger logger = LogManager.GetCurrentClassLogger(); @@ -25,12 +27,20 @@ public static class StringExtensions : newValue.Trim(); return newValue; - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int CountChars(this string value, char c) - { - return value.Count(x => x == c); - } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static int CountChars(this string value, char c) + => value.Count(x => x == c); + + /// Removes only ASCII Space char (0x32) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string RemoveSpacesFast(this string source) + => source.Replace(" ", string.Empty); + + /// Removes all Unicode character which is categorized as white space. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string RemoveSpacesEx(this string source) + => string.Concat(source.Where(c => !char.IsWhiteSpace(c))); } } diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index 9bc3fb6..4f375e4 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -1,132 +1,129 @@ using System; -using System.Collections.Generic; -using System.Diagnostics; using System.Globalization; using System.Linq; -using System.Text; using System.Text.RegularExpressions; -using NavfertyExcelAddIn.Commons; +using Navferty.Common; namespace NavfertyExcelAddIn.ParseNumerics { - /// - /// Parser implemented in https://github.com/navferty/NumericParser - /// - public static class DecimalParser - { - private static readonly Regex SpacesPattern = new(@"\s"); - private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); - private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); - - public static NumericParseResult? ParseDecimal(this string value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return null; - } - - var v = value.RemoveSpacesEx(); - - if (ExponentPattern.IsMatch(v)) - { - return new NumericParseResult(v.TryParseExponent()); - } - - if (!DecimalPattern.IsMatch(value)) - { - return null;//Doesn't look like a number at all. - } - - //Determine the decimal separator . or , - if (v.Contains(",") && v.Contains(".")) - { - var last = v.LastIndexOfAny(new[] { ',', '.' }); - var c = v[last]; - return v.CountChars(c) == 1 - ? v.TryParse(c == '.' ? Format.Dot : Format.Comma) - : null; - } - - if (v.Contains(",")) - { - return v.CountChars(',') == 1 - ? v.TryParse(Format.Comma) - : v.TryParse(Format.Dot); - } - - if (v.Contains(".")) - { - return v.CountChars('.') == 1 - ? v.TryParse(Format.Dot) - : v.TryParse(Format.Comma); - } - - return v.TryParse(Format.Dot); - } - - private static double? TryParseExponent(this string value) - { - return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) - ? result - : null; - } - - private static Lazy _allCurrencySymbolsCacheLazy = new(() - => CultureInfo.GetCultures(CultureTypes.AllCultures) - .Select(ci => ci.NumberFormat.CurrencySymbol) - .Distinct() - .Where(cur => !string.IsNullOrWhiteSpace(cur)) - .ToArray()); - - private static NumericParseResult? TryParse(this string value, Format info) - { - var formatInfo = (NumberFormatInfo)NumberFormatInfo.InvariantInfo.Clone(); - - if (info == Format.Comma) - { - formatInfo.CurrencyDecimalSeparator = ","; - formatInfo.CurrencyGroupSeparator = "."; - formatInfo.NumberDecimalSeparator = ","; - formatInfo.NumberGroupSeparator = "."; - } - else - { - formatInfo.CurrencyDecimalSeparator = "."; - formatInfo.CurrencyGroupSeparator = ","; - } - // добавить тест-кейсов на формат валют - //formatInfo.CurrencyNegativePattern = 8; - //formatInfo.CurrencyPositivePattern = 3; - - var valueParsed = double.TryParse(value, NumberStyles.Currency, formatInfo, out double result); - if (valueParsed) return new NumericParseResult(result);//Parsed without our help - - //.TryParse cannot parse a string with a CurrencySymbol and number format not from the current user culture - //Therefore, we help to parse the string with an arbitrary currency manualy... - - //detect how many different currency symbols contains source string. - var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur));//.ToArray(); - if (currenciesInValue.Count() == 1)// TODO: If the string contains several different currency symbols, do not convert, because currency priority is not clear - { - var curSymb = currenciesInValue.First(); - //Remove found currencySymbol from source string - var valueWithoutCurrencySymbol = value.Replace(curSymb, string.Empty); - valueParsed = double.TryParse(valueWithoutCurrencySymbol, NumberStyles.Currency, formatInfo, out result); - if (valueParsed) - { - //Debug.WriteLine($"Parsed value: '{value}, valueWithoutCurrencySymbol: {valueWithoutCurrencySymbol}', result: {result}, currency: {curSymb}"); - return new NumericParseResult(result, curSymb); - } - //It was not possible to parse the line, even after removing the currencySymbol, most likely this is not about money at all... - } - return null;//Not found any currency symbols, or found more than one, or even not number... - } - - private enum Format - { - Dot, - Comma - } - } + /// + /// Parser implemented in https://github.com/navferty/NumericParser + /// + public static class DecimalParser + { + private static readonly Regex SpacesPattern = new(@"\s"); + private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); + private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); + + public static NumericParseResult? ParseDecimal(this string value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return null; + } + + var v = value.RemoveSpacesEx(); + + if (ExponentPattern.IsMatch(v)) + { + return new NumericParseResult(v.TryParseExponent()); + } + + if (!DecimalPattern.IsMatch(value)) + { + return null;//Doesn't look like a number at all. + } + + //Determine the decimal separator . or , + if (v.Contains(",") && v.Contains(".")) + { + var last = v.LastIndexOfAny(new[] { ',', '.' }); + var c = v[last]; + return v.CountChars(c) == 1 + ? v.TryParse(c == '.' ? Format.Dot : Format.Comma) + : null; + } + + if (v.Contains(",")) + { + return v.CountChars(',') == 1 + ? v.TryParse(Format.Comma) + : v.TryParse(Format.Dot); + } + + if (v.Contains(".")) + { + return v.CountChars('.') == 1 + ? v.TryParse(Format.Dot) + : v.TryParse(Format.Comma); + } + + return v.TryParse(Format.Dot); + } + + private static double? TryParseExponent(this string value) + { + return double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double result) + ? result + : null; + } + + private static Lazy _allCurrencySymbolsCacheLazy = new(() + => CultureInfo.GetCultures(CultureTypes.AllCultures) + .Select(ci => ci.NumberFormat.CurrencySymbol) + .Distinct() + .Where(cur => !string.IsNullOrWhiteSpace(cur)) + .ToArray()); + + private static NumericParseResult? TryParse(this string value, Format info) + { + var formatInfo = (NumberFormatInfo)NumberFormatInfo.InvariantInfo.Clone(); + + if (info == Format.Comma) + { + formatInfo.CurrencyDecimalSeparator = ","; + formatInfo.CurrencyGroupSeparator = "."; + formatInfo.NumberDecimalSeparator = ","; + formatInfo.NumberGroupSeparator = "."; + } + else + { + formatInfo.CurrencyDecimalSeparator = "."; + formatInfo.CurrencyGroupSeparator = ","; + } + // добавить тест-кейсов на формат валют + //formatInfo.CurrencyNegativePattern = 8; + //formatInfo.CurrencyPositivePattern = 3; + + var valueParsed = double.TryParse(value, NumberStyles.Currency, formatInfo, out double result); + if (valueParsed) return new NumericParseResult(result);//Parsed without our help + + //.TryParse cannot parse a string with a CurrencySymbol and number format not from the current user culture + //Therefore, we help to parse the string with an arbitrary currency manualy... + + //detect how many different currency symbols contains source string. + var currenciesInValue = _allCurrencySymbolsCacheLazy.Value.Where(cur => value.Contains(cur));//.ToArray(); + if (currenciesInValue.Count() == 1)// TODO: If the string contains several different currency symbols, do not convert, because currency priority is not clear + { + var curSymb = currenciesInValue.First(); + //Remove found currencySymbol from source string + var valueWithoutCurrencySymbol = value.Replace(curSymb, string.Empty); + valueParsed = double.TryParse(valueWithoutCurrencySymbol, NumberStyles.Currency, formatInfo, out result); + if (valueParsed) + { + //Debug.WriteLine($"Parsed value: '{value}, valueWithoutCurrencySymbol: {valueWithoutCurrencySymbol}', result: {result}, currency: {curSymb}"); + return new NumericParseResult(result, curSymb); + } + //It was not possible to parse the line, even after removing the currencySymbol, most likely this is not about money at all... + } + return null;//Not found any currency symbols, or found more than one, or even not number... + } + + private enum Format + { + Dot, + Comma + } + } } From 9a57f53628002351abb4b5b4716560d197ca04c9 Mon Sep 17 00:00:00 2001 From: uom Date: Mon, 7 Feb 2022 01:21:37 +0700 Subject: [PATCH 10/10] merge fixing --- NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs index 4f375e4..d0de650 100644 --- a/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs +++ b/NavfertyExcelAddIn/ParseNumerics/DecimalParser.cs @@ -12,7 +12,7 @@ namespace NavfertyExcelAddIn.ParseNumerics /// public static class DecimalParser { - private static readonly Regex SpacesPattern = new(@"\s"); + //private static readonly Regex SpacesPattern = new(@"\s"); private static readonly Regex DecimalPattern = new(@"[\d\.\,\s]+"); private static readonly Regex ExponentPattern = new(@"[-+]?\d*\.?\d+[eE][-+]?\d+"); @@ -26,14 +26,10 @@ public static class DecimalParser var v = value.RemoveSpacesEx(); if (ExponentPattern.IsMatch(v)) - { return new NumericParseResult(v.TryParseExponent()); - } if (!DecimalPattern.IsMatch(value)) - { return null;//Doesn't look like a number at all. - } //Determine the decimal separator . or , if (v.Contains(",") && v.Contains("."))