diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs index a61e38e6..2c5639ee 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs @@ -217,12 +217,17 @@ private Tuple GetCellValue(int rowIndex, int cellIndex, if (TypeHelper.IsNumericType(type)) { - var dataType = _configuration.Culture == CultureInfo.InvariantCulture ? "n" : "str"; - string cellValue; + var cellValue = GetNumericValue(value, type); - cellValue = GetNumericValue(value, type); - - return Tuple.Create("2", dataType, cellValue); + if (columnInfo == null || columnInfo.ExcelFormat == null) + { + var dataType = _configuration.Culture == CultureInfo.InvariantCulture ? "n" : "str"; + return Tuple.Create("2", dataType, cellValue); + } + else + { + return Tuple.Create(columnInfo.ExcelFormatId.ToString(), (string)null, cellValue); + } } if (type == typeof(bool)) diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleBuildContext.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleBuildContext.cs index 00b8af8d..f8612142 100644 --- a/src/MiniExcel/OpenXml/Styles/SheetStyleBuildContext.cs +++ b/src/MiniExcel/OpenXml/Styles/SheetStyleBuildContext.cs @@ -127,7 +127,7 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos) NewXmlWriter = XmlWriter.Create(newXmlWriterStream, new XmlWriterSettings() { Indent = true, Encoding = _encoding, Async = true }); GenerateElementInfos = generateElementInfos; - ColumnsToApply = SheetStyleBuilderHelper.GenerateStyleIds(OldElementInfos.CellXfCount + generateElementInfos.CellXfCount, _columns); + ColumnsToApply = SheetStyleBuilderHelper.GenerateStyleIds(OldElementInfos.CellXfCount + generateElementInfos.CellXfCount, _columns).ToArray();//ToArray to avoid multiple calculations, if there is a performance problem then consider optimizing the CustomFormatCount = ColumnsToApply.Count(); initialized = true; diff --git a/src/MiniExcel/Utils/ExcelNumberFormat.cs b/src/MiniExcel/Utils/ExcelNumberFormat.cs index 27fec513..877e3486 100644 --- a/src/MiniExcel/Utils/ExcelNumberFormat.cs +++ b/src/MiniExcel/Utils/ExcelNumberFormat.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Text; namespace MiniExcelLibs.Utils @@ -55,6 +56,7 @@ public ExcelNumberFormat(string formatString) internal List
Sections { get; } } + internal static class Evaluator { public static Section GetSection(List
sections, object value) @@ -112,10 +114,14 @@ private static Section GetNumericSection(List
sections, double value) internal enum SectionType { General, + Number, + Fraction, + Exponential, Date, Duration, Text } + internal class Section { public int SectionIndex { get; set; } @@ -125,6 +131,362 @@ internal class Section public List GeneralTextDateDurationParts { get; set; } } + internal class FractionSection + { + public List IntegerPart { get; set; } + + public List Numerator { get; set; } + + public List DenominatorPrefix { get; set; } + + public List Denominator { get; set; } + + public int DenominatorConstant { get; set; } + + public List DenominatorSuffix { get; set; } + + public List FractionSuffix { get; set; } + + static public bool TryParse(List tokens, out FractionSection format) + { + List numeratorParts = null; + List denominatorParts = null; + + for (var i = 0; i < tokens.Count; i++) + { + var part = tokens[i]; + if (part == "/") + { + numeratorParts = tokens.GetRange(0, i); + i++; + denominatorParts = tokens.GetRange(i, tokens.Count - i); + break; + } + } + + if (numeratorParts == null) + { + format = null; + return false; + } + + GetNumerator(numeratorParts, out var integerPart, out var numeratorPart); + + if (!TryGetDenominator(denominatorParts, out var denominatorPrefix, out var denominatorPart, out var denominatorConstant, out var denominatorSuffix, out var fractionSuffix)) + { + format = null; + return false; + } + + format = new FractionSection() + { + IntegerPart = integerPart, + Numerator = numeratorPart, + DenominatorPrefix = denominatorPrefix, + Denominator = denominatorPart, + DenominatorConstant = denominatorConstant, + DenominatorSuffix = denominatorSuffix, + FractionSuffix = fractionSuffix + }; + + return true; + } + + static void GetNumerator(List tokens, out List integerPart, out List numeratorPart) + { + var hasPlaceholder = false; + var hasSpace = false; + var hasIntegerPart = false; + var numeratorIndex = -1; + var index = tokens.Count - 1; + while (index >= 0) + { + var token = tokens[index]; + if (Token.IsPlaceholder(token)) + { + hasPlaceholder = true; + + if (hasSpace) + { + hasIntegerPart = true; + break; + } + } + else + { + if (hasPlaceholder && !hasSpace) + { + // First time we get here marks the end of the integer part + hasSpace = true; + numeratorIndex = index + 1; + } + } + index--; + } + + if (hasIntegerPart) + { + integerPart = tokens.GetRange(0, numeratorIndex); + numeratorPart = tokens.GetRange(numeratorIndex, tokens.Count - numeratorIndex); + } + else + { + integerPart = null; + numeratorPart = tokens; + } + } + + static bool TryGetDenominator(List tokens, out List denominatorPrefix, out List denominatorPart, out int denominatorConstant, out List denominatorSuffix, out List fractionSuffix) + { + var index = 0; + var hasPlaceholder = false; + var hasConstant = false; + + var constant = new StringBuilder(); + + // Read literals until the first number placeholder or digit + while (index < tokens.Count) + { + var token = tokens[index]; + if (Token.IsPlaceholder(token)) + { + hasPlaceholder = true; + break; + } + else + if (Token.IsDigit19(token)) + { + hasConstant = true; + break; + } + index++; + } + + if (!hasPlaceholder && !hasConstant) + { + denominatorPrefix = null; + denominatorPart = null; + denominatorConstant = 0; + denominatorSuffix = null; + fractionSuffix = null; + return false; + } + + // The denominator starts here, keep the index + var denominatorIndex = index; + + // Read placeholders or digits in sequence + while (index < tokens.Count) + { + var token = tokens[index]; + if (hasPlaceholder && Token.IsPlaceholder(token)) + { + ; // OK + } + else + if (hasConstant && (Token.IsDigit09(token))) + { + constant.Append(token); + } + else + { + break; + } + index++; + } + + // 'index' is now at the first token after the denominator placeholders. + // The remaining, if anything, is to be treated in one or two parts: + // Any ultimately terminating literals are considered the "Fraction suffix". + // Anything between the denominator and the fraction suffix is the "Denominator suffix". + // Placeholders in the denominator suffix are treated as insignificant zeros. + + // Scan backwards to determine the fraction suffix + int fractionSuffixIndex = tokens.Count; + while (fractionSuffixIndex > index) + { + var token = tokens[fractionSuffixIndex - 1]; + if (Token.IsPlaceholder(token)) + { + break; + } + + fractionSuffixIndex--; + } + + // Finally extract the detected token ranges + + if (denominatorIndex > 0) + denominatorPrefix = tokens.GetRange(0, denominatorIndex); + else + denominatorPrefix = null; + + if (hasConstant) + denominatorConstant = int.Parse(constant.ToString()); + else + denominatorConstant = 0; + + denominatorPart = tokens.GetRange(denominatorIndex, index - denominatorIndex); + + if (index < fractionSuffixIndex) + denominatorSuffix = tokens.GetRange(index, fractionSuffixIndex - index); + else + denominatorSuffix = null; + + if (fractionSuffixIndex < tokens.Count) + fractionSuffix = tokens.GetRange(fractionSuffixIndex, tokens.Count - fractionSuffixIndex); + else + fractionSuffix = null; + + return true; + } + } + + internal class ExponentialSection + { + public List BeforeDecimal { get; set; } + + public bool DecimalSeparator { get; set; } + + public List AfterDecimal { get; set; } + + public string ExponentialToken { get; set; } + + public List Power { get; set; } + + public static bool TryParse(List tokens, out ExponentialSection format) + { + format = null; + + string exponentialToken; + + int partCount = Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal); + + if (partCount == 0) + return false; + + int position = partCount; + if (position < tokens.Count && Token.IsExponent(tokens[position])) + { + exponentialToken = tokens[position]; + position++; + } + else + { + return false; + } + + format = new ExponentialSection() + { + BeforeDecimal = beforeDecimal, + DecimalSeparator = decimalSeparator, + AfterDecimal = afterDecimal, + ExponentialToken = exponentialToken, + Power = tokens.GetRange(position, tokens.Count - position) + }; + + return true; + } + } + + internal class DecimalSection + { + public bool ThousandSeparator { get; set; } + + public double ThousandDivisor { get; set; } + + public double PercentMultiplier { get; set; } + + public List BeforeDecimal { get; set; } + + public bool DecimalSeparator { get; set; } + + public List AfterDecimal { get; set; } + + public static bool TryParse(List tokens, out DecimalSection format) + { + if (Parser.ParseNumberTokens(tokens, 0, out var beforeDecimal, out var decimalSeparator, out var afterDecimal) == tokens.Count) + { + bool thousandSeparator; + var divisor = GetTrailingCommasDivisor(tokens, out thousandSeparator); + var multiplier = GetPercentMultiplier(tokens); + + format = new DecimalSection() + { + BeforeDecimal = beforeDecimal, + DecimalSeparator = decimalSeparator, + AfterDecimal = afterDecimal, + PercentMultiplier = multiplier, + ThousandDivisor = divisor, + ThousandSeparator = thousandSeparator + }; + + return true; + } + + format = null; + return false; + } + + static double GetPercentMultiplier(List tokens) + { + // If there is a percentage literal in the part list, multiply the result by 100 + foreach (var token in tokens) + { + if (token == "%") + return 100; + } + + return 1; + } + + static double GetTrailingCommasDivisor(List tokens, out bool thousandSeparator) + { + // This parses all comma literals in the part list: + // Each comma after the last digit placeholder divides the result by 1000. + // If there are any other commas, display the result with thousand separators. + bool hasLastPlaceholder = false; + var divisor = 1.0; + + for (var j = 0; j < tokens.Count; j++) + { + var tokenIndex = tokens.Count - 1 - j; + var token = tokens[tokenIndex]; + + if (!hasLastPlaceholder) + { + if (Token.IsPlaceholder(token)) + { + // Each trailing comma multiplies the divisor by 1000 + for (var k = tokenIndex + 1; k < tokens.Count; k++) + { + token = tokens[k]; + if (token == ",") + divisor *= 1000.0; + else + break; + } + + // Continue scanning backwards from the last digit placeholder, + // but now look for a thousand separator comma + hasLastPlaceholder = true; + } + } + else + { + if (token == ",") + { + thousandSeparator = true; + return divisor; + } + } + } + + thousandSeparator = false; + return divisor; + } + } + /// /// Similar to regular .NET DateTime, but also supports 0/1 1900 and 29/2 1900. /// @@ -367,7 +729,10 @@ private static Section ParseSection(Tokenizer reader, int index, out bool syntax return null; } - SectionType type; + SectionType type; + FractionSection fraction = null; + ExponentialSection exponential = null; + DecimalSection number = null; List generalTextDateDuration = null; if (hasDateParts) @@ -393,6 +758,18 @@ private static Section ParseSection(Tokenizer reader, int index, out bool syntax type = SectionType.Text; generalTextDateDuration = tokens; } + else if (FractionSection.TryParse(tokens, out fraction)) + { + type = SectionType.Fraction; + } + else if (ExponentialSection.TryParse(tokens, out exponential)) + { + type = SectionType.Exponential; + } + else if (DecimalSection.TryParse(tokens, out number)) + { + type = SectionType.Number; + } else { // Unable to parse format string @@ -407,6 +784,7 @@ private static Section ParseSection(Tokenizer reader, int index, out bool syntax GeneralTextDateDurationParts = generalTextDateDuration }; } + internal static int ParseNumberTokens(List tokens, int startPosition, out List beforeDecimal, out bool decimalSeparator, out List afterDecimal) { beforeDecimal = null; @@ -655,6 +1033,13 @@ public bool ReadEnclosed(char open, char close) internal static class Token { + public static bool IsExponent(string token) + { + return + (string.Compare(token, "e+", StringComparison.OrdinalIgnoreCase) == 0) || + (string.Compare(token, "e-", StringComparison.OrdinalIgnoreCase) == 0); + } + public static bool IsLiteral(string token) { return