diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs index 6a143cc7..2a22a243 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.Async.cs @@ -1,4 +1,5 @@ -using MiniExcelLibs.OpenXml.Constants; +using System.ComponentModel; +using MiniExcelLibs.OpenXml.Constants; using MiniExcelLibs.OpenXml.Models; using MiniExcelLibs.OpenXml.Styles; using MiniExcelLibs.Utils; @@ -10,13 +11,17 @@ namespace MiniExcelLibs.OpenXml; -internal partial class ExcelOpenXmlSheetWriter : IExcelWriter +internal partial class ExcelOpenXmlSheetWriter { public async Task SaveAsAsync(CancellationToken cancellationToken = default) { try { - await GenerateDefaultOpenXmlAsync(cancellationToken); + await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken); + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken); + + using var sbc = _sheetStyleBuildContext; + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken); var sheets = GetSheets(); var rowsWritten = new List(); @@ -31,6 +36,7 @@ public async Task SaveAsAsync(CancellationToken cancellationToken = defau rowsWritten.Add(rows); } + await styleBuilder.BuildAsync(cancellationToken); await GenerateEndXmlAsync(cancellationToken); return rowsWritten.ToArray(); } @@ -47,8 +53,6 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke if (!_configuration.FastMode) throw new InvalidOperationException("Insert requires fast mode to be enabled"); - cancellationToken.ThrowIfCancellationRequested(); - var sheetRecords = new ExcelOpenXmlSheetReader(_stream, _configuration).GetWorkbookRels(_archive.Entries).ToArray(); foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id)) { @@ -59,7 +63,7 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke if (existSheetDto != null && !overwriteSheet) throw new Exception($"Sheet “{_defaultSheetName}” already exist"); - await GenerateStylesXmlAsync(cancellationToken);//GenerateStylesXml必须在校验overwriteSheet之后,避免不必要的样式更改 + var styleBuilder = await GetSheetStyleBuilderAsync(cancellationToken).ConfigureAwait(false); int rowsWritten; if (existSheetDto == null) @@ -77,6 +81,7 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, cancellationToken); } + await styleBuilder.BuildAsync(cancellationToken); await AddFilesToZipAsync(cancellationToken); _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); @@ -85,7 +90,7 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken); - GenerateWorkBookXmls(out StringBuilder workbookXml, out StringBuilder workbookRelsXml, out Dictionary sheetsRelsXml); + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); foreach (var sheetRelsXml in sheetsRelsXml) { var sheetRelsXmlPath = ExcelFileNames.SheetRels(sheetRelsXml.Key); @@ -105,17 +110,15 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke } finally { +#if NET10_0_OR_GREATER + await _archive.DisposeAsync(); +#else _archive.Dispose(); +#endif + _sheetStyleBuildContext.Dispose(); } } - internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationToken) - { - await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken); - await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken); - await GenerateStylesXmlAsync(cancellationToken); - } - private async Task CreateSheetXmlAsync(object values, string sheetPath, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -124,7 +127,7 @@ private async Task CreateSheetXmlAsync(object values, string sheetPath, Can var rowsWritten = 0; using (var zipStream = entry.Open()) - using (var writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) + using (var writer = new MiniExcelAsyncStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize, cancellationToken)) { if (values == null) { @@ -169,21 +172,21 @@ private async Task WriteValuesAsync(MiniExcelAsyncStreamWriter writer, obje IMiniExcelWriteAdapter writeAdapter = null; #if NETSTANDARD2_0_OR_GREATER || NET - IAsyncMiniExcelWriteAdapter asyncWriteAdapter = null; + IAsyncMiniExcelWriteAdapter asyncWriteAdapter = null; #endif try { #if NETSTANDARD2_0_OR_GREATER || NET - if (!MiniExcelWriteAdapterFactory.TryGetAsyncWriteAdapter(values, _configuration, out asyncWriteAdapter)) - { - writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); - } + if (!MiniExcelWriteAdapterFactory.TryGetAsyncWriteAdapter(values, _configuration, out asyncWriteAdapter)) + { + writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); + } - var count = 0; - var isKnownCount = writeAdapter != null && writeAdapter.TryGetKnownCount(out count); - var props = writeAdapter != null - ? writeAdapter?.GetColumns() - : await asyncWriteAdapter.GetColumnsAsync(); + var count = 0; + var isKnownCount = writeAdapter != null && writeAdapter.TryGetKnownCount(out count); + var props = writeAdapter != null + ? writeAdapter?.GetColumns() + : await asyncWriteAdapter.GetColumnsAsync(); #else writeAdapter = MiniExcelWriteAdapterFactory.GetWriteAdapter(values, _configuration); @@ -191,19 +194,20 @@ private async Task WriteValuesAsync(MiniExcelAsyncStreamWriter writer, obje var props = writeAdapter.GetColumns(); #endif - if (props == null) + if (props is null or { Count: 0 }) { await WriteEmptySheetAsync(writer); return 0; } - var maxColumnIndex = props.Count; + _sheetStyleBuildContext.UpdateFormatIds(props); + int maxRowIndex; + var maxColumnIndex = props.Count; + long dimensionPlaceholderPostition = 0; await writer.WriteAsync(WorksheetXml.StartWorksheetWithRelationship); - long dimensionPlaceholderPostition = 0; - // We can write the dimensions directly if the row count is known if (_configuration.FastMode && !isKnownCount) { @@ -370,7 +374,7 @@ private async Task PrintHeaderAsync(MiniExcelAsyncStreamWriter writer, List GetSheetStyleBuilderAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - - using (var context = new SheetStyleBuildContext(_zipDictionary, _archive, _utf8WithBom, _configuration.DynamicColumns)) + SheetStyleBuilderBase builder = _configuration.TableStyles switch { - ISheetStyleBuilder builder = null; - switch (_configuration.TableStyles) - { - case TableStyles.None: - builder = new MinimalSheetStyleBuilder(context); - break; - case TableStyles.Default: - builder = new DefaultSheetStyleBuilder(context, _configuration.StyleOptions); - break; - } - var result = await builder.BuildAsync(cancellationToken); - _cellXfIdMap = result.CellXfIdMap; - } + TableStyles.None => new MinimalSheetStyleBuilder(_sheetStyleBuildContext), + TableStyles.Default => new DefaultSheetStyleBuilder(_sheetStyleBuildContext, _configuration.StyleOptions), + _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) + }; + + var newInfos = builder.GetGenerateElementInfos(); + await _sheetStyleBuildContext.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); + + return builder; } private async Task GenerateDrawinRelXmlAsync(CancellationToken cancellationToken) @@ -589,7 +581,7 @@ private async Task CreateZipEntryAsync(string path, string contentType, string c #else using (var zipStream = entry.Open()) #endif - using (var writer = new MiniExcelAsyncStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize, cancellationToken)) + using (var writer = new MiniExcelAsyncStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize, cancellationToken)) await writer.WriteAsync(content); if (!string.IsNullOrEmpty(contentType)) diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs index d59c576b..7b0f18d5 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.DefaultOpenXml.cs @@ -9,10 +9,22 @@ namespace MiniExcelLibs.OpenXml; -internal partial class ExcelOpenXmlSheetWriter : IExcelWriter +internal partial class ExcelOpenXmlSheetWriter { - private readonly Dictionary _zipDictionary = new Dictionary(); - private Dictionary _cellXfIdMap; + private const string DefaultCellStyleIndex = "0"; + private const string HeaderCellStyleIndex = "1"; + private const string RegularCellStyleIndex = "2"; + private const string DateCellStyleIndex = "3"; + private const string FillCellStyleIndex = "4"; + private const string TimeCellStyleIndex = "5"; + + private const string StringDataType = "str"; + private const string NumericDataType = "n"; + private const string BooleanDataType = "b"; + + private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); + + private readonly Dictionary _zipDictionary = new(); private IEnumerable> GetSheets() { @@ -113,8 +125,8 @@ private string GetPanes() } sb.Append( WorksheetXml.StartPane( - xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : (int?)null, - ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : (int?)null, + xSplit: _configuration.FreezeColumnCount > 0 ? _configuration.FreezeColumnCount : null, + ySplit: _configuration.FreezeRowCount > 0 ? _configuration.FreezeRowCount : null, topLeftCell: ExcelOpenXmlUtils.ConvertXyToCell( _configuration.FreezeColumnCount + 1, _configuration.FreezeRowCount + 1 @@ -168,77 +180,79 @@ private string GetPanes() private Tuple GetCellValue(int rowIndex, int cellIndex, object value, ExcelColumnInfo columnInfo, bool valueIsNull) { if (valueIsNull) - return Tuple.Create("2", "str", string.Empty); + return Tuple.Create(RegularCellStyleIndex, StringDataType, string.Empty); if (value is string str) - return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(str)); + return Tuple.Create(RegularCellStyleIndex, StringDataType, ExcelOpenXmlUtils.EncodeXML(str)); var type = GetValueType(value, columnInfo); - if (columnInfo?.ExcelFormat != null && columnInfo?.ExcelFormatId == -1 && value is IFormattable formattableValue) + if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); - return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(formattedStr)); + return Tuple.Create(RegularCellStyleIndex, StringDataType, ExcelOpenXmlUtils.EncodeXML(formattedStr)); } if (type == typeof(DateTime)) return GetDateTimeValue((DateTime)value, columnInfo); + if (type == typeof(DateTimeOffset)) + return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnInfo); + + if (type == typeof(TimeSpan)) + return GetTimeSpanValue((TimeSpan)value, columnInfo); + #if NET6_0_OR_GREATER if (type == typeof(DateOnly)) return GetDateTimeValue(((DateOnly)value).ToDateTime(new TimeOnly()), columnInfo); + + if (type == typeof(TimeOnly)) + return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnInfo); #endif if (type.IsEnum) { var description = CustomPropertyHelper.DescriptionAttr(type, value); - return Tuple.Create("2", "str", description ?? value.ToString()); + return Tuple.Create(RegularCellStyleIndex, StringDataType, description ?? value.ToString()); } if (TypeHelper.IsNumericType(type)) { var cellValue = GetNumericValue(value, type); - if (columnInfo == null || columnInfo.ExcelFormat == null) + if (columnInfo?.ExcelFormat is null) { - var dataType = _configuration.Culture == CultureInfo.InvariantCulture ? "n" : "str"; - return Tuple.Create("2", dataType, cellValue); + var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? NumericDataType : StringDataType; + return Tuple.Create(RegularCellStyleIndex, dataType, cellValue); } return Tuple.Create(columnInfo.ExcelFormatId.ToString(), (string)null, cellValue); } if (type == typeof(bool)) - return Tuple.Create("2", "b", (bool)value ? "1" : "0"); + return Tuple.Create(RegularCellStyleIndex, BooleanDataType, (bool)value ? "1" : "0"); if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { var base64 = GetFileValue(rowIndex, cellIndex, value); if (_configuration.EnableWriteFilePath) { - return Tuple.Create("4", "str", ExcelOpenXmlUtils.EncodeXML(base64)); + return Tuple.Create(FillCellStyleIndex, StringDataType, ExcelOpenXmlUtils.EncodeXML(base64)); } - return Tuple.Create("4", "str", ""); + return Tuple.Create(FillCellStyleIndex, StringDataType, ""); } - return Tuple.Create("2", "str", ExcelOpenXmlUtils.EncodeXML(value.ToString())); + return Tuple.Create(RegularCellStyleIndex, StringDataType, ExcelOpenXmlUtils.EncodeXML(value.ToString())); } private static Type GetValueType(object value, ExcelColumnInfo columnInfo) { - Type type; - if (columnInfo == null || columnInfo.Key != null) - { - // TODO: need to optimize - // Dictionary need to check type every time, so it's slow.. - type = value.GetType(); - type = Nullable.GetUnderlyingType(type) ?? type; - } - else - { - type = columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop - } + if (columnInfo is { Key: null }) + return columnInfo.ExcludeNullableType; //sometime it doesn't need to re-get type like prop - return type; + // TODO: need to optimize + // Dictionary need to check type every time, so it's slow.. + var type = value.GetType(); + return Nullable.GetUnderlyingType(type) ?? type; } private string GetNumericValue(object value, Type type) @@ -270,7 +284,7 @@ private string GetNumericValue(object value, Type type) if (type.IsAssignableFrom(typeof(float))) return ((float)value).ToString(_configuration.Culture); - return (decimal.Parse(value.ToString())).ToString(_configuration.Culture); + return decimal.Parse(value.ToString()).ToString(_configuration.Culture); } private string GetFileValue(int rowIndex, int cellIndex, object value) @@ -282,7 +296,7 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) //it can't insert to zip first to avoid cache image to memory //because sheet xml is opening.. https://github.com/mini-software/MiniExcel/issues/304#issuecomment-1017031691 //int rowIndex, int cellIndex - var file = new FileDto() + var file = new FileDto { Byte = bytes, RowIndex = rowIndex, @@ -309,28 +323,28 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) private Tuple GetDateTimeValue(DateTime value, ExcelColumnInfo columnInfo) { - string cellValue = null; + string cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return Tuple.Create("2", "str", cellValue); + return Tuple.Create(DateCellStyleIndex, StringDataType, cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - var format = columnInfo?.ExcelFormat != null ? columnInfo.ExcelFormatId.ToString() : "3"; + var format = columnInfo?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "3"; return Tuple.Create(format, (string)null, cellValue); } private static double CorrectDateTimeValue(DateTime value) { - var oaDate = value.ToOADate(); - // Excel says 1900 was a leap year :( Replicate an incorrect behavior thanks // to Lotus 1-2-3 decision from 1983... // https://github.com/ClosedXML/ClosedXML/blob/develop/ClosedXML/Extensions/DateTimeExtensions.cs#L45 const int nonExistent1900Feb29SerialDate = 60; + + var oaDate = value.ToOADate(); if (oaDate <= nonExistent1900Feb29SerialDate) { oaDate -= 1; @@ -339,12 +353,21 @@ private static double CorrectDateTimeValue(DateTime value) return oaDate; } + private Tuple GetTimeSpanValue(TimeSpan value, ExcelColumnInfo columnInfo) + { + if (value.TotalDays >= 1) + return GetDateTimeValue(ExcelZeroDate + value, columnInfo); + + var cellValue = value.TotalDays.ToString(CultureInfo.InvariantCulture); + var format = columnInfo?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : TimeCellStyleIndex; + + return Tuple.Create(format, (string)null, cellValue); + } + private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) { string dimensionRef; - if (maxRowIndex == 0 && maxColumnIndex == 0) - dimensionRef = "A1"; - else if (maxRowIndex <= 1 && maxColumnIndex == 0) + if (maxRowIndex <= 1 && maxColumnIndex == 0) dimensionRef = "A1"; else if (maxColumnIndex <= 1) dimensionRef = $"A1:A{maxRowIndex}"; @@ -352,6 +375,7 @@ private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}1"; else dimensionRef = $"A1:{ColumnHelper.GetAlphabetColumnName(maxColumnIndex - 1)}{maxRowIndex}"; + return dimensionRef; } @@ -415,9 +439,4 @@ private string GetContentTypesXml() sb.Append(ExcelXml.EndTypes); return sb.ToString(); } - - private string GetCellXfId(string styleIndex) - { - return _cellXfIdMap.TryGetValue(styleIndex, out var cellXfId) ? cellXfId : styleIndex; - } -} \ No newline at end of file +} diff --git a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs index 0fc5af3d..8d8cbbc4 100644 --- a/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs +++ b/src/MiniExcel/OpenXml/ExcelOpenXmlSheetWriter.cs @@ -1,4 +1,5 @@ -using MiniExcelLibs.Attributes; +using System.ComponentModel; +using MiniExcelLibs.Attributes; using MiniExcelLibs.OpenXml.Constants; using MiniExcelLibs.OpenXml.Models; using MiniExcelLibs.OpenXml.Styles; @@ -13,17 +14,22 @@ namespace MiniExcelLibs.OpenXml; internal partial class ExcelOpenXmlSheetWriter : IExcelWriter { + private static readonly UTF8Encoding Utf8WithBom = new UTF8Encoding(true); private readonly MiniExcelZipArchive _archive; - private static readonly UTF8Encoding _utf8WithBom = new UTF8Encoding(true); - private readonly OpenXmlConfiguration _configuration; private readonly Stream _stream; + + private readonly OpenXmlConfiguration _configuration; private readonly bool _printHeader; private readonly object _value; private readonly string _defaultSheetName; - private readonly List _sheets = new List(); - private readonly List _files = new List(); + + private readonly List _sheets = []; + private readonly List _files = []; + private readonly SheetStyleBuildContext _sheetStyleBuildContext; + private int _currentSheetIndex = 0; + public ExcelOpenXmlSheetWriter(Stream stream, object value, string sheetName, IConfiguration configuration, bool printHeader) { _stream = stream; @@ -34,17 +40,25 @@ public ExcelOpenXmlSheetWriter(Stream stream, object value, string sheetName, IC throw new InvalidOperationException("Auto width requires fast mode to be enabled"); var archiveMode = _configuration.FastMode ? ZipArchiveMode.Update : ZipArchiveMode.Create; - _archive = new MiniExcelZipArchive(_stream, archiveMode, true, _utf8WithBom); + _archive = new MiniExcelZipArchive(_stream, archiveMode, true, Utf8WithBom); _value = value; _printHeader = printHeader; _defaultSheetName = sheetName; + + _sheetStyleBuildContext = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom); } public int[] SaveAs() { - GenerateDefaultOpenXml(); + using var archive = _archive; + + CreateZipEntry(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels); + CreateZipEntry(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString); + using var sbc = _sheetStyleBuildContext; + var styleBuilder = GetSheetStyleBuilder(_sheetStyleBuildContext); + var sheets = GetSheets(); var rowsWritten = new List(); @@ -56,81 +70,82 @@ public int[] SaveAs() rowsWritten.Add(rows); } + styleBuilder.Build(); GenerateEndXml(); - _archive.Dispose(); return rowsWritten.ToArray(); } public int Insert(bool overwriteSheet = false) { - if (!_configuration.FastMode) + try { - throw new InvalidOperationException("Insert requires fast mode to be enabled"); - } + if (!_configuration.FastMode) + throw new InvalidOperationException("Insert requires fast mode to be enabled"); - var sheetRecords = new ExcelOpenXmlSheetReader(_stream, _configuration).GetWorkbookRels(_archive.Entries).ToArray(); - foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id)) - { - _sheets.Add(new SheetDto { Name = sheetRecord.Name, SheetIdx = (int)sheetRecord.Id, State = sheetRecord.State }); - } - var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); - if (existSheetDto != null && !overwriteSheet) - { - throw new Exception($"Sheet “{_defaultSheetName}” already exist"); - } + var sheetRecords = new ExcelOpenXmlSheetReader(_stream, _configuration).GetWorkbookRels(_archive.Entries).ToArray(); + foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id)) + { + _sheets.Add(new SheetDto { Name = sheetRecord.Name, SheetIdx = (int)sheetRecord.Id, State = sheetRecord.State }); + } + var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); + if (existSheetDto != null && !overwriteSheet) + { + throw new Exception($"Sheet “{_defaultSheetName}” already exist"); + } - GenerateStylesXml();//GenerateStylesXml必须在校验overwriteSheet之后,避免不必要的样式更改 - int rowsWritten; - if (existSheetDto == null) - { - _currentSheetIndex = (int)sheetRecords.Max(m => m.Id) + 1; - var insertSheetInfo = GetSheetInfos(_defaultSheetName); - var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); - _sheets.Add(insertSheetDto); - rowsWritten = CreateSheetXml(_value, insertSheetDto.Path); - } - else - { - _currentSheetIndex = existSheetDto.SheetIdx; - _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); - rowsWritten = CreateSheetXml(_value, existSheetDto.Path); - } + var styleBuilder = GetSheetStyleBuilder(_sheetStyleBuildContext); - AddFilesToZip(); + int rowsWritten; + if (existSheetDto == null) + { + _currentSheetIndex = (int)sheetRecords.Max(m => m.Id) + 1; + var insertSheetInfo = GetSheetInfos(_defaultSheetName); + var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); + _sheets.Add(insertSheetDto); + rowsWritten = CreateSheetXml(_value, insertSheetDto.Path); + } + else + { + _currentSheetIndex = existSheetDto.SheetIdx; + _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); + rowsWritten = CreateSheetXml(_value, existSheetDto.Path); + } - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); - GenerateDrawinRelXml(_currentSheetIndex - 1); + styleBuilder.Build(); + AddFilesToZip(); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); - GenerateDrawingXml(_currentSheetIndex - 1); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); + GenerateDrawinRelXml(_currentSheetIndex - 1); - GenerateWorkBookXmls(out StringBuilder workbookXml, out StringBuilder workbookRelsXml, out Dictionary sheetsRelsXml); - foreach (var sheetRelsXml in sheetsRelsXml) - { - var sheetRelsXmlPath = ExcelFileNames.SheetRels(sheetRelsXml.Key); - _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); - CreateZipEntry(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value)); - } + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); + GenerateDrawingXml(_currentSheetIndex - 1); - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); - CreateZipEntry(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); + foreach (var sheetRelsXml in sheetsRelsXml) + { + var sheetRelsXmlPath = ExcelFileNames.SheetRels(sheetRelsXml.Key); + _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); + CreateZipEntry(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value)); + } - _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); - CreateZipEntry(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); + CreateZipEntry(ExcelFileNames.Workbook, ExcelContentTypes.Workbook, ExcelXml.DefaultWorkbookXml.Replace("{{sheets}}", workbookXml.ToString())); - InsertContentTypesXml(); + _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.WorkbookRels)?.Delete(); + CreateZipEntry(ExcelFileNames.WorkbookRels, null, ExcelXml.DefaultWorkbookXmlRels.Replace("{{sheets}}", workbookRelsXml.ToString())); - _archive.Dispose(); - - return rowsWritten; - } + InsertContentTypesXml(); - internal void GenerateDefaultOpenXml() - { - CreateZipEntry(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels); - CreateZipEntry(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString); - GenerateStylesXml(); + _archive.Dispose(); + + return rowsWritten; + } + finally + { + _sheetStyleBuildContext.Dispose(); + _archive.Dispose(); + } } private int CreateSheetXml(object values, string sheetPath) @@ -139,7 +154,7 @@ private int CreateSheetXml(object values, string sheetPath) var rowsWritten = 0; using (var zipStream = entry.Open()) - using (var writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + using (var writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize)) { if (values == null) { @@ -184,14 +199,17 @@ private int WriteValues(MiniExcelStreamWriter writer, object values) var isKnownCount = writeAdapter.TryGetKnownCount(out var count); var props = writeAdapter.GetColumns(); - if (props == null) + + if (props is null or { Count: 0 }) { WriteEmptySheet(writer); return 0; } - + + _sheetStyleBuildContext.UpdateFormatIds(props); + int maxRowIndex; - var maxColumnIndex = props.Count(x => x != null && !x.ExcelIgnore); + var maxColumnIndex = props.Count(x => x is { ExcelIgnore: false }); writer.Write(WorksheetXml.StartWorksheetWithRelationship); @@ -319,7 +337,7 @@ private void PrintHeader(MiniExcelStreamWriter writer, List pro continue; var r = ExcelOpenXmlUtils.ConvertXyToCell(xIndex, yIndex); - WriteCell(writer, r, columnName: p.ExcelColumnName); + writer.Write(WorksheetXml.Cell(r, StringDataType, HeaderCellStyleIndex, ExcelOpenXmlUtils.EncodeXML(p.ExcelColumnName))); } xIndex++; } @@ -342,13 +360,11 @@ private void WriteCell(MiniExcelStreamWriter writer, int rowIndex, int cellIndex } var columnReference = ExcelOpenXmlUtils.ConvertXyToCell(cellIndex, rowIndex); - var valueIsNull = value is null || - value is DBNull || - (_configuration.WriteEmptyStringAsNull && value is string vs && vs == string.Empty); + var valueIsNull = value is null or DBNull || (_configuration.WriteEmptyStringAsNull && value is ""); if (_configuration.EnableWriteNullValueCell && valueIsNull) { - writer.Write(WorksheetXml.EmptyCell(columnReference, GetCellXfId("2"))); + writer.Write(WorksheetXml.EmptyCell(columnReference, DefaultCellStyleIndex)); return; } @@ -362,13 +378,10 @@ value is DBNull || /*Prefix and suffix blank space will lost after SaveAs #294*/ var preserveSpace = cellValue != null && (cellValue.StartsWith(" ", StringComparison.Ordinal) || cellValue.EndsWith(" ", StringComparison.Ordinal)); - writer.Write(WorksheetXml.Cell(columnReference, dataType, GetCellXfId(styleIndex), cellValue, preserveSpace: preserveSpace, columnType: columnType)); + writer.Write(WorksheetXml.Cell(columnReference, dataType, styleIndex, cellValue, preserveSpace: preserveSpace, columnType: columnType)); widthCollection?.AdjustWidth(cellIndex, cellValue); } - private void WriteCell(MiniExcelStreamWriter writer, string cellReference, string columnName) - => writer.Write(WorksheetXml.Cell(cellReference, "str", GetCellXfId("1"), ExcelOpenXmlUtils.EncodeXML(columnName))); - private void GenerateEndXml() { AddFilesToZip(); @@ -386,23 +399,19 @@ private void AddFilesToZip() } } - private void GenerateStylesXml() + private ISheetStyleBuilder GetSheetStyleBuilder(SheetStyleBuildContext context) { - using (var context = new SheetStyleBuildContext(_zipDictionary, _archive, _utf8WithBom, _configuration.DynamicColumns)) + SheetStyleBuilderBase builder = _configuration.TableStyles switch { - ISheetStyleBuilder builder = null; - switch (_configuration.TableStyles) - { - case TableStyles.None: - builder = new MinimalSheetStyleBuilder(context); - break; - case TableStyles.Default: - builder = new DefaultSheetStyleBuilder(context, _configuration.StyleOptions); - break; - } - var result = builder?.Build(); - _cellXfIdMap = result?.CellXfIdMap; - } + TableStyles.None => new MinimalSheetStyleBuilder(context), + TableStyles.Default => new DefaultSheetStyleBuilder(context, _configuration.StyleOptions), + _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) + }; + + var newInfos = builder.GetGenerateElementInfos(); + context.Create(newInfos); + + return builder; } private void GenerateDrawinRelXml() @@ -508,7 +517,7 @@ private void CreateZipEntry(string path, string contentType, string content) { ZipArchiveEntry entry = _archive.CreateEntry(path, CompressionLevel.Fastest); using (var zipStream = entry.Open()) - using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, _utf8WithBom, _configuration.BufferSize)) + using (MiniExcelStreamWriter writer = new MiniExcelStreamWriter(zipStream, Utf8WithBom, _configuration.BufferSize)) { writer.Write(content); } diff --git a/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs index bbbc9ed7..1b18def9 100644 --- a/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs +++ b/src/MiniExcel/OpenXml/Styles/DefaultSheetStyleBuilder.cs @@ -2,32 +2,25 @@ namespace MiniExcelLibs.OpenXml.Styles; -internal class DefaultSheetStyleBuilder : SheetStyleBuilderBase +internal class DefaultSheetStyleBuilder(SheetStyleBuildContext context, OpenXmlStyleOptions styleOptions) : SheetStyleBuilderBase(context) { - private static readonly SheetStyleElementInfos GenerateElementInfos = new SheetStyleElementInfos + private static readonly SheetStyleElementInfos GenerateElementInfos = new() { - NumFmtCount = 0,//The default NumFmt number is 0, but there will be NumFmt dynamically generated based on ColumnsToApply + NumFmtCount = 0, //The default NumFmt number is 0, but there will be NumFmt dynamically generated based on format mappings FontCount = 2, FillCount = 3, BorderCount = 2, CellStyleXfCount = 3, - CellXfCount = 5 + CellXfCount = 6 }; private static readonly Color DefaultBackgroundColor = Color.FromArgb(0x284472C4); - private const HorizontalCellAlignment DefaultHorizontalAlignment = HorizontalCellAlignment.Left; - private const VerticalCellAlignment DefaultVerticalAlignment = VerticalCellAlignment.Bottom; - private readonly SheetStyleBuildContext _context; - private readonly OpenXmlStyleOptions _styleOptions; + private readonly SheetStyleBuildContext _context = context; + private readonly OpenXmlStyleOptions _styleOptions = styleOptions; - public DefaultSheetStyleBuilder(SheetStyleBuildContext context, OpenXmlStyleOptions styleOptions) : base(context) - { - _context = context; - _styleOptions = styleOptions; - } - protected override SheetStyleElementInfos GetGenerateElementInfos() + protected internal override SheetStyleElementInfos GetGenerateElementInfos() { return GenerateElementInfos; } @@ -37,7 +30,7 @@ protected override void GenerateNumFmt() const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var item in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -46,8 +39,8 @@ protected override void GenerateNumFmt() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "numFmt", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); - _context.NewXmlWriter.WriteAttributeString("formatCode", item.Format); - _context.NewXmlWriter.WriteFullEndElement(); + _context.NewXmlWriter.WriteAttributeString("formatCode", item.Key); + _context.NewXmlWriter.WriteEndElement(); } } @@ -55,7 +48,7 @@ protected override async Task GenerateNumFmtAsync() { const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var item in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -64,8 +57,8 @@ protected override async Task GenerateNumFmtAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "numFmt", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); ; - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Format); - await _context.NewXmlWriter.WriteFullEndElementAsync(); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Key); + await _context.NewXmlWriter.WriteEndElementAsync(); } } @@ -504,9 +497,9 @@ protected override void GenerateCellStyleXf() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", "0"); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); + _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount}"); _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); _context.NewXmlWriter.WriteAttributeString("applyFill", "1"); _context.NewXmlWriter.WriteAttributeString("applyBorder", "0"); @@ -546,8 +539,8 @@ protected override void GenerateCellStyleXf() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", "0"); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); _context.NewXmlWriter.WriteAttributeString("applyFill", "1"); @@ -570,9 +563,9 @@ protected override async Task GenerateCellStyleXfAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyFill", null, "1"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyBorder", null, "0"); @@ -612,8 +605,8 @@ protected override async Task GenerateCellStyleXfAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyFill", null, "1"); @@ -629,11 +622,39 @@ protected override async Task GenerateCellStyleXfAsync() protected override void GenerateCellXf() { + var headerHorizontalAlignment = _styleOptions.HeaderStyle?.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }; + + var headerVerticalAlignment = _styleOptions.HeaderStyle?.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }; + + var cellHorizontalAlignment = _styleOptions.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }; + + var cellVerticalAlignment = _styleOptions.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }; + /* - * + * */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); - _context.NewXmlWriter.WriteEndElement(); + _context.NewXmlWriter.WriteFullEndElement(); /* * @@ -652,18 +673,11 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteAttributeString("applyBorder", "1"); _context.NewXmlWriter.WriteAttributeString("applyAlignment", "1"); _context.NewXmlWriter.WriteAttributeString("applyProtection", "1"); - _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString(null, "horizontal", null, headerHorizontalAlignment); + _context.NewXmlWriter.WriteAttributeString(null, "vertical", null, headerVerticalAlignment); - var horizontalAlignment = _styleOptions.HeaderStyle?.HorizontalAlignment ?? DefaultHorizontalAlignment; - var horizontalAlignmentStr = horizontalAlignment.ToString().ToLowerInvariant(); - _context.NewXmlWriter.WriteAttributeString(null, "horizontal", null, horizontalAlignmentStr); - - var verticalAlignment = _styleOptions.HeaderStyle?.VerticalAlignment ?? DefaultVerticalAlignment; - var verticalAlignmentStr = verticalAlignment.ToString().ToLowerInvariant(); - _context.NewXmlWriter.WriteAttributeString(null, "vertical", null, verticalAlignmentStr); - - var wrapHeader = (_styleOptions.HeaderStyle?.WrapText ?? false) ? "1" : "0"; + var wrapHeader = _styleOptions.HeaderStyle?.WrapText is true ? "1" : "0"; _context.NewXmlWriter.WriteAttributeString(null, "wrapText", null, wrapHeader); _context.NewXmlWriter.WriteAttributeString("textRotation", "0"); @@ -688,8 +702,8 @@ protected override void GenerateCellXf() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", "0"); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); _context.NewXmlWriter.WriteAttributeString("xfId", "0"); _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); @@ -699,36 +713,8 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteAttributeString("applyProtection", "1"); _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style1HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style1HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style1HorizontalAlignment = "right"; - break; - default: - style1HorizontalAlignment = "general"; - break; - } - - string style1VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style1VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style1VerticalAlignment = "center"; - break; - default: - style1VerticalAlignment = "bottom"; - break; - } - - _context.NewXmlWriter.WriteAttributeString("horizontal", style1HorizontalAlignment); - _context.NewXmlWriter.WriteAttributeString("vertical", style1VerticalAlignment); + _context.NewXmlWriter.WriteAttributeString("horizontal", cellHorizontalAlignment); + _context.NewXmlWriter.WriteAttributeString("vertical", cellVerticalAlignment); _context.NewXmlWriter.WriteAttributeString("textRotation", "0"); var wrapContent = _styleOptions.WrapCellContents ? "1" : "0"; @@ -754,8 +740,8 @@ protected override void GenerateCellXf() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", "14"); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); _context.NewXmlWriter.WriteAttributeString("xfId", "0"); _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); @@ -763,38 +749,10 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteAttributeString("applyBorder", "1"); _context.NewXmlWriter.WriteAttributeString("applyAlignment", "1"); _context.NewXmlWriter.WriteAttributeString("applyProtection", "1"); - - _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style2HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style2HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style2HorizontalAlignment = "right"; - break; - default: - style2HorizontalAlignment = "general"; - break; - } - - string style2VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style2VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style2VerticalAlignment = "center"; - break; - default: - style2VerticalAlignment = "bottom"; - break; - } - _context.NewXmlWriter.WriteAttributeString("horizontal", style2HorizontalAlignment); - _context.NewXmlWriter.WriteAttributeString("vertical", style2VerticalAlignment); + _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString("horizontal", cellHorizontalAlignment); + _context.NewXmlWriter.WriteAttributeString("vertical", cellVerticalAlignment); _context.NewXmlWriter.WriteAttributeString("textRotation", "0"); _context.NewXmlWriter.WriteAttributeString("wrapText", "0"); _context.NewXmlWriter.WriteAttributeString("indent", "0"); @@ -817,8 +775,8 @@ protected override void GenerateCellXf() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", "0"); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); _context.NewXmlWriter.WriteAttributeString("xfId", "0"); _context.NewXmlWriter.WriteAttributeString("applyBorder", "1"); @@ -829,12 +787,43 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteEndElement(); _context.NewXmlWriter.WriteEndElement(); + /* + * + * + * + */ + _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString("numFmtId", "21"); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); + _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); + _context.NewXmlWriter.WriteAttributeString("xfId", "0"); + _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); + _context.NewXmlWriter.WriteAttributeString("applyFill", "1"); + _context.NewXmlWriter.WriteAttributeString("applyBorder", "1"); + _context.NewXmlWriter.WriteAttributeString("applyAlignment", "1"); + _context.NewXmlWriter.WriteAttributeString("applyProtection", "1"); + _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString("horizontal", cellHorizontalAlignment); + _context.NewXmlWriter.WriteAttributeString("vertical", cellVerticalAlignment); + _context.NewXmlWriter.WriteAttributeString("textRotation", "0"); + _context.NewXmlWriter.WriteAttributeString("wrapText", "0"); + _context.NewXmlWriter.WriteAttributeString("indent", "0"); + _context.NewXmlWriter.WriteAttributeString("relativeIndent", "0"); + _context.NewXmlWriter.WriteAttributeString("justifyLastLine", "0"); + _context.NewXmlWriter.WriteAttributeString("shrinkToFit", "0"); + _context.NewXmlWriter.WriteAttributeString("readingOrder", "0"); + _context.NewXmlWriter.WriteEndElement(); + _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "protection", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString("locked", "1"); + _context.NewXmlWriter.WriteAttributeString("hidden", "0"); + _context.NewXmlWriter.WriteEndElement(); + _context.NewXmlWriter.WriteEndElement(); + const int numFmtIndex = 166; - var index = 0; - foreach (var item in _context.ColumnsToApply) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * * @@ -842,9 +831,9 @@ protected override void GenerateCellXf() * */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); - _context.NewXmlWriter.WriteAttributeString("numFmtId", (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); - _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount + 0}"); - _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount + 0}"); + _context.NewXmlWriter.WriteAttributeString("numFmtId", (numFmtIndex + i + _context.OldElementInfos.NumFmtCount).ToString()); + _context.NewXmlWriter.WriteAttributeString("fontId", $"{_context.OldElementInfos.FontCount}"); + _context.NewXmlWriter.WriteAttributeString("fillId", $"{_context.OldElementInfos.FillCount}"); _context.NewXmlWriter.WriteAttributeString("borderId", $"{_context.OldElementInfos.BorderCount + 1}"); _context.NewXmlWriter.WriteAttributeString("xfId", "0"); _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); @@ -854,36 +843,8 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteAttributeString("applyProtection", "1"); _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style3HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style3HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style3HorizontalAlignment = "right"; - break; - default: - style3HorizontalAlignment = "general"; - break; - } - - string style3VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style3VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style3VerticalAlignment = "center"; - break; - default: - style3VerticalAlignment = "bottom"; - break; - } - - _context.NewXmlWriter.WriteAttributeString("horizontal", style3HorizontalAlignment); - _context.NewXmlWriter.WriteAttributeString("vertical", style3VerticalAlignment); + _context.NewXmlWriter.WriteAttributeString("horizontal", cellHorizontalAlignment); + _context.NewXmlWriter.WriteAttributeString("vertical", cellVerticalAlignment); _context.NewXmlWriter.WriteAttributeString("textRotation", "0"); _context.NewXmlWriter.WriteAttributeString("wrapText", "0"); _context.NewXmlWriter.WriteAttributeString("indent", "0"); @@ -903,8 +864,36 @@ protected override void GenerateCellXf() protected override async Task GenerateCellXfAsync() { + var headerHorizontalAlignment = _styleOptions.HeaderStyle?.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }; + + var headerVerticalAlignment = _styleOptions.HeaderStyle?.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }; + + var cellHorizontalAlignment = _styleOptions.HorizontalAlignment switch + { + HorizontalCellAlignment.Center => "center", + HorizontalCellAlignment.Right => "right", + _ => "general" + }; + + var cellVerticalAlignment = _styleOptions.VerticalAlignment switch + { + VerticalCellAlignment.Top => "top", + VerticalCellAlignment.Center => "center", + _ => "bottom" + }; + /* - * + * */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteEndElementAsync(); @@ -927,16 +916,10 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1"); await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - - var horizontalAlignment = _styleOptions.HeaderStyle?.HorizontalAlignment ?? DefaultHorizontalAlignment; - var horizontalAlignmentStr = horizontalAlignment.ToString().ToLowerInvariant(); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, horizontalAlignmentStr).ConfigureAwait(false); - - var verticalAlignment = _styleOptions.HeaderStyle?.VerticalAlignment ?? DefaultVerticalAlignment; - var verticalAlignmentStr = verticalAlignment.ToString().ToLowerInvariant(); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, verticalAlignmentStr).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, headerHorizontalAlignment).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, headerVerticalAlignment).ConfigureAwait(false); - var wrapHeader = (_styleOptions.HeaderStyle?.WrapText ?? false) ? "1" : "0"; + var wrapHeader = _styleOptions.HeaderStyle?.WrapText is true ? "1" : "0"; await _context.NewXmlWriter.WriteAttributeStringAsync(null, "wrapText", null, wrapHeader).ConfigureAwait(false); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "textRotation", null, "0"); @@ -960,8 +943,8 @@ protected override async Task GenerateCellXfAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "xfId", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); @@ -971,37 +954,8 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1"); await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style1HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style1HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style1HorizontalAlignment = "right"; - break; - default: - style1HorizontalAlignment = "general"; - break; - } - - string style1VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style1VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style1VerticalAlignment = "center"; - break; - default: - style1VerticalAlignment = "bottom"; - break; - } - - await _context.NewXmlWriter.WriteAttributeStringAsync(null,"horizontal", null, style1HorizontalAlignment).ConfigureAwait(false); - await _context.NewXmlWriter.WriteAttributeStringAsync(null,"vertical", null, style1VerticalAlignment).ConfigureAwait(false); - + await _context.NewXmlWriter.WriteAttributeStringAsync(null,"horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null,"vertical", null, cellVerticalAlignment).ConfigureAwait(false); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "textRotation", null, "0"); var wrapContent = _styleOptions.WrapCellContents ? "1" : "0"; @@ -1027,8 +981,8 @@ protected override async Task GenerateCellXfAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "14"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "xfId", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); @@ -1038,37 +992,8 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1"); await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style2HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style2HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style2HorizontalAlignment = "right"; - break; - default: - style2HorizontalAlignment = "general"; - break; - } - - string style2VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style2VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style2VerticalAlignment = "center"; - break; - default: - style2VerticalAlignment = "bottom"; - break; - } - - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, style2HorizontalAlignment); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, style2VerticalAlignment); - + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "textRotation", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "wrapText", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "indent", null, "0"); @@ -1091,8 +1016,8 @@ protected override async Task GenerateCellXfAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "0"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "xfId", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1"); @@ -1102,12 +1027,43 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteEndElementAsync(); await _context.NewXmlWriter.WriteEndElementAsync(); + /* + * + * + * + */ + await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteEndElementAsync().ConfigureAwait(false); + await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "protection", _context.OldXmlReader.NamespaceURI).ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false); + await _context.NewXmlWriter.WriteEndElementAsync().ConfigureAwait(false); + await _context.NewXmlWriter.WriteEndElementAsync().ConfigureAwait(false); + const int numFmtIndex = 166; - var index = 0; - foreach (var item in _context.ColumnsToApply) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * * @@ -1115,9 +1071,9 @@ protected override async Task GenerateCellXfAsync() * */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}"); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + _context.OldElementInfos.NumFmtCount).ToString()); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount}"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "xfId", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); @@ -1127,36 +1083,8 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1"); await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "alignment", _context.OldXmlReader.NamespaceURI); - string style3HorizontalAlignment; - switch (_styleOptions.HorizontalAlignment) - { - case HorizontalCellAlignment.Center: - style3HorizontalAlignment = "center"; - break; - case HorizontalCellAlignment.Right: - style3HorizontalAlignment = "right"; - break; - default: - style3HorizontalAlignment = "general"; - break; - } - - string style3VerticalAlignment; - switch (_styleOptions.VerticalAlignment) - { - case VerticalCellAlignment.Top: - style3VerticalAlignment = "top"; - break; - case VerticalCellAlignment.Center: - style3VerticalAlignment = "center"; - break; - default: - style3VerticalAlignment = "bottom"; - break; - } - - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, style3HorizontalAlignment); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, style3VerticalAlignment); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "textRotation", null, "0"); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "wrapText", null, "0"); @@ -1174,4 +1102,4 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteEndElementAsync(); } } -} \ No newline at end of file +} diff --git a/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs index 05f9f6ec..9f950410 100644 --- a/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs +++ b/src/MiniExcel/OpenXml/Styles/ISheetStyleBuilder.cs @@ -2,7 +2,7 @@ internal interface ISheetStyleBuilder { - SheetStyleBuildResult Build(); + void Build(); - Task BuildAsync(CancellationToken cancellationToken = default); + Task BuildAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs b/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs index a165a736..a31e134d 100644 --- a/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel/OpenXml/Styles/MinimalSheetStyleBuilder.cs @@ -1,25 +1,20 @@ namespace MiniExcelLibs.OpenXml.Styles; -internal class MinimalSheetStyleBuilder : SheetStyleBuilderBase +internal class MinimalSheetStyleBuilder(SheetStyleBuildContext context) : SheetStyleBuilderBase(context) { - internal static SheetStyleElementInfos GenerateElementInfos = new SheetStyleElementInfos + private static readonly SheetStyleElementInfos GenerateElementInfos = new() { - NumFmtCount = 0,//默认的NumFmt数量是0,但是会有根据ColumnsToApply动态生成的NumFmt + NumFmtCount = 0, FontCount = 1, FillCount = 1, BorderCount = 1, CellStyleXfCount = 1, - CellXfCount = 5 + CellXfCount = 6 }; - private readonly SheetStyleBuildContext _context; + private readonly SheetStyleBuildContext _context = context; - public MinimalSheetStyleBuilder(SheetStyleBuildContext context) : base(context) - { - _context = context; - } - - protected override SheetStyleElementInfos GetGenerateElementInfos() + protected internal override SheetStyleElementInfos GetGenerateElementInfos() { return GenerateElementInfos; } @@ -29,7 +24,7 @@ protected override void GenerateNumFmt() const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var item in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -38,7 +33,7 @@ protected override void GenerateNumFmt() */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "numFmt", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteAttributeString("numFmtId", (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); - _context.NewXmlWriter.WriteAttributeString("formatCode", item.Format); + _context.NewXmlWriter.WriteAttributeString("formatCode", item.Key); _context.NewXmlWriter.WriteFullEndElement(); } } @@ -47,7 +42,7 @@ protected override async Task GenerateNumFmtAsync() { const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var item in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -56,7 +51,7 @@ protected override async Task GenerateNumFmtAsync() */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "numFmt", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()); - await _context.NewXmlWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Format); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Key); await _context.NewXmlWriter.WriteFullEndElementAsync(); } } @@ -141,6 +136,7 @@ protected override void GenerateCellXf() * * * + * */ _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteFullEndElement(); @@ -154,20 +150,21 @@ protected override void GenerateCellXf() _context.NewXmlWriter.WriteFullEndElement(); _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); _context.NewXmlWriter.WriteFullEndElement(); + _context.NewXmlWriter.WriteStartElement(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); + _context.NewXmlWriter.WriteAttributeString("numFmtId", "21"); + _context.NewXmlWriter.WriteAttributeString("applyNumberFormat", "1"); + _context.NewXmlWriter.WriteFullEndElement(); const int numFmtIndex = 166; - var index = 0; - foreach (var item in _context.ColumnsToApply) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * * * + * */ await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); await _context.NewXmlWriter.WriteFullEndElementAsync(); @@ -191,19 +189,20 @@ protected override async Task GenerateCellXfAsync() await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); await _context.NewXmlWriter.WriteFullEndElementAsync(); await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); + await _context.NewXmlWriter.WriteEndElementAsync(); + await _context.NewXmlWriter.WriteStartElementAsync(_context.OldXmlReader.Prefix, "xf", _context.OldXmlReader.NamespaceURI); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21"); + await _context.NewXmlWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1"); await _context.NewXmlWriter.WriteFullEndElementAsync(); const int numFmtIndex = 166; - var index = 0; - foreach (var item in _context.ColumnsToApply) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * zipDictionary, MiniExcelZipArchive archive, Encoding encoding) : IDisposable { - private static readonly string _emptyStylesXml = ExcelOpenXmlUtils.MinifyXml - (@" - - - " - ); - - private readonly Dictionary _zipDictionary; - private readonly MiniExcelZipArchive _archive; - private readonly Encoding _encoding; - private readonly ICollection _columns; + private const string EmptyStylesXml = + """ + + + """; private StringReader _emptyStylesXmlStringReader; private ZipArchiveEntry _oldStyleXmlZipEntry; @@ -30,28 +25,73 @@ internal class SheetStyleBuildContext : IDisposable private bool _initialized; private bool _finalized; private bool _disposed; - - public SheetStyleBuildContext(Dictionary zipDictionary, MiniExcelZipArchive archive, Encoding encoding, ICollection columns) - { - _zipDictionary = zipDictionary; - _archive = archive; - _encoding = encoding; - _columns = columns; - } + + internal readonly SheetStyleFormatsCache SheetStyleFormatsCache = new(); public XmlReader OldXmlReader { get; private set; } public XmlWriter NewXmlWriter { get; private set; } public SheetStyleElementInfos OldElementInfos { get; private set; } public SheetStyleElementInfos GenerateElementInfos { get; private set; } - public IEnumerable ColumnsToApply { get; private set; } - public int CustomFormatCount { get; private set; } + public int CustomFormatCount => SheetStyleFormatsCache.FormatMappingsCount; + + public void Create(SheetStyleElementInfos generatedElementInfos) + { + SheetStyleElementInfos infos; + var styleEntry = archive.Mode == ZipArchiveMode.Update + ? archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) + : null; + + if (styleEntry is not null) + { + using var oldStyleXmlStream = styleEntry.Open(); + using var reader = XmlReader.Create(oldStyleXmlStream, new XmlReaderSettings { IgnoreWhitespace = true }); + infos = ReadSheetStyleElementInfos(reader); + } + else + { + infos = new SheetStyleElementInfos(); + } + + SheetStyleFormatsCache.SetCurrentIndex(infos.CellXfCount + generatedElementInfos.CellXfCount); + } + + public async Task CreateAsync(SheetStyleElementInfos generatedElementInfos, CancellationToken cancellationToken = default) + { + SheetStyleElementInfos infos; + var styleEntry = archive.Mode == ZipArchiveMode.Update + ? archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) + : null; + if (styleEntry is not null) + { +#if NET10_0_OR_GREATER + var oldStyleXmlStream = await styleEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using var disposableStream = oldStyleXmlStream.ConfigureAwait(false); +#else + using var oldStyleXmlStream = styleEntry.Open(); +#endif + using var reader = XmlReader.Create(oldStyleXmlStream, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); + infos = await ReadSheetStyleElementInfosAsync(reader, cancellationToken).ConfigureAwait(false); + } + else + { + infos = new SheetStyleElementInfos(); + } + + SheetStyleFormatsCache.SetCurrentIndex(infos.CellXfCount + generatedElementInfos.CellXfCount); + } + public void Initialize(SheetStyleElementInfos generateElementInfos) { if (_initialized) throw new InvalidOperationException("The context has been initialized."); - _oldStyleXmlZipEntry = _archive.Mode == ZipArchiveMode.Update ? _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) : null; + GenerateElementInfos = generateElementInfos; + + _oldStyleXmlZipEntry = archive.Mode == ZipArchiveMode.Update + ? archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) + : null; + if (_oldStyleXmlZipEntry != null) { using (var oldStyleXmlStream = _oldStyleXmlZipEntry.Open()) @@ -62,24 +102,20 @@ public void Initialize(SheetStyleElementInfos generateElementInfos) _oldXmlReaderStream = _oldStyleXmlZipEntry.Open(); OldXmlReader = XmlReader.Create(_oldXmlReaderStream, new XmlReaderSettings { IgnoreWhitespace = true }); - _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles + ".temp", CompressionLevel.Fastest); + _newStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles + ".temp", CompressionLevel.Fastest); } else { OldElementInfos = new SheetStyleElementInfos(); - _emptyStylesXmlStringReader = new StringReader(_emptyStylesXml); + _emptyStylesXmlStringReader = new StringReader(EmptyStylesXml); OldXmlReader = XmlReader.Create(_emptyStylesXmlStringReader, new XmlReaderSettings { IgnoreWhitespace = true }); - _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); + _newStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); } _newXmlWriterStream = _newStyleXmlZipEntry.Open(); - NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = _encoding }); - - GenerateElementInfos = generateElementInfos; - ColumnsToApply = SheetStyleBuilderHelper.GenerateStyleIds(OldElementInfos.CellXfCount + generateElementInfos.CellXfCount, _columns).ToArray();//这里暂时加ToArray,避免多次计算,如果有性能问题再考虑优化 - CustomFormatCount = ColumnsToApply.Count(); + NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = encoding }); _initialized = true; } @@ -88,40 +124,57 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C { if (_initialized) throw new InvalidOperationException("The context has already been initialized."); - - cancellationToken.ThrowIfCancellationRequested(); - - _oldStyleXmlZipEntry = _archive.Mode == ZipArchiveMode.Update ? _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) : null; + + GenerateElementInfos = generateElementInfos; + + _oldStyleXmlZipEntry = archive.Mode == ZipArchiveMode.Update + ? archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) + : null; + if (_oldStyleXmlZipEntry != null) { +#if NET10_0_OR_GREATER + var oldStyleXmlStream = await _oldStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); + await using (_ = oldStyleXmlStream.ConfigureAwait(false)) +#else using (var oldStyleXmlStream = _oldStyleXmlZipEntry.Open()) +#endif { OldElementInfos = await ReadSheetStyleElementInfosAsync(XmlReader.Create(oldStyleXmlStream, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }), cancellationToken); } + +#if NET10_0_OR_GREATER + _oldXmlReaderStream = await _oldStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#else _oldXmlReaderStream = _oldStyleXmlZipEntry.Open(); +#endif OldXmlReader = XmlReader.Create(_oldXmlReaderStream, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); - - _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles + ".temp", CompressionLevel.Fastest); + _newStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles + ".temp", CompressionLevel.Fastest); } else { OldElementInfos = new SheetStyleElementInfos(); - _emptyStylesXmlStringReader = new StringReader(_emptyStylesXml); + _emptyStylesXmlStringReader = new StringReader(EmptyStylesXml); OldXmlReader = XmlReader.Create(_emptyStylesXmlStringReader, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); - _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); + _newStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); } +#if NET10_0_OR_GREATER + _newXmlWriterStream = await _newStyleXmlZipEntry.OpenAsync(cancellationToken).ConfigureAwait(false); +#else _newXmlWriterStream = _newStyleXmlZipEntry.Open(); - NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = _encoding, Async = true }); - - GenerateElementInfos = generateElementInfos; - 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(); +#endif + NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = encoding, Async = true }); _initialized = true; } - + + public void UpdateFormatIds(ICollection mappings) + { + SheetStyleFormatsCache.AddMappings(mappings); + } + public void FinalizeAndUpdateZipDictionary() { if (!_initialized) @@ -151,13 +204,13 @@ public void FinalizeAndUpdateZipDictionary() if (_oldStyleXmlZipEntry == null) { - _zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry, ExcelContentTypes.Styles)); + zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry, ExcelContentTypes.Styles)); } else { _oldStyleXmlZipEntry?.Delete(); _oldStyleXmlZipEntry = null; - var finalStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); + var finalStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); using (var tempStream = _newStyleXmlZipEntry.Open()) using (var newStream = finalStyleXmlZipEntry.Open()) @@ -165,7 +218,7 @@ public void FinalizeAndUpdateZipDictionary() tempStream.CopyTo(newStream); } - _zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); + zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); _newStyleXmlZipEntry.Delete(); _newStyleXmlZipEntry = null; } @@ -209,13 +262,13 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella if (_oldStyleXmlZipEntry == null) { - _zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry, ExcelContentTypes.Styles)); + zipDictionary.Add(ExcelFileNames.Styles, new ZipPackageInfo(_newStyleXmlZipEntry, ExcelContentTypes.Styles)); } else { _oldStyleXmlZipEntry?.Delete(); _oldStyleXmlZipEntry = null; - var finalStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); + var finalStyleXmlZipEntry = archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); using (var tempStream = _newStyleXmlZipEntry.Open()) using (var newStream = finalStyleXmlZipEntry.Open()) @@ -223,7 +276,7 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella await tempStream.CopyToAsync(newStream, 4096, cancellationToken); } - _zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); + zipDictionary[ExcelFileNames.Styles] = new ZipPackageInfo(finalStyleXmlZipEntry, ExcelContentTypes.Styles); _newStyleXmlZipEntry.Delete(); _newStyleXmlZipEntry = null; } @@ -300,10 +353,9 @@ int GetCount() public void Dispose() { Dispose(true); - GC.SuppressFinalize(this); } - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (_disposed) return; diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleBuildResult.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleBuildResult.cs deleted file mode 100644 index bf0c9164..00000000 --- a/src/MiniExcel/OpenXml/Styles/SheetStyleBuildResult.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MiniExcelLibs.OpenXml.Styles; - -internal class SheetStyleBuildResult -{ - public SheetStyleBuildResult(Dictionary cellXfIdMap) - { - CellXfIdMap = cellXfIdMap; - } - - public Dictionary CellXfIdMap { get; set; } -} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderBase.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderBase.cs index f1f98a79..a7dd488d 100644 --- a/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderBase.cs +++ b/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderBase.cs @@ -2,9 +2,11 @@ namespace MiniExcelLibs.OpenXml.Styles; -internal abstract class SheetStyleBuilderBase : ISheetStyleBuilder +internal abstract class SheetStyleBuilderBase(SheetStyleBuildContext context) : ISheetStyleBuilder { - internal readonly static Dictionary _allElements = new Dictionary + private readonly SheetStyleBuildContext _context = context; + + private static readonly Dictionary AllElements = new() { ["numFmts"] = 0, ["fonts"] = 1, @@ -18,14 +20,8 @@ internal abstract class SheetStyleBuilderBase : ISheetStyleBuilder ["extLst"] = 9 }; - private readonly SheetStyleBuildContext _context; - public SheetStyleBuilderBase(SheetStyleBuildContext context) - { - _context = context; - } - - public virtual SheetStyleBuildResult Build() + public virtual void Build() { _context.Initialize(GetGenerateElementInfos()); @@ -80,12 +76,10 @@ public virtual SheetStyleBuildResult Build() } _context.FinalizeAndUpdateZipDictionary(); - - return new SheetStyleBuildResult(GetCellXfIdMap()); } // Todo: add CancellationToken to all methods called inside of BuildAsync - public virtual async Task BuildAsync(CancellationToken cancellationToken = default) + public virtual async Task BuildAsync(CancellationToken cancellationToken = default) { await _context.InitializeAsync(GetGenerateElementInfos(), cancellationToken); @@ -136,11 +130,9 @@ public virtual async Task BuildAsync(CancellationToken ca } await _context.FinalizeAndUpdateZipDictionaryAsync(cancellationToken); - - return new SheetStyleBuildResult(GetCellXfIdMap()); } - protected abstract SheetStyleElementInfos GetGenerateElementInfos(); + protected internal abstract SheetStyleElementInfos GetGenerateElementInfos(); protected virtual void WriteAttributes(string element) { @@ -266,36 +258,36 @@ protected virtual async Task WriteAttributesAsync(string element, CancellationTo protected virtual void GenerateElementBeforStartElement() { - if (!_allElements.TryGetValue(_context.OldXmlReader.LocalName, out var elementIndex)) + if (!AllElements.TryGetValue(_context.OldXmlReader.LocalName, out var elementIndex)) { return; } - if (!_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts && _allElements["numFmts"] < elementIndex) + if (!_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts && AllElements["numFmts"] < elementIndex) { GenerateNumFmts(); _context.GenerateElementInfos.ExistsNumFmts = true; } - else if (!_context.OldElementInfos.ExistsFonts && !_context.GenerateElementInfos.ExistsFonts && _allElements["fonts"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFonts && !_context.GenerateElementInfos.ExistsFonts && AllElements["fonts"] < elementIndex) { GenerateFonts(); _context.GenerateElementInfos.ExistsFonts = true; } - else if (!_context.OldElementInfos.ExistsFills && !_context.GenerateElementInfos.ExistsFills && _allElements["fills"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFills && !_context.GenerateElementInfos.ExistsFills && AllElements["fills"] < elementIndex) { GenerateFills(); _context.GenerateElementInfos.ExistsFills = true; } - else if (!_context.OldElementInfos.ExistsBorders && !_context.GenerateElementInfos.ExistsBorders && _allElements["borders"] < elementIndex) + else if (!_context.OldElementInfos.ExistsBorders && !_context.GenerateElementInfos.ExistsBorders && AllElements["borders"] < elementIndex) { GenerateBorders(); _context.GenerateElementInfos.ExistsBorders = true; } - else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GenerateElementInfos.ExistsCellStyleXfs && _allElements["cellStyleXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GenerateElementInfos.ExistsCellStyleXfs && AllElements["cellStyleXfs"] < elementIndex) { GenerateCellStyleXfs(); _context.GenerateElementInfos.ExistsCellStyleXfs = true; } - else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GenerateElementInfos.ExistsCellXfs && _allElements["cellXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GenerateElementInfos.ExistsCellXfs && AllElements["cellXfs"] < elementIndex) { GenerateCellXfs(); _context.GenerateElementInfos.ExistsCellXfs = true; @@ -304,36 +296,36 @@ protected virtual void GenerateElementBeforStartElement() protected virtual async Task GenerateElementBeforStartElementAsync() { - if (!_allElements.TryGetValue(_context.OldXmlReader.LocalName, out var elementIndex)) + if (!AllElements.TryGetValue(_context.OldXmlReader.LocalName, out var elementIndex)) { return; } - if (!_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts && _allElements["numFmts"] < elementIndex) + if (!_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts && AllElements["numFmts"] < elementIndex) { await GenerateNumFmtsAsync(); _context.GenerateElementInfos.ExistsNumFmts = true; } - else if (!_context.OldElementInfos.ExistsFonts && !_context.GenerateElementInfos.ExistsFonts && _allElements["fonts"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFonts && !_context.GenerateElementInfos.ExistsFonts && AllElements["fonts"] < elementIndex) { await GenerateFontsAsync(); _context.GenerateElementInfos.ExistsFonts = true; } - else if (!_context.OldElementInfos.ExistsFills && !_context.GenerateElementInfos.ExistsFills && _allElements["fills"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFills && !_context.GenerateElementInfos.ExistsFills && AllElements["fills"] < elementIndex) { await GenerateFillsAsync(); _context.GenerateElementInfos.ExistsFills = true; } - else if (!_context.OldElementInfos.ExistsBorders && !_context.GenerateElementInfos.ExistsBorders && _allElements["borders"] < elementIndex) + else if (!_context.OldElementInfos.ExistsBorders && !_context.GenerateElementInfos.ExistsBorders && AllElements["borders"] < elementIndex) { await GenerateBordersAsync(); _context.GenerateElementInfos.ExistsBorders = true; } - else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GenerateElementInfos.ExistsCellStyleXfs && _allElements["cellStyleXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GenerateElementInfos.ExistsCellStyleXfs && AllElements["cellStyleXfs"] < elementIndex) { await GenerateCellStyleXfsAsync(); _context.GenerateElementInfos.ExistsCellStyleXfs = true; } - else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GenerateElementInfos.ExistsCellXfs && _allElements["cellXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GenerateElementInfos.ExistsCellXfs && AllElements["cellXfs"] < elementIndex) { await GenerateCellXfsAsync(); _context.GenerateElementInfos.ExistsCellXfs = true; @@ -568,14 +560,4 @@ protected virtual async Task GenerateCellXfsAsync() protected abstract void GenerateCellXf(); protected abstract Task GenerateCellXfAsync(); - - private Dictionary GetCellXfIdMap() - { - var result = new Dictionary(); - for (int i = 0; i < _context.GenerateElementInfos.CellXfCount; i++) - { - result.Add(i.ToString(), (_context.OldElementInfos.CellXfCount + i).ToString()); - } - return result; - } -} \ No newline at end of file +} diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs deleted file mode 100644 index fcafd2de..00000000 --- a/src/MiniExcel/OpenXml/Styles/SheetStyleBuilderHelper.cs +++ /dev/null @@ -1,21 +0,0 @@ -using MiniExcelLibs.Attributes; -using MiniExcelLibs.Utils; - -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++; - } - } -} \ No newline at end of file diff --git a/src/MiniExcel/OpenXml/Styles/SheetStyleFormatsCache.cs b/src/MiniExcel/OpenXml/Styles/SheetStyleFormatsCache.cs new file mode 100644 index 00000000..6e17e09e --- /dev/null +++ b/src/MiniExcel/OpenXml/Styles/SheetStyleFormatsCache.cs @@ -0,0 +1,31 @@ +using MiniExcelLibs.Utils; + +namespace MiniExcelLibs.OpenXml.Styles; + +internal class SheetStyleFormatsCache +{ + private readonly Dictionary _formatMappings = []; + private int _stylesCount; + + internal int FormatMappingsCount => _formatMappings.Count; + internal IEnumerable> FormatMappings => _formatMappings; + + public void AddMappings(IEnumerable mappings) + { + foreach (var mapping in mappings.Where(map => map is { ExcelIgnore: false })) + { + if (!string.IsNullOrWhiteSpace(mapping!.ExcelFormat) && new ExcelNumberFormat(mapping.ExcelFormat).IsValid) + { + if (!_formatMappings.TryGetValue(mapping.ExcelFormat, out var formatId)) + { + formatId = _stylesCount++; + _formatMappings.Add(mapping.ExcelFormat, formatId); + } + + mapping.ExcelFormatId = formatId; + } + } + } + + internal void SetCurrentIndex(int index) => _stylesCount = index; +} diff --git a/src/MiniExcel/Utils/CustomPropertyHelper.cs b/src/MiniExcel/Utils/CustomPropertyHelper.cs index 8cba17d2..0c7b025d 100644 --- a/src/MiniExcel/Utils/CustomPropertyHelper.cs +++ b/src/MiniExcel/Utils/CustomPropertyHelper.cs @@ -21,7 +21,7 @@ internal class ExcelColumnInfo public string ExcelIndexName { get; internal set; } public bool ExcelHidden { get; internal set; } public bool ExcelIgnore { get; internal set; } - public int ExcelFormatId { get; internal set; } + public int ExcelFormatId { get; internal set; } = -1; public ColumnType ExcelColumnType { get; internal set; } public Func CustomFormatter { get; set; } } diff --git a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs index c33efa33..1a82c6aa 100644 --- a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs @@ -25,6 +25,9 @@ public class MiniExcelIssueAsyncTests(ITestOutputHelper output) [Fact] public async Task Issue255() { + var dt1 = new DateTime(2021, 01, 01); + var dt2 = new DateTime(2022, 01, 01); + //tempalte { var templatePath = PathHelper.GetFile("xlsx/TestsIssue255_Template.xlsx"); @@ -33,7 +36,7 @@ public async Task Issue255() { Issue255DTO = new[] { - new Issue255DTO { Time = new DateTime(2021, 01, 01), Time2 = new DateTime(2021, 01, 01) } + new Issue255DTO { Time = dt1, Time2 = dt2 } } }; @@ -42,27 +45,28 @@ public async Task Issue255() var rows = q.ToList(); Assert.Equal("2021", rows[1].A.ToString()); - Assert.Equal("2021", rows[1].B.ToString()); + Assert.Equal("2022", rows[1].B.ToString()); } //saveas { - using var path = AutoDeletingPath.Create(); - var value = new[] - { - new Issue255DTO - { - Time = new DateTime(2021, 01, 01), - Time2 = new DateTime(2021, 01, 01) - } - }; - var rowsWritten = await MiniExcel.SaveAsAsync(path.ToString(), value); + using var ms = new MemoryStream(); + Issue255DTO[] value = + [ + new() { Time = dt1, Time2 = dt2 } + ]; + + var rowsWritten = MiniExcel.SaveAs(ms, value); Assert.Single(rowsWritten); Assert.Equal(1, rowsWritten[0]); - - var q = await MiniExcel.QueryAsync(path.ToString()); - var rows = q.ToList(); - Assert.Equal("2021", rows[1].A.ToString()); - Assert.Equal("2021", rows[1].B.ToString()); + + ms.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(ms); + + var cells = package.Workbook.Worksheets[0].Cells; + Assert.Equal(dt1, DateTime.FromOADate((double)cells["A2"].Value)); + Assert.Equal("2021", cells["A2"].Text); + Assert.Equal(dt2, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal("2022", cells["B2"].Text); } } @@ -265,28 +269,23 @@ public async Task Issue241() // xlsx { + var date1 = new DateTime(2021, 01, 04); + var date2 = new DateTime(2020, 04, 05); + using var file = AutoDeletingPath.Create(); var path = file.ToString(); - var rowsWritten = await MiniExcel.SaveAsAsync(path, value); - + var rowsWritten = MiniExcel.SaveAs(path, value); + Assert.Single(rowsWritten); Assert.Equal(2, rowsWritten[0]); - { - var q = await MiniExcel.QueryAsync(path, true); - var rows = q.ToList(); - - Assert.Equal(rows[0].InDate, "01 04, 2021"); - Assert.Equal(rows[1].InDate, "04 05, 2020"); - } + using var package = new ExcelPackage(path); + var cells = package.Workbook.Worksheets.First().Cells; - { - var q = await MiniExcel.QueryAsync(path); - var rows = q.ToList(); - - Assert.Equal(rows[0].InDate, new DateTime(2021, 01, 04)); - Assert.Equal(rows[1].InDate, new DateTime(2020, 04, 05)); - } + Assert.Equal(date1, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal("01 04, 2021", cells["B2"].Text); + Assert.Equal(date2, DateTime.FromOADate((double)cells["B3"].Value)); + Assert.Equal("04 05, 2020", cells["B3"].Text); } } diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index fe5a0922..baf21e65 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -376,15 +376,16 @@ public void TestIssue369() public void TestIssueI4ZYUU() { using var path = AutoDeletingPath.Create(); - TestIssueI4ZYUUDto[] value = [new() { MyProperty = "1", MyProperty2 = new DateTime(2022, 10, 15) }]; + + var dt = new DateTime(2022, 10, 15); + TestIssueI4ZYUUDto[] value = [new() { MyProperty = "1", MyProperty2 = dt }]; MiniExcel.SaveAs(path.ToString(), value); - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("2022-10", rows[1].B); - - using var workbook = new XLWorkbook(path.ToString()); + using var workbook = new ClosedXML.Excel.XLWorkbook(path.ToString()); var ws = workbook.Worksheet(1); + Assert.Equal(dt, ws.Cell(2, "B").Value.GetDateTime()); + Assert.Equal("2022-10", ws.Cell(2, "B").GetFormattedString()); Assert.True(ws.Column("A").Width > 0); Assert.True(ws.Column("B").Width > 0); } @@ -1172,16 +1173,21 @@ public void TestIssueI49RZH() { // xlsx { + var dt = new DateTime(2022, 01, 22); + using var path = AutoDeletingPath.Create(); - var value = new[] - { - new TestIssueI49RZHDto{ dd = DateTimeOffset.Parse("2022-01-22")}, - new TestIssueI49RZHDto{ dd = null} - }; - MiniExcel.SaveAs(path.ToString(), value); + TestIssueI49RZHDto[] value = + [ + new() { dd = dt }, + new() { dd = null } + ]; + MiniExcel.SaveAs(path.FilePath, value, overwriteFile: true); - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("2022-01-22", rows[1].A); + using var package = new ExcelPackage(path.ToString()); + var cells = package.Workbook.Worksheets[0].Cells; + + Assert.Equal(dt, DateTime.FromOADate((double)cells["A2"].Value)); + Assert.Equal("22-01-2022", cells["A2"].Text); } //TODO:CSV @@ -1195,13 +1201,13 @@ public void TestIssueI49RZH() MiniExcel.SaveAs(path.ToString(), value); var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("2022-01-22", rows[1].A); + Assert.Equal("22-01-2022", rows[1].A); } } private class TestIssueI49RZHDto { - [ExcelFormat("yyyy-MM-dd")] + [ExcelFormat("dd-MM-yyyy")] public DateTimeOffset? dd { get; set; } } @@ -1216,13 +1222,17 @@ public void TestIssue312() using var path = AutoDeletingPath.Create(); TestIssue312Dto[] value = [ - new() { Value = 12345.6789}, - new() { Value = null} + new() { Value = 12_345.6789 }, + new() { Value = null } ]; MiniExcel.SaveAs(path.ToString(), value); - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("12,345.68", rows[1].A); + using var package = new ExcelPackage(path.ToString()); + var cells = package.Workbook.Worksheets[0].Cells; + + var fmt = cells["A2"].Style.Numberformat.Format; + Assert.Equal(12_345.68.ToString(fmt), cells["A2"].Text); + Assert.Equal(12_345.6789, (double)cells["A2"].Value); } //TODO:CSV @@ -1877,6 +1887,9 @@ private static void CsvToXlsx(string csvPath, string xlsxPath) [Fact] public void Issue255() { + var dt1 = new DateTime(2021, 01, 01); + var dt2 = new DateTime(2022, 01, 01); + //template { var templatePath = PathHelper.GetFile("xlsx/TestsIssue255_Template.xlsx"); @@ -1885,24 +1898,34 @@ public void Issue255() { Issue255DTO = new[] { - new Issue255DTO { Time = new DateTime(2021, 01, 01), Time2 = new DateTime(2021, 01, 01) } + new Issue255DTO { Time = dt1, Time2 = dt2 } } }; MiniExcel.SaveAsByTemplate(path.ToString(), templatePath, value); var rows = MiniExcel.Query(path.ToString()).ToList(); Assert.Equal("2021", rows[1].A.ToString()); - Assert.Equal("2021", rows[1].B.ToString()); + Assert.Equal("2022", rows[1].B.ToString()); } //saveas { - using var path = AutoDeletingPath.Create(); - var value = new[] - { - new Issue255DTO { Time = new DateTime(2021, 01, 01) } - }; - MiniExcel.SaveAs(path.ToString(), value); - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("2021", rows[1].A.ToString()); + using var ms = new MemoryStream(); + Issue255DTO[] value = + [ + new() { Time = dt1, Time2 = dt2 } + ]; + + var rowsWritten = MiniExcel.SaveAs(ms, value); + Assert.Single(rowsWritten); + Assert.Equal(1, rowsWritten[0]); + + ms.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(ms); + + var cells = package.Workbook.Worksheets[0].Cells; + Assert.Equal(dt1, DateTime.FromOADate((double)cells["A2"].Value)); + Assert.Equal("2021", cells["A2"].Text); + Assert.Equal(dt2, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal("2022", cells["B2"].Text); } } @@ -2053,11 +2076,10 @@ private class Issue243Dto [Fact] public void Issue241() { - Issue241Dto[] value = [ - new() { Name = "Jack", InDate = new DateTime(2021,01,04) }, - new() { Name = "Henry", InDate = new DateTime(2020,04,05) } + new() { Name = "Jack", InDate = new DateTime(2021, 01, 04) }, + new() { Name = "Henry", InDate = new DateTime(2020, 04, 05) } ]; // csv @@ -2080,20 +2102,23 @@ public void Issue241() // xlsx { - using var path = AutoDeletingPath.Create(); - MiniExcel.SaveAs(path.ToString(), value); + var date1 = new DateTime(2021, 01, 04); + var date2 = new DateTime(2020, 04, 05); - { - var rows = MiniExcel.Query(path.ToString(), true).ToList(); - Assert.Equal(rows[0].InDate, "01 04, 2021"); - Assert.Equal(rows[1].InDate, "04 05, 2020"); - } + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); + var rowsWritten = MiniExcel.SaveAs(path, value); - { - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal(rows[0].InDate, new DateTime(2021, 01, 04)); - Assert.Equal(rows[1].InDate, new DateTime(2020, 04, 05)); - } + Assert.Single(rowsWritten); + Assert.Equal(2, rowsWritten[0]); + + using var package = new ExcelPackage(path); + var cells = package.Workbook.Worksheets.First().Cells; + + Assert.Equal(date1, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal("01 04, 2021", cells["B2"].Text); + Assert.Equal(date2, DateTime.FromOADate((double)cells["B3"].Value)); + Assert.Equal("04 05, 2020", cells["B3"].Text); } } diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs index be0fd956..7adcfc64 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlAsyncTests.cs @@ -605,7 +605,7 @@ public async Task SaveAsFileWithDimension() await using var stream = File.OpenRead(path); var d = await stream.QueryAsync(); var rows = d.ToList(); - Assert.Single(rows); + Assert.Empty(rows); } await MiniExcel.SaveAsAsync(path, table, printHeader: false, overwriteFile: true); Assert.Equal("A1", Helpers.GetFirstSheetDimensionRefValue(path)); @@ -1653,4 +1653,325 @@ class Issue951 public object this[string test] => new(); } -} \ No newline at end of file + + [Fact] + public async Task NumericFormattingWithMiniExcelFormatAttributeTest() + { + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); + + NumericFormattingTestDto[] testData = + [ + new(currency: 1234.56m, + alignedCurrency: 9876.54m, + percentage: 0.85m, + scientificNotation: 1234567890.123d, + fixedDecimal: 42.123456m, + phoneNumber: 5551234567, + veryLongNumber: 155043269579349, + customFormat: 999.999 + ), + + new(currency: -500.00m, + alignedCurrency: -250.75m, + percentage: 0.42m, + scientificNotation: 987654321.456d, + fixedDecimal: 15.5m, + phoneNumber: 4155552671, + veryLongNumber: 20573068629711152, + customFormat: 100.012 + ) + ]; + + await MiniExcel.SaveAsAsync(path, testData); + + using var package = new ExcelPackage(path); + var cells = package.Workbook.Worksheets[0].Cells; + + // Verify headers + Assert.Equal("Currency", cells["A1"].Value); + Assert.Equal("AlignedCurrency", cells["B1"].Value); + Assert.Equal("Percentage", cells["C1"].Value); + Assert.Equal("ScientificNotation", cells["D1"].Value); + Assert.Equal("FixedDecimal", cells["F1"].Value); + Assert.Equal("PhoneNumber", cells["G1"].Value); + Assert.Equal("VeryLongNumber", cells["H1"].Value); + Assert.Equal("CustomFormat", cells["I1"].Value); + + // Verify first row of data + Assert.Equal(1234.56, cells["A2"].Value); + Assert.Equal("\"$\"#,##0.00", cells["A2"].Style.Numberformat.Format); + + Assert.Equal(9876.54, cells["B2"].Value); + Assert.Equal("$#,##0.00_);($#,##0.00)", cells["B2"].Style.Numberformat.Format); + + Assert.Equal(0.85, cells["C2"].Value); + Assert.Equal("0%", cells["C2"].Style.Numberformat.Format); + + Assert.Equal(1234567890.123, cells["D2"].Value); + Assert.Equal("0.00E+00", cells["D2"].Style.Numberformat.Format); + + Assert.Equal(42.123456, cells["F2"].Value); + Assert.Equal("0.000000", cells["F2"].Style.Numberformat.Format); + + Assert.Equal(5551234567, Convert.ToInt64(cells["G2"].Value)); + Assert.Equal("[<=9999999]###-####;(###) ###-####", cells["G2"].Style.Numberformat.Format); + + Assert.Equal(155043269579349, Convert.ToInt64(cells["H2"].Value)); + Assert.Equal("#", cells["H2"].Style.Numberformat.Format); + + Assert.Equal(999.999, cells["I2"].Value); + Assert.Equal("0.000", cells["I2"].Style.Numberformat.Format); + + // Verify second row of data + Assert.Equal(-500.00, cells["A3"].Value); + Assert.Equal("\"$\"#,##0.00", cells["A3"].Style.Numberformat.Format); + + Assert.Equal(-250.75, cells["B3"].Value); + Assert.Equal("$#,##0.00_);($#,##0.00)", cells["B3"].Style.Numberformat.Format); + + Assert.Equal(0.42, cells["C3"].Value); + Assert.Equal("0%", cells["C3"].Style.Numberformat.Format); + + Assert.Equal(987654321.456, cells["D3"].Value); + Assert.Equal("0.00E+00", cells["D3"].Style.Numberformat.Format); + + Assert.Equal(15.5, cells["F3"].Value); + Assert.Equal("0.000000", cells["F3"].Style.Numberformat.Format); + + Assert.Equal(4155552671, Convert.ToInt64(cells["G3"].Value)); + Assert.Equal("[<=9999999]###-####;(###) ###-####", cells["G3"].Style.Numberformat.Format); + + Assert.Equal(20573068629711152, Convert.ToInt64(cells["H3"].Value)); + Assert.Equal("#", cells["H3"].Style.Numberformat.Format); + + Assert.Equal(100.012, cells["I3"].Value); + Assert.Equal("0.000", cells["I3"].Style.Numberformat.Format); + } + + /// + /// Test class with multiple numeric properties using MiniExcelFormatAttribute + /// to verify that formatting is correctly applied during Excel export. + /// + private class NumericFormattingTestDto( + decimal currency, + decimal alignedCurrency, + decimal percentage, + double scientificNotation, + decimal fixedDecimal, + long phoneNumber, + long veryLongNumber, + double customFormat) + { + + /// + /// Regular currency format with 2 decimal places + /// + [ExcelFormat("\"$\"#,##0.00")] + public decimal Currency { get; set; } = currency; + + /// + /// Currency format with 2 decimal places, parentheses for negatives + /// + [ExcelFormat("$#,##0.00_);($#,##0.00)")] + public decimal AlignedCurrency { get; set; } = alignedCurrency; + + /// + /// Percentage format with 0 decimal places + /// + [ExcelFormat("0%")] + public decimal Percentage { get; set; } = percentage; + + /// + /// Scientific notation format with 2 decimal places + /// + [ExcelFormat("0.00E+00")] + public double ScientificNotation { get; set; } = scientificNotation; + + [ExcelFormat("0.00E+00"), ExcelHidden] + public double ScientificNotationDuplicate { get; set; } = scientificNotation; + + /// + /// Fixed decimal places (6 decimal places) + /// + [ExcelFormat("0.000000")] + public decimal FixedDecimal { get; set; } = fixedDecimal; + + /// + /// Phone number format + /// + [ExcelFormat("[<=9999999]###-####;(###) ###-####")] + public long PhoneNumber { get; set; } = phoneNumber; + + /// + /// Simple integer format that shows the number in its full length (no scientific notation) + /// + [ExcelFormat("#")] + public long VeryLongNumber { get; set; } = veryLongNumber; + + /// + /// Simple decimal format with 3 decimal places + /// + [ExcelFormat("0.000")] + public double CustomFormat { get; set; } = customFormat; + } + + [Fact] + public async Task DateTimeFormattingWithMiniExcelFormatAttributeTest() + { + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); + + // Create fixed DateTime values for consistent testing + var baseDate = new DateTime(2026, 5, 8, 14, 30, 45); + var baseTime = new TimeSpan(14, 30, 45); + + DateTimeFormattingTestDto[] testData = + [ + new( + shortDate: baseDate, + longDate: baseDate, + dateWithTime: baseDate, + timeOnly: baseTime, + isoDateTime: baseDate, + customDateTime: baseDate, + monthYear: baseDate + ), + new( + shortDate: new DateTime(2020, 12, 25), + longDate: new DateTime(2020, 12, 25), + dateWithTime: new DateTime(2020, 12, 25, 8, 15, 30), + timeOnly: new TimeSpan(8, 15, 30), + isoDateTime: new DateTime(2020, 12, 25, 8, 15, 30), + customDateTime: new DateTime(2020, 12, 25, 8, 15, 30), + monthYear: new DateTime(2020, 12, 25) + ) + ]; + + await MiniExcel.SaveAsAsync(path, testData); + + using var package = new ExcelPackage(path); + var cells = package.Workbook.Worksheets[0].Cells; + + // Verify headers + Assert.Equal("ShortDate", cells["A1"].Value); + Assert.Equal("LongDate", cells["B1"].Value); + Assert.Equal("DateWithTime", cells["C1"].Value); + Assert.Equal("TimeOnly", cells["D1"].Value); + Assert.Equal("IsoDateTime", cells["E1"].Value); + Assert.Equal("CustomDateTime", cells["F1"].Value); + Assert.Equal("MonthYear", cells["G1"].Value); + + // Verify first row + Assert.Equal(baseDate, GetDateTime(cells["A2"].Value)); + Assert.Equal("mm/dd/yyyy", cells["A2"].Style.Numberformat.Format); + + // Long date format (dddd, mmmm dd, yyyy) + Assert.Equal(baseDate, GetDateTime(cells["B2"].Value)); + Assert.Equal("dddd, mmmm dd, yyyy", cells["B2"].Style.Numberformat.Format); + + // Date with time (yyyy-mm-dd hh:mm:ss) + Assert.Equal(baseDate, GetDateTime(cells["C2"].Value)); + Assert.Equal("yyyy-mm-dd hh:mm:ss", cells["C2"].Style.Numberformat.Format); + + // Time only format ([h]:mm:ss) + Assert.Equal(baseTime, GetDateTime(cells["D2"].Value).TimeOfDay); + Assert.Equal("[h]:mm:ss", cells["D2"].Style.Numberformat.Format); + + // ISO 8601 format (yyyy-mm-ddThh:mm:ss) + Assert.Equal(baseDate, GetDateTime(cells["E2"].Value)); + Assert.Equal("yyyy-mm-dd\"T\"hh:mm:ss", cells["E2"].Style.Numberformat.Format); + + // Custom format (dd.mm.yyyy hh:mm) + Assert.Equal(baseDate, GetDateTime(cells["F2"].Value)); + Assert.Equal("dd.mm.yyyy hh:mm", cells["F2"].Style.Numberformat.Format); + + // Month/Year format (mmmm yyyy) + Assert.Equal(baseDate, GetDateTime(cells["G2"].Value)); + Assert.Equal("mmmm yyyy", cells["G2"].Style.Numberformat.Format); + + // Verify second row + var secondRowDate = new DateTime(2020, 12, 25); + var secondRowTime = new TimeSpan(8, 15, 30); + + Assert.Equal(secondRowDate, GetDateTime(cells["A3"].Value)); + Assert.Equal("mm/dd/yyyy", cells["A3"].Style.Numberformat.Format); + + Assert.Equal(secondRowDate, GetDateTime(cells["B3"].Value)); + Assert.Equal("dddd, mmmm dd, yyyy", cells["B3"].Style.Numberformat.Format); + + Assert.Equal(new DateTime(2020, 12, 25, 8, 15, 30), GetDateTime(cells["C3"].Value)); + Assert.Equal("yyyy-mm-dd hh:mm:ss", cells["C3"].Style.Numberformat.Format); + + Assert.Equal(secondRowTime, GetDateTime(cells["D3"].Value).TimeOfDay); + Assert.Equal("[h]:mm:ss", cells["D3"].Style.Numberformat.Format); + + Assert.Equal(new DateTime(2020, 12, 25, 8, 15, 30), GetDateTime(cells["E3"].Value)); + Assert.Equal("yyyy-mm-dd\"T\"hh:mm:ss", cells["E3"].Style.Numberformat.Format); + + Assert.Equal(new DateTime(2020, 12, 25, 8, 15, 30), GetDateTime(cells["F3"].Value)); + Assert.Equal("dd.mm.yyyy hh:mm", cells["F3"].Style.Numberformat.Format); + + Assert.Equal(secondRowDate, GetDateTime(cells["G3"].Value)); + Assert.Equal("mmmm yyyy", cells["G3"].Style.Numberformat.Format); + return; + + static DateTime GetDateTime(object value) => DateTime.FromOADate((double)value); + } + + /// + /// Test class with multiple date and time properties using MiniExcelFormatAttribute + /// to verify that date/time formatting is correctly applied during Excel export. + /// + private class DateTimeFormattingTestDto( + DateTime shortDate, + DateTime longDate, + DateTime dateWithTime, + TimeSpan timeOnly, + DateTime isoDateTime, + DateTime customDateTime, + DateTime monthYear) + { + /// + /// Short date format (mm/dd/yyyy) + /// + [ExcelFormat("mm/dd/yyyy")] + public DateTime ShortDate { get; set; } = shortDate; + + /// + /// Long date format (dddd, mmmm dd, yyyy) + /// + [ExcelFormat("dddd, mmmm dd, yyyy")] + public DateTime LongDate { get; set; } = longDate; + + /// + /// Date with time format (yyyy-mm-dd hh:mm:ss) + /// + [ExcelFormat("yyyy-mm-dd hh:mm:ss")] + public DateTime DateWithTime { get; set; } = dateWithTime; + + /// + /// Time only format ([h]:mm:ss) + /// + [ExcelFormat("[h]:mm:ss")] + public TimeSpan TimeOnly { get; set; } = timeOnly; + + /// + /// ISO 8601 datetime format (yyyy-mm-ddThh:mm:ss) + /// + [ExcelFormat("yyyy-mm-dd\"T\"hh:mm:ss")] + public DateTime IsoDateTime { get; set; } = isoDateTime; + + /// + /// Custom European format (dd.mm.yyyy hh:mm) + /// + [ExcelFormat("dd.mm.yyyy hh:mm")] + public DateTime CustomDateTime { get; set; } = customDateTime; + + /// + /// Month and year format (mmmm yyyy) + /// + [ExcelFormat("mmmm yyyy")] + public DateTime MonthYear { get; set; } = monthYear; + } +} diff --git a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs index 47001a2a..af2bdb22 100644 --- a/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcelTests/MiniExcelOpenXmlTests.cs @@ -641,7 +641,7 @@ public void SaveAsFileWithDimension() { using var stream = File.OpenRead(path); var rows = stream.Query().ToList(); - Assert.Single(rows); + Assert.Empty(rows); } MiniExcel.SaveAs(path, table, printHeader: false, overwriteFile: true);