From 67e97f36b5ccc5ca9eb2adb1076dceeca1a555da Mon Sep 17 00:00:00 2001 From: meld-cp <18450687+meld-cp@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:32:04 +1200 Subject: [PATCH] Fix Issue 632, refactor sheet styles (#640) * fix for issue 606 * fix formatting * add test * refactor sheet styles, fix #632 * change tabs to spaces --- src/MiniExcel/OpenXml/Constants/ExcelXml.cs | 206 +----------------- .../ExcelOpenXmlSheetWriter.DefaultOpenXml.cs | 5 +- .../OpenXml/ExcelOpenXmlTemplate.Impl.cs | 72 +++--- src/MiniExcel/OpenXml/ExcelOpenXmlUtils.cs | 2 + .../Styles/DefaultSheetStyleBuilder.cs | 156 +++++++++++++ .../OpenXml/Styles/ISheetStyleBuilder.cs | 10 + .../Styles/MinimalSheetStyleBuilder.cs | 71 ++++++ .../OpenXml/Styles/SheetStyleBuilderHelper.cs | 24 ++ tests/MiniExcelTests/MiniExcelIssueTests.cs | 44 +++- .../MiniExcelOpenXmlAsyncTests.cs | 6 +- 10 files changed, 355 insertions(+), 241 deletions(-) create mode 100644 src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs create mode 100644 src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs create mode 100644 src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs create mode 100644 src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs diff --git a/src/MiniExcel/OpenXml/Constants/ExcelXml.cs b/src/MiniExcel/OpenXml/Constants/ExcelXml.cs index b1a0d7a..c238528 100644 --- a/src/MiniExcel/OpenXml/Constants/ExcelXml.cs +++ b/src/MiniExcel/OpenXml/Constants/ExcelXml.cs @@ -1,9 +1,4 @@ -using MiniExcelLibs.Attributes; -using MiniExcelLibs.OpenXml.Models; -using MiniExcelLibs.Utils; -using System.Collections.Generic; -using System.Linq; -using System.Text; +using MiniExcelLibs.OpenXml.Models; namespace MiniExcelLibs.OpenXml.Constants { @@ -11,15 +6,13 @@ namespace MiniExcelLibs.OpenXml.Constants { static ExcelXml() { - DefaultRels = MinifyXml(DefaultRels); - DefaultWorkbookXml = MinifyXml(DefaultWorkbookXml); - DefaultStylesXml = MinifyXml(DefaultStylesXml); - DefaultWorkbookXmlRels = MinifyXml(DefaultWorkbookXmlRels); - DefaultSheetRelXml = MinifyXml(DefaultSheetRelXml); - DefaultDrawing = MinifyXml(DefaultDrawing); + DefaultRels = ExcelOpenXmlUtils.MinifyXml( DefaultRels); + DefaultWorkbookXml = ExcelOpenXmlUtils.MinifyXml(DefaultWorkbookXml); + DefaultWorkbookXmlRels = ExcelOpenXmlUtils.MinifyXml(DefaultWorkbookXmlRels); + DefaultSheetRelXml = ExcelOpenXmlUtils.MinifyXml(DefaultSheetRelXml); + DefaultDrawing = ExcelOpenXmlUtils.MinifyXml(DefaultDrawing); } - private static string MinifyXml(string xml) => xml.Replace("\r", "").Replace("\n", "").Replace("\t", ""); internal static readonly string EmptySheetXml = $@""; @@ -35,146 +28,6 @@ namespace MiniExcelLibs.OpenXml.Constants "; - internal static readonly string NoneStylesXml = @" - - - - - - - - - - - - - - - - - - - -"; - - #region StyleSheet - - private const int startUpNumFmts = 1; - private const string NumFmtsToken = "{{numFmts}}"; - private const string NumFmtsCountToken = "{{numFmtCount}}"; - - private const int startUpCellXfs = 5; - private const string cellXfsToken = "{{cellXfs}}"; - private const string cellXfsCountToken = "{{cellXfsCount}}"; - - internal static readonly string DefaultStylesXml = $@" - - - - {NumFmtsToken} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {cellXfsToken} - - - - -"; - - #endregion - internal static readonly string DefaultWorkbookXml = @" @@ -251,51 +104,6 @@ namespace MiniExcelLibs.OpenXml.Constants internal static string Sheet(SheetDto sheetDto, int sheetId) => $@""; - internal static string SetupStyleXml(string styleXml, ICollection columns) - { - const int numFmtIndex = 166; - - var sb = new StringBuilder(styleXml); - var columnsToApply = GenerateStyleIds(columns); - - var numFmts = columnsToApply.Select((x, i) => - { - return new - { - numFmt = -$@"", - - cellXfs = -$@" - - " - }; - }).ToArray(); - - sb.Replace(NumFmtsToken, string.Join(string.Empty, numFmts.Select(x => x.numFmt))); - sb.Replace(NumFmtsCountToken, (startUpNumFmts + numFmts.Length).ToString()); - - sb.Replace(cellXfsToken, string.Join(string.Empty, numFmts.Select(x => x.cellXfs))); - sb.Replace(cellXfsCountToken, (5 + numFmts.Length).ToString()); - return sb.ToString(); - } - - private static IEnumerable GenerateStyleIds(ICollection dynamicColumns) - { - if (dynamicColumns == null) - yield break; - - int index = 0; - foreach (var g in dynamicColumns?.Where(x => !string.IsNullOrWhiteSpace(x.Format) && new ExcelNumberFormat(x.Format).IsValid).GroupBy(x => x.Format)) - { - foreach (var col in g) - col.FormatId = startUpCellXfs + index; - - yield return g.First(); - index++; - } - } - } + } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs index 3ec3283..007e9b1 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs @@ -1,6 +1,7 @@ using MiniExcelLibs.Attributes; using MiniExcelLibs.OpenXml.Constants; using MiniExcelLibs.OpenXml.Models; +using MiniExcelLibs.OpenXml.Styles; using MiniExcelLibs.Utils; using MiniExcelLibs.Zip; using System; @@ -364,9 +365,9 @@ namespace MiniExcelLibs.OpenXml switch (_configuration.TableStyles) { case TableStyles.None: - return ExcelXml.SetupStyleXml(ExcelXml.NoneStylesXml, columns); + return new MinimalSheetStyleBuilder().Build( columns); case TableStyles.Default: - return ExcelXml.SetupStyleXml(ExcelXml.DefaultStylesXml, columns); + return new DefaultSheetStyleBuilder().Build( columns ); default: return string.Empty; } diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs index 187382a..40cb8bc 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlTemplate.Impl.cs @@ -105,7 +105,7 @@ namespace MiniExcelLibs.OpenXml } } - private List XRowInfos { get; set; } + private List XRowInfos { get; set; } private readonly List CalcChainCellRefs = new List(); @@ -691,9 +691,9 @@ namespace MiniExcelLibs.OpenXml var mergeBaseRowIndex = newRowIndex; newRowIndex += rowInfo.IEnumerableMercell?.Height ?? 1; - // replace formulas - ProcessFormulas( rowXml, newRowIndex ); - writer.Write(CleanXml( rowXml, endPrefix)); // pass StringBuilder for netcoreapp3.0 or above + // replace formulas + ProcessFormulas( rowXml, newRowIndex ); + writer.Write(CleanXml( rowXml, endPrefix)); // pass StringBuilder for netcoreapp3.0 or above //mergecells if (rowInfo.RowMercells != null) @@ -757,9 +757,9 @@ namespace MiniExcelLibs.OpenXml .Replace($"{{{{$enumrowend}}}}", enumrowend.ToString()) .AppendFormat("", row.Name); - ProcessFormulas( rowXml, newRowIndex ); + ProcessFormulas( rowXml, newRowIndex ); - writer.Write(CleanXml( rowXml, endPrefix)); // pass StringBuilder for netcoreapp3.0 or above + writer.Write(CleanXml( rowXml, endPrefix)); // pass StringBuilder for netcoreapp3.0 or above //mergecells if (rowInfo.RowMercells != null) @@ -805,48 +805,48 @@ namespace MiniExcelLibs.OpenXml return; } - XmlReaderSettings settings = new XmlReaderSettings { NameTable = _ns.NameTable }; - XmlParserContext context = new XmlParserContext( null, _ns, "", XmlSpace.Default ); - XmlReader reader = XmlReader.Create( new StringReader( rowXmlString ), settings, context ); + XmlReaderSettings settings = new XmlReaderSettings { NameTable = _ns.NameTable }; + XmlParserContext context = new XmlParserContext( null, _ns, "", XmlSpace.Default ); + XmlReader reader = XmlReader.Create( new StringReader( rowXmlString ), settings, context ); XmlDocument d = new XmlDocument(); d.Load( reader ); var row = d.FirstChild as XmlElement; - // convert cells starting with '$=' into formulas - var cs = row.SelectNodes( $"x:c", _ns ); - for ( var ci = 0; ci < cs.Count; ci++ ) + // convert cells starting with '$=' into formulas + var cs = row.SelectNodes( $"x:c", _ns ); + for ( var ci = 0; ci < cs.Count; ci++ ) { - var c = cs.Item( ci ) as XmlElement; - if ( c == null ) { - continue; - } - /* Target: - - SUM(C2:C7) - - */ - var vs = c.SelectNodes( $"x:v", _ns ); - foreach ( XmlElement v in vs ) + var c = cs.Item( ci ) as XmlElement; + if ( c == null ) { + continue; + } + /* Target: + + SUM(C2:C7) + + */ + var vs = c.SelectNodes( $"x:v", _ns ); + foreach ( XmlElement v in vs ) { - if ( !v.InnerText.StartsWith( "$=" ) ) + if ( !v.InnerText.StartsWith( "$=" ) ) { - continue; - } - var fNode = c.OwnerDocument.CreateElement( "f", Config.SpreadsheetmlXmlns ); - fNode.InnerText = v.InnerText.Substring( 2 ); - c.InsertBefore( fNode, v ); - c.RemoveChild( v ); + continue; + } + var fNode = c.OwnerDocument.CreateElement( "f", Config.SpreadsheetmlXmlns ); + fNode.InnerText = v.InnerText.Substring( 2 ); + c.InsertBefore( fNode, v ); + c.RemoveChild( v ); - var celRef = ExcelOpenXmlUtils.ConvertXyToCell( ci + 1, rowIndex ); - CalcChainCellRefs.Add( celRef ); + var celRef = ExcelOpenXmlUtils.ConvertXyToCell( ci + 1, rowIndex ); + CalcChainCellRefs.Add( celRef ); - } - } + } + } rowXml.Clear(); - rowXml.Append( row.OuterXml ); - } + rowXml.Append( row.OuterXml ); + } private static string ConvertToDateTimeString(KeyValuePair propInfo, object cellValue) { diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlUtils.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlUtils.cs index f22f39e..946606f 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlUtils.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlUtils.cs @@ -9,6 +9,8 @@ #endif static class ExcelOpenXmlUtils { + public static string MinifyXml( string xml ) => xml.Replace( "\r", "" ).Replace( "\n", "" ).Replace( "\t", "" ).Trim(); + /// /// Encode to XML (special characteres: ' " > < &) /// diff --git a/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs new file mode 100644 index 0000000..5468af0 --- /dev/null +++ b/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs @@ -0,0 +1,156 @@ +using MiniExcelLibs.Attributes; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MiniExcelLibs.OpenXml.Styles { + + public class DefaultSheetStyleBuilder : ISheetStyleBuilder + { + private const int startUpNumFmts = 1; + private const string NumFmtsToken = "{{numFmts}}"; + private const string NumFmtsCountToken = "{{numFmtCount}}"; + + private const int startUpCellXfs = 5; + private const string cellXfsToken = "{{cellXfs}}"; + private const string cellXfsCountToken = "{{cellXfsCount}}"; + + internal static readonly string DefaultStylesXml = ExcelOpenXmlUtils.MinifyXml + ( $@" + + + + + {NumFmtsToken} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {cellXfsToken} + + + + + " + ); + + public string Build( ICollection columns ) + { + const int numFmtIndex = 166; + + var sb = new StringBuilder( DefaultStylesXml ); + var columnsToApply = SheetStyleBuilderHelper.GenerateStyleIds( startUpCellXfs, columns ); + + var numFmts = columnsToApply.Select( ( x, i ) => + { + return new + { + numFmt = $@"", + + cellXfs = $@" + + " + }; + } ).ToArray(); + + sb.Replace( NumFmtsToken, string.Join( string.Empty, numFmts.Select( x => x.numFmt ) ) ); + sb.Replace( NumFmtsCountToken, (startUpNumFmts + numFmts.Length).ToString() ); + + sb.Replace( cellXfsToken, string.Join( string.Empty, numFmts.Select( x => x.cellXfs ) ) ); + sb.Replace( cellXfsCountToken, (5 + numFmts.Length).ToString() ); + return sb.ToString(); + } + } + +} diff --git a/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs new file mode 100644 index 0000000..5be8dca --- /dev/null +++ b/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs @@ -0,0 +1,10 @@ +using MiniExcelLibs.Attributes; +using System.Collections.Generic; + +namespace MiniExcelLibs.OpenXml.Styles { + public interface ISheetStyleBuilder + { + string Build( ICollection columns ); + } + +} diff --git a/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs new file mode 100644 index 0000000..ec7e87f --- /dev/null +++ b/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs @@ -0,0 +1,71 @@ +using MiniExcelLibs.Attributes; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace MiniExcelLibs.OpenXml.Styles { + public class MinimalSheetStyleBuilder : ISheetStyleBuilder + { + private const int startUpNumFmts = 1; + private const string NumFmtsToken = "{{numFmts}}"; + private const string NumFmtsCountToken = "{{numFmtCount}}"; + + private const int startUpCellXfs = 5; + private const string cellXfsToken = "{{cellXfs}}"; + private const string cellXfsCountToken = "{{cellXfsCount}}"; + + internal static readonly string NoneStylesXml = ExcelOpenXmlUtils.MinifyXml + ( $@" + + + + + {NumFmtsToken} + + + + + + + + + + + + + + + + + + + + {cellXfsToken} + + " + ); + + public string Build( ICollection columns ) + { + const int numFmtIndex = 166; + + var sb = new StringBuilder( NoneStylesXml ); + var columnsToApply = SheetStyleBuilderHelper.GenerateStyleIds( startUpCellXfs, columns ); + + var numFmts = columnsToApply.Select( ( x, i ) => { + return new { + numFmt = $@"", + cellXfs = $@"" + }; + } ).ToArray(); + + sb.Replace( NumFmtsToken, string.Join( string.Empty, numFmts.Select( x => x.numFmt ) ) ); + sb.Replace( NumFmtsCountToken, (startUpNumFmts + numFmts.Length).ToString() ); + + sb.Replace( cellXfsToken, string.Join( string.Empty, numFmts.Select( x => x.cellXfs ) ) ); + sb.Replace( cellXfsCountToken, (5 + numFmts.Length).ToString() ); + return sb.ToString(); + } + } + +} diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs new file mode 100644 index 0000000..8d171ed --- /dev/null +++ b/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs @@ -0,0 +1,24 @@ +using MiniExcelLibs.Attributes; +using MiniExcelLibs.Utils; +using System.Collections.Generic; +using System.Linq; + +namespace MiniExcelLibs.OpenXml.Styles { + public static class SheetStyleBuilderHelper + { + public static IEnumerable GenerateStyleIds( int startUpCellXfs, ICollection dynamicColumns ) { + if ( dynamicColumns == null ) + yield break; + + int index = 0; + foreach ( var g in dynamicColumns?.Where( x => !string.IsNullOrWhiteSpace( x.Format ) && new ExcelNumberFormat( x.Format ).IsValid ).GroupBy( x => x.Format ) ) { + foreach ( var col in g ) + col.FormatId = startUpCellXfs + index; + + yield return g.First(); + index++; + } + } + } + +} diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index e849293..b3ab18e 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -1,4 +1,4 @@ -using Dapper; +using Dapper; using MiniExcelLibs.Attributes; using MiniExcelLibs.Csv; using MiniExcelLibs.Exceptions; @@ -3692,5 +3692,47 @@ MyProperty4,MyProperty1,MyProperty5,MyProperty2,MyProperty6,,MyProperty3 } + [Fact] + public void Issue632_1() + { + //https://github.com/mini-software/MiniExcel/issues/632 + var values = new List>(); + + foreach ( var item in Enumerable.Range( 1, 100 ) ) { + var dict = new Dictionary + { + { "Id", item }, + { "Time", DateTime.Now.ToLocalTime() }, + { "CPU Usage (%)", Math.Round( 56.345, 1 ) }, + { "Memory Usage (%)", Math.Round( 98.234, 1 ) }, + { "Disk Usage (%)", Math.Round( 32.456, 1 ) }, + { "CPU Temperature (°C)", Math.Round( 74.234, 1 ) }, + { "Voltage (V)", Math.Round( 6.3223, 1 ) }, + { "Network Usage (Kb/s)", Math.Round( 4503.23422, 1 ) }, + { "Instrument", "QT800050" } + }; + values.Add( dict ); + } + + var config = new OpenXmlConfiguration + { + TableStyles = TableStyles.None, + DynamicColumns = new DynamicExcelColumn[] + { + //new DynamicExcelColumn("Time") { Index = 0, Width = 20, Format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern + " " + CultureInfo.CurrentCulture.DateTimeFormat.LongTimePattern }, + //new DynamicExcelColumn("Time") { Index = 0, Width = 20, Format = CultureInfo.InvariantCulture.DateTimeFormat.ShortDatePattern + " " + CultureInfo.InvariantCulture.DateTimeFormat.LongTimePattern }, + //new DynamicExcelColumn("Time") { Index = 0, Width = 20 }, + new DynamicExcelColumn("Time") { Index = 0, Width = 20, Format = "d.MM.yyyy" }, + } + }; + + var path = Path.Combine( + Path.GetTempPath(), + string.Concat( nameof( MiniExcelIssueTests ), "_", nameof( Issue632_1 ), ".xlsx" ) + ); + + MiniExcel.SaveAs( path, values, excelType: ExcelType.XLSX, configuration: config, overwriteFile: true ); + + } } } \ No newline at end of file diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs index ecf4fd0..b7dd134 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs @@ -29,10 +29,10 @@ namespace MiniExcelLibs.Tests string path = GetTempXlsxPath(); char[] chars = new char[] {'\u0000','\u0001','\u0002','\u0003','\u0004','\u0005','\u0006','\u0007','\u0008', '\u0009', // - '\u000A', // - '\u000B','\u000C', + '\u000A', // + '\u000B','\u000C', '\u000D', // - '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', + '\u000E','\u000F','\u0010','\u0011','\u0012','\u0013','\u0014','\u0015','\u0016', '\u0017','\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F','\u007F' }; var input = chars.Select(s => new { Test = s.ToString() });