diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs index fb754da8..c64929ff 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriteAdapterAsync.cs @@ -1,6 +1,6 @@ namespace MiniExcelLib.Core.Abstractions; -public interface IMiniExcelWriteAdapterAsync +public interface IMiniExcelWriteAdapterAsync : IAsyncDisposable { Task?> GetColumnsAsync(); IAsyncEnumerable GetRowsAsync(List mappings, CancellationToken cancellationToken); diff --git a/src/MiniExcel.Core/Helpers/Polyfills.cs b/src/MiniExcel.Core/Helpers/Polyfills.cs index c84b1421..03fd1bf2 100644 --- a/src/MiniExcel.Core/Helpers/Polyfills.cs +++ b/src/MiniExcel.Core/Helpers/Polyfills.cs @@ -15,10 +15,17 @@ public static class Polyfills return dictionary.TryGetValue(key, out var value) ? value : defaultValue; } + [EditorBrowsable(EditorBrowsableState.Advanced)] + public static void Deconstruct(this KeyValuePair kvp, out TKey key, out TValue value) + { + key = kvp.Key; + value = kvp.Value; + } + extension(Math) { [EditorBrowsable(EditorBrowsableState.Advanced)] - public static TNumber Clamp(TNumber value, TNumber min, TNumber max) where TNumber : unmanaged, IComparable + public static TNumber Clamp(TNumber value, TNumber min, TNumber max) where TNumber : IComparable { if (value.CompareTo(min) < 0) return min; if (value.CompareTo(max) > 0) return max; diff --git a/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs b/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs index 47e26380..45cbce09 100644 --- a/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs +++ b/src/MiniExcel.Core/Reflection/MiniExcelColumnMapping.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using MiniExcelLib.Core.Attributes; namespace MiniExcelLib.Core.Reflection; @@ -16,7 +17,10 @@ public class MiniExcelColumnMapping public string? ExcelIndexName { get; internal set; } public bool ExcelHiddenColumn { get; internal set; } public bool ExcelIgnoreColumn { 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; } + + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetFormatId(int fmtId) => ExcelFormatId = fmtId; } \ No newline at end of file diff --git a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs index 32ed1509..28a6c7e1 100644 --- a/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs +++ b/src/MiniExcel.Core/WriteAdapters/AsyncEnumerableWriteAdapter.cs @@ -1,13 +1,13 @@ namespace MiniExcelLib.Core.WriteAdapters; -internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync, IAsyncDisposable +internal sealed class AsyncEnumerableWriteAdapter(IAsyncEnumerable values, MiniExcelBaseConfiguration configuration) : IMiniExcelWriteAdapterAsync { private readonly IAsyncEnumerable _values = values; private readonly MiniExcelBaseConfiguration _configuration = configuration; private IAsyncEnumerator? _enumerator; private bool _empty; - private bool _disposed = false; + private bool _disposed; public async Task?> GetColumnsAsync() diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs index bd7bc3ac..9e44f8af 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.DefaultOpenXml.cs @@ -6,10 +6,14 @@ namespace MiniExcelLib.OpenXml; internal partial class OpenXmlWriter { + private const string DefaultCellStyleIndex = "0"; + private const string HeaderCellStyleIndex = "1"; + private const string RegularCellStyleIndex = "2"; + private static readonly DateTime ExcelZeroDate = new(1899, 12, 31); + private readonly Dictionary _zipDictionary = []; - private Dictionary _cellXfIdMap = []; - private IEnumerable<(SheetDto, object?)> GetSheets() + private IEnumerable<(SheetDto Sheet, object? Data)> GetSheets() { var sheetId = 0; if (_value is IDictionary dictionary) @@ -153,29 +157,39 @@ private string GetPanes() return sb.ToString(); } - private (string, string?, string?) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) + private (string StyleIndex, string? DataType, string? CellValue) GetCellValue(int rowIndex, int cellIndex, object value, MiniExcelColumnMapping? columnInfo, bool valueIsNull) { if (valueIsNull) - return ("2", "str", string.Empty); + return (RegularCellStyleIndex, "str", string.Empty); if (value is string str) - return ("2", "str", XmlHelper.EncodeXml(str)); + return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(str)); var type = GetValueType(value, columnInfo); if (columnInfo is { ExcelFormat: not null, ExcelFormatId: -1 } && value is IFormattable formattableValue) { var formattedStr = formattableValue.ToString(columnInfo.ExcelFormat, _configuration.Culture); - return ("2", "str", XmlHelper.EncodeXml(formattedStr)); + return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(formattedStr)); } if (type == typeof(DateTime)) return GetDateTimeValue((DateTime)value, columnInfo); -#if NET6_0_OR_GREATER + if (type == typeof(DateTimeOffset)) + return GetDateTimeValue(((DateTimeOffset)value).DateTime, columnInfo); + + if (type == typeof(TimeSpan)) + return GetTimeSpanValue((TimeSpan)value, columnInfo); + +#if NET8_0_OR_GREATER if (type == typeof(DateOnly)) - return GetDateTimeValue(((DateOnly)value).ToDateTime(new TimeOnly()), columnInfo); + return GetDateTimeValue(((DateOnly)value).ToDateTime(default), columnInfo); + + if (type == typeof(TimeOnly)) + return GetTimeSpanValue(((TimeOnly)value).ToTimeSpan(), columnInfo); #endif + if (type.IsEnum) { string? description = null; @@ -188,24 +202,23 @@ private string GetPanes() } description ??= value.ToString(); - return ("2", "str", description); + return (RegularCellStyleIndex, "str", description); } if (TypeHelper.IsNumericType(type)) { var cellValue = GetNumericValue(value, type); - if (columnInfo?.ExcelFormat is null) { var dataType = ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture) ? "n" : "str"; - return ("2", dataType, cellValue); + return (RegularCellStyleIndex, dataType, cellValue); } return (columnInfo.ExcelFormatId.ToString(), null, cellValue); } if (type == typeof(bool)) - return ("2", "b", (bool)value ? "1" : "0"); + return (RegularCellStyleIndex, "b", (bool)value ? "1" : "0"); if (type == typeof(byte[]) && _configuration.EnableConvertByteArray) { @@ -216,25 +229,18 @@ private string GetPanes() return ("4", "str", XmlHelper.EncodeXml(base64)); } - return ("2", "str", XmlHelper.EncodeXml(value.ToString())); + return (RegularCellStyleIndex, "str", XmlHelper.EncodeXml(value.ToString())); } - private static Type? GetValueType(object value, MiniExcelColumnMapping? columnInfo) + private static Type GetValueType(object value, MiniExcelColumnMapping? columnInfo) { - Type type; - if (columnInfo is not { 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) @@ -303,30 +309,31 @@ private string GetFileValue(int rowIndex, int cellIndex, object value) return base64; } - private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping columnInfo) + //todo:reconsider cultureinfo + private (string, string?, string) GetDateTimeValue(DateTime value, MiniExcelColumnMapping? columnMapping) { string? cellValue; if (!ReferenceEquals(_configuration.Culture, CultureInfo.InvariantCulture)) { cellValue = value.ToString(_configuration.Culture); - return ("2", (string?)"str", cellValue); + return (RegularCellStyleIndex, (string?)"str", cellValue); } var oaDate = CorrectDateTimeValue(value); cellValue = oaDate.ToString(CultureInfo.InvariantCulture); - var format = columnInfo?.ExcelFormat is not null ? columnInfo.ExcelFormatId.ToString() : "3"; + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "3"; return (format, 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; @@ -335,6 +342,17 @@ private static double CorrectDateTimeValue(DateTime value) return oaDate; } + private (string, string?, string) GetTimeSpanValue(TimeSpan value, MiniExcelColumnMapping? columnMapping) + { + if (value.TotalDays >= 1) + return GetDateTimeValue(ExcelZeroDate + value, columnMapping); + + var cellValue = (value.TotalSeconds / 86400).ToString(CultureInfo.InvariantCulture); + var format = columnMapping?.ExcelFormatId is { } fmt and not -1 ? fmt.ToString() : "5"; + + return (format, null, cellValue); + } + private static string GetDimensionRef(int maxRowIndex, int maxColumnIndex) { return (maxRowIndex, maxColumnIndex) switch @@ -406,9 +424,4 @@ private string GetContentTypesXml() sb.Append(ExcelXml.EndTypes); return sb.ToString(); } - - private string GetCellXfId(string styleIndex) - { - return _cellXfIdMap.GetValueOrDefault(styleIndex, styleIndex); - } -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/OpenXmlWriter.cs b/src/MiniExcel.OpenXml/OpenXmlWriter.cs index 19ff839f..02219182 100644 --- a/src/MiniExcel.OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.OpenXml/OpenXmlWriter.cs @@ -16,7 +16,8 @@ internal partial class OpenXmlWriter : IMiniExcelWriter private readonly OpenXmlConfiguration _configuration; private readonly List _sheets = []; private readonly List _files = []; - + private readonly SheetStyleBuildContext _sheetStyleBuildContext; + private readonly string? _defaultSheetName; private readonly bool _printHeader; private readonly object? _value; @@ -33,6 +34,8 @@ private OpenXmlWriter(Stream stream, ZipArchive archive, object? value, string? _value = value; _printHeader = printHeader; _defaultSheetName = sheetName; + + _sheetStyleBuildContext = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom); } [CreateSyncVersion] @@ -55,70 +58,73 @@ internal static async ValueTask CreateAsync(Stream stream, object [CreateSyncVersion] public async Task SaveAsAsync(IProgress? progress = null, CancellationToken cancellationToken = default) { - try - { - await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); - await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); - await GenerateStylesXmlAsync(cancellationToken).ConfigureAwait(false); - - var sheets = GetSheets(); - var rowsWritten = new List(); +#if NET10_0_OR_GREATER + await using var disposableArchive = _archive.ConfigureAwait(false); +#else + using var disposableArchive = _archive; +#endif + await CreateZipEntryAsync(ExcelFileNames.Rels, ExcelContentTypes.Relationships, ExcelXml.DefaultRels, cancellationToken).ConfigureAwait(false); + await CreateZipEntryAsync(ExcelFileNames.SharedStrings, ExcelContentTypes.SharedStrings, ExcelXml.DefaultSharedString, cancellationToken).ConfigureAwait(false); - foreach (var sheet in sheets) - { - cancellationToken.ThrowIfCancellationRequested(); + await using var sbc = _sheetStyleBuildContext.ConfigureAwait(false); + var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); - _sheets.Add(sheet.Item1); //TODO:remove - _currentSheetIndex = sheet.Item1.SheetIdx; - var rows = await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, progress, cancellationToken).ConfigureAwait(false); - rowsWritten.Add(rows); - } + var sheets = GetSheets(); + var rowsWritten = new List(); - await GenerateEndXmlAsync(cancellationToken).ConfigureAwait(false); - return rowsWritten.ToArray(); - } - finally + foreach (var sheet in sheets) { -#if NET10_0_OR_GREATER - await _archive.DisposeAsync().ConfigureAwait(false); -#else - _archive.Dispose(); -#endif + _sheets.Add(sheet.Sheet); //TODO:remove + _currentSheetIndex = sheet.Sheet.SheetIdx; + + var rows = await CreateSheetXmlAsync(sheet.Data, sheet.Sheet.Path, progress, cancellationToken).ConfigureAwait(false); + rowsWritten.Add(rows); } + + await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); + await GenerateEndXmlAsync(cancellationToken).ConfigureAwait(false); + + return rowsWritten.ToArray(); } [CreateSyncVersion] public async Task InsertAsync(bool overwriteSheet = false, IProgress? progress = null, CancellationToken cancellationToken = default) { + if (!_configuration.FastMode) + throw new InvalidOperationException("Insert requires fast mode to be enabled"); + try { - if (!_configuration.FastMode) - throw new InvalidOperationException("Insert requires fast mode to be enabled"); +#if NET10_0_OR_GREATER + await using var disposableArchive = _archive.ConfigureAwait(false); +#else + using var disposableArchive = _archive; +#endif using var reader = await OpenXmlReader.CreateAsync(_stream, _configuration, cancellationToken: cancellationToken).ConfigureAwait(false); - var sheetRecords = (await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false))?.ToArray() ?? []; - foreach (var sheetRecord in sheetRecords.OrderBy(o => o.Id)) - { - cancellationToken.ThrowIfCancellationRequested(); - _sheets.Add(new SheetDto + var rels = await reader.GetWorkbookRelsAsync(_archive.Entries, cancellationToken).ConfigureAwait(false) ?? []; + + _sheets.AddRange(rels + .OrderBy(sheet => sheet.Id) + .Select(sheet => new SheetDto { - Name = sheetRecord.Name, - SheetIdx = (int)sheetRecord.Id, - State = sheetRecord.State - }); - } + Name = sheet.Name, + SheetIdx = (int)sheet.Id, + State = sheet.State + }) + ); var existSheetDto = _sheets.SingleOrDefault(s => s.Name == _defaultSheetName); if (existSheetDto is not null && !overwriteSheet) - throw new Exception($"Sheet “{_defaultSheetName}” already exist"); + throw new Exception($"Sheet \"{_defaultSheetName}\" already exist"); // GenerateStylesXml must be invoked after validating the overwritesheet parameter to avoid unnecessary style changes. - await GenerateStylesXmlAsync(cancellationToken).ConfigureAwait(false); + var styleBuilder = await GetSheetStyleBuilderAsync(_sheetStyleBuildContext, cancellationToken).ConfigureAwait(false); int rowsWritten; if (existSheetDto is null) { - _currentSheetIndex = (int)sheetRecords.Max(m => m.Id) + 1; + _currentSheetIndex = (int)rels.Max(m => m.Id) + 1; var insertSheetInfo = GetSheetInfos(_defaultSheetName); var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); _sheets.Add(insertSheetDto); @@ -131,6 +137,7 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); } + await styleBuilder.BuildAsync(cancellationToken).ConfigureAwait(false); await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.DrawingRels(_currentSheetIndex - 1))?.Delete(); @@ -139,12 +146,12 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Drawing(_currentSheetIndex - 1))?.Delete(); await GenerateDrawingXmlAsync(_currentSheetIndex - 1, cancellationToken).ConfigureAwait(false); - GenerateWorkBookXmls(out StringBuilder workbookXml, out StringBuilder workbookRelsXml, out Dictionary sheetsRelsXml); - foreach (var sheetRelsXml in sheetsRelsXml) + GenerateWorkBookXmls(out var workbookXml, out var workbookRelsXml, out var sheetsRelsXml); + foreach (var (key, value) in sheetsRelsXml) { - var sheetRelsXmlPath = ExcelFileNames.SheetRels(sheetRelsXml.Key); + var sheetRelsXmlPath = ExcelFileNames.SheetRels(key); _archive.Entries.SingleOrDefault(s => s.FullName == sheetRelsXmlPath)?.Delete(); - await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), cancellationToken).ConfigureAwait(false); + await CreateZipEntryAsync(sheetRelsXmlPath, null, ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); } _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Workbook)?.Delete(); @@ -159,11 +166,7 @@ public async Task InsertAsync(bool overwriteSheet = false, IProgress? } finally { -#if NET10_0_OR_GREATER - await _archive.DisposeAsync().ConfigureAwait(false); -#else - _archive.Dispose(); -#endif + await _sheetStyleBuildContext.DisposeAsync().ConfigureAwait(false); } } @@ -226,8 +229,6 @@ private static async Task WriteDimensionAsync(MiniExcelStreamWriter writer, int [CreateSyncVersion] private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object values, CancellationToken cancellationToken, IProgress? progress = null) { - cancellationToken.ThrowIfCancellationRequested(); - IMiniExcelWriteAdapter? writeAdapter = null; if (!MiniExcelWriteAdapterFactory.TryGetAsyncWriteAdapter(values, _configuration, out var asyncWriteAdapter)) { @@ -237,22 +238,24 @@ private async Task WriteValuesAsync(MiniExcelStreamWriter writer, object va try { var count = 0; - var isKnownCount = writeAdapter is not null && writeAdapter.TryGetKnownCount(out count); + var isKnownCount = writeAdapter?.TryGetKnownCount(out count) is true; #if SYNC_ONLY var mappings = writeAdapter?.GetColumns(); #else - var mappings = writeAdapter is not null - ? writeAdapter.GetColumns() - : await (asyncWriteAdapter?.GetColumnsAsync() ?? Task.FromResult?>(null)).ConfigureAwait(false); + var mappings = asyncWriteAdapter is not null + ? await asyncWriteAdapter.GetColumnsAsync().ConfigureAwait(false) + : writeAdapter?.GetColumns() ?? []; #endif - if (mappings is null) + if (mappings is null or []) { await WriteEmptySheetAsync(writer).ConfigureAwait(false); return 0; } + _sheetStyleBuildContext.UpdateFormatIds(mappings); + int maxRowIndex; var maxColumnIndex = mappings.Count(x => x is { ExcelIgnoreColumn: false }); long dimensionPlaceholderPostition = 0; @@ -416,7 +419,7 @@ private static async Task WriteColumnsWidthsAsync(MiniExcelStreamWriter writer, } [CreateSyncVersion] - private async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, CancellationToken cancellationToken = default) + private static async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List mappings, CancellationToken cancellationToken = default) { const int yIndex = 1; await writer.WriteAsync(WorksheetXml.StartRow(yIndex), cancellationToken).ConfigureAwait(false); @@ -431,7 +434,7 @@ private async Task PrintHeaderAsync(MiniExcelStreamWriter writer, List GetSheetStyleBuilderAsync(SheetStyleBuildContext context, CancellationToken cancellationToken = default) { -#if NET8_0_OR_GREATER - var context = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom, _configuration.DynamicColumns ?? []); - await using var disposableContext = context.ConfigureAwait(false); -#else - using var context = new SheetStyleBuildContext(_zipDictionary, _archive, Utf8WithBom, _configuration.DynamicColumns ?? []); -#endif - ISheetStyleBuilder builder = _configuration.TableStyles switch + SheetStyleBuilderBase builder = _configuration.TableStyles switch { TableStyles.None => new MinimalSheetStyleBuilder(context), TableStyles.Default => new DefaultSheetStyleBuilder(context, _configuration.StyleOptions), _ => throw new InvalidEnumArgumentException(nameof(_configuration.TableStyles), (int)_configuration.TableStyles, typeof(TableStyles)) }; - var result = await builder.BuildAsync(cancellationToken).ConfigureAwait(false); - _cellXfIdMap = result.CellXfIdMap; + var newInfos = builder.GetGeneratedElementInfos(); + await context.CreateAsync(newInfos, cancellationToken).ConfigureAwait(false); + + return builder; } [CreateSyncVersion] @@ -571,12 +564,12 @@ private async Task GenerateWorkbookXmlAsync(CancellationToken cancellationToken) out StringBuilder workbookRelsXml, out Dictionary sheetsRelsXml); - foreach (var sheetRelsXml in sheetsRelsXml) + foreach (var (key, value) in sheetsRelsXml) { await CreateZipEntryAsync( - ExcelFileNames.SheetRels(sheetRelsXml.Key), + ExcelFileNames.SheetRels(key), null, - ExcelXml.DefaultSheetRelXml.Replace("{{format}}", sheetRelsXml.Value), + ExcelXml.DefaultSheetRelXml.Replace("{{format}}", value), cancellationToken).ConfigureAwait(false); } @@ -619,7 +612,7 @@ private async Task InsertContentTypesXmlAsync(CancellationToken cancellationToke var doc = XDocument.Load(stream); #endif - var ns = doc.Root?.GetDefaultNamespace(); + var ns = doc.Root!.GetDefaultNamespace(); var typesElement = doc.Descendants(ns + "Types").Single(); var partNames = new HashSet(StringComparer.InvariantCultureIgnoreCase); diff --git a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs index beefee8c..45fbeab5 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/DefaultSheetStyleBuilder.cs @@ -3,32 +3,31 @@ namespace MiniExcelLib.OpenXml.Styles.Builder; -internal partial class DefaultSheetStyleBuilder(SheetStyleBuildContext context, OpenXmlStyleOptions styleOptions) - : SheetStyleBuilderBase(context) +internal partial class DefaultSheetStyleBuilder(SheetStyleBuildContext context, OpenXmlStyleOptions styleOptions) : SheetStyleBuilderBase(context) { - private static readonly SheetStyleElementInfos GenerateElementInfos = new() + private const HorizontalCellAlignment DefaultHorizontalAlignment = HorizontalCellAlignment.Left; + private const VerticalCellAlignment DefaultVerticalAlignment = VerticalCellAlignment.Bottom; + private static readonly Color DefaultBackgroundColor = Color.FromArgb(0x284472C4); + + private static readonly SheetStyleElementInfos GeneratedElementInfos = 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 others will be 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 = context; private readonly OpenXmlStyleOptions _styleOptions = styleOptions; private XmlReader OldReader => _context.OldXmlReader!; private XmlWriter NewWriter => _context.NewXmlWriter!; - protected override SheetStyleElementInfos GetGenerateElementInfos() + protected internal override SheetStyleElementInfos GetGeneratedElementInfos() { - return GenerateElementInfos; + return GeneratedElementInfos; } [CreateSyncVersion] @@ -36,7 +35,7 @@ protected override async Task GenerateNumFmtAsync() { const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var map in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -45,8 +44,8 @@ protected override async Task GenerateNumFmtAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmt", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Format).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, map.Format).ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } } @@ -336,9 +335,37 @@ protected override async Task GenerateCellStyleXfAsync() [CreateSyncVersion] 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" + }; + + /* Empty style is required because Excel always considers the first one to be the default and ignores all its properties + * + * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteEndElementAsync().ConfigureAwait(false); @@ -360,18 +387,11 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); - - var headerHorizontalAlignment = _styleOptions.HeaderStyle?.HorizontalAlignment ?? DefaultHorizontalAlignment; - var headerHorizontalAlignmentStr = headerHorizontalAlignment.ToString().ToLowerInvariant(); - await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, headerHorizontalAlignmentStr).ConfigureAwait(false); - - var headerVerticalAlignment = _styleOptions.HeaderStyle?.VerticalAlignment ?? DefaultVerticalAlignment; - var headerVerticalAlignmentStr = headerVerticalAlignment.ToString().ToLowerInvariant(); - await NewWriter.WriteAttributeStringAsync(null, "vertical", null, headerVerticalAlignmentStr).ConfigureAwait(false); - + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, headerHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, headerVerticalAlignment).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); - var wrapHeader = (_styleOptions.HeaderStyle?.WrapText ?? false) ? "1" : "0"; + var wrapHeader = _styleOptions.HeaderStyle?.WrapText is true ? "1" : "0"; await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, wrapHeader).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); @@ -404,23 +424,8 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); - - var style1HorizontalAlignment = _styleOptions.HorizontalAlignment switch - { - HorizontalCellAlignment.Center => "center", - HorizontalCellAlignment.Right => "right", - _ => "general" - }; - - var style1VerticalAlignment = _styleOptions.VerticalAlignment switch - { - VerticalCellAlignment.Top => "top", - VerticalCellAlignment.Center => "center", - _ => "bottom" - }; - - await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, style1HorizontalAlignment).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "vertical", null, style1VerticalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); var wrapContent = _styleOptions.WrapCellContents ? "1" : "0"; @@ -456,23 +461,8 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); - - var style2HorizontalAlignment = _styleOptions.HorizontalAlignment switch - { - HorizontalCellAlignment.Center => "center", - HorizontalCellAlignment.Right => "right", - _ => "general" - }; - - var style2VerticalAlignment = _styleOptions.VerticalAlignment switch - { - VerticalCellAlignment.Top => "top", - VerticalCellAlignment.Center => "center", - _ => "bottom" - }; - - await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, style2HorizontalAlignment).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "vertical", null, style2VerticalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); @@ -505,12 +495,43 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + /* + * + * + * + */ + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "xfId", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyFill", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyBorder", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "relativeIndent", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "justifyLastLine", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "shrinkToFit", null, "0").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "readingOrder", null, "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "protection", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "locked", null, "1").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "hidden", null, "0").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + const int numFmtIndex = 166; - var index = 0; - foreach (var _ in _context.ColumnsToApply) + for (var i = 1; i <= _context.CustomFormatCount; i++) { - index++; - /* * * @@ -518,7 +539,7 @@ protected override async Task GenerateCellXfAsync() * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + i + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "fontId", null, $"{_context.OldElementInfos.FontCount + 0}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "fillId", null, $"{_context.OldElementInfos.FillCount + 0}").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "borderId", null, $"{_context.OldElementInfos.BorderCount + 1}").ConfigureAwait(false); @@ -529,23 +550,8 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteAttributeStringAsync(null, "applyAlignment", null, "1").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyProtection", null, "1").ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "alignment", OldReader.NamespaceURI).ConfigureAwait(false); - - var style3HorizontalAlignment = _styleOptions.HorizontalAlignment switch - { - HorizontalCellAlignment.Center => "center", - HorizontalCellAlignment.Right => "right", - _ => "general" - }; - - var style3VerticalAlignment = _styleOptions.VerticalAlignment switch - { - VerticalCellAlignment.Top => "top", - VerticalCellAlignment.Center => "center", - _ => "bottom" - }; - - await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, style3HorizontalAlignment).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "vertical", null, style3VerticalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "horizontal", null, cellHorizontalAlignment).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "vertical", null, cellVerticalAlignment).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "textRotation", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "wrapText", null, "0").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "indent", null, "0").ConfigureAwait(false); diff --git a/src/MiniExcel.OpenXml/Styles/Builder/ISheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/ISheetStyleBuilder.cs index b7423b25..8ec9a429 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/ISheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/ISheetStyleBuilder.cs @@ -3,5 +3,5 @@ internal partial interface ISheetStyleBuilder { [CreateSyncVersion] - Task BuildAsync(CancellationToken cancellationToken = default); -} \ No newline at end of file + Task BuildAsync(CancellationToken cancellationToken = default); +} diff --git a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs index c4811cb5..7228be1d 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/MinimalSheetStyleBuilder.cs @@ -2,22 +2,22 @@ internal partial class MinimalSheetStyleBuilder(SheetStyleBuildContext context) : SheetStyleBuilderBase(context) { - internal static SheetStyleElementInfos GenerateElementInfos = new() + 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 others will be dynamically generated based on format mappings FontCount = 1, FillCount = 1, BorderCount = 1, CellStyleXfCount = 1, - CellXfCount = 5 + CellXfCount = 6 }; private readonly SheetStyleBuildContext _context = context; + private XmlReader OldReader => _context.OldXmlReader!; private XmlWriter NewWriter => _context.NewXmlWriter!; - - protected override SheetStyleElementInfos GetGenerateElementInfos() + protected internal override SheetStyleElementInfos GetGeneratedElementInfos() { return GenerateElementInfos; } @@ -27,7 +27,7 @@ protected override async Task GenerateNumFmtAsync() { const int numFmtIndex = 166; var index = 0; - foreach (var item in _context.ColumnsToApply) + foreach (var map in _context.SheetStyleFormatsCache.FormatMappings) { index++; @@ -36,8 +36,8 @@ protected override async Task GenerateNumFmtAsync() */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmt", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index + _context.OldElementInfos.NumFmtCount).ToString()).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, item.Format).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "formatCode", null, map.Format).ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } } @@ -48,7 +48,7 @@ protected override async Task GenerateFontAsync() * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "font", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -58,7 +58,7 @@ protected override async Task GenerateFillAsync() * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "fill", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -68,7 +68,7 @@ protected override async Task GenerateBorderAsync() * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "border", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -78,7 +78,7 @@ protected override async Task GenerateCellStyleXfAsync() * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] @@ -90,23 +90,28 @@ protected override async Task GenerateCellXfAsync() * * * + * */ await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "14").ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, "21").ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); const int numFmtIndex = 166; var index = 0; - foreach (var _ in _context.ColumnsToApply) + for (var i = 0; i < _context.CustomFormatCount; i++) { index++; @@ -116,7 +121,7 @@ protected override async Task GenerateCellXfAsync() await NewWriter.WriteStartElementAsync(OldReader.Prefix, "xf", OldReader.NamespaceURI).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "numFmtId", null, (numFmtIndex + index).ToString()).ConfigureAwait(false); await NewWriter.WriteAttributeStringAsync(null, "applyNumberFormat", null, "1").ConfigureAwait(false); - await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); + await NewWriter.WriteEndElementAsync().ConfigureAwait(false); } } -} \ No newline at end of file +} diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs index 7e7d5d12..4f0b5e7a 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildContext.cs @@ -1,24 +1,18 @@ -using MiniExcelLib.Core.Attributes; -using MiniExcelLib.OpenXml.Constants; +using MiniExcelLib.OpenXml.Constants; namespace MiniExcelLib.OpenXml.Styles.Builder; -internal sealed partial class SheetStyleBuildContext : IDisposable -#if NET8_0_OR_GREATER - , IAsyncDisposable -#endif +internal sealed partial class SheetStyleBuildContext(Dictionary zipDictionary, ZipArchive archive, Encoding encoding) : IDisposable, IAsyncDisposable { - private static readonly string EmptyStylesXml = XmlHelper.MinifyXml( + private const string EmptyStylesXml = """ - - - """); + + """; - private readonly Dictionary _zipDictionary; - private readonly ZipArchive _archive; - private readonly Encoding _encoding; - private readonly ICollection _columns; + private readonly Dictionary _zipDictionary = zipDictionary; + private readonly ZipArchive _archive = archive; + private readonly Encoding _encoding = encoding; private StringReader? _emptyStylesXmlStringReader; private ZipArchiveEntry? _oldStyleXmlZipEntry; @@ -30,33 +24,68 @@ internal sealed partial class SheetStyleBuildContext : IDisposable private bool _finalized; private bool _disposed; - public SheetStyleBuildContext(Dictionary zipDictionary, ZipArchive 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; } = null!; - public SheetStyleElementInfos GenerateElementInfos { get; private set; } = null!; - public MiniExcelColumnAttribute[] ColumnsToApply { get; private set; } = []; - public int CustomFormatCount { get; private set; } + public SheetStyleElementInfos GeneratedElementInfos { get; private set; } = null!; + public int CustomFormatCount => SheetStyleFormatsCache.FormatMappingsCount; + + [CreateSyncVersion] + public async Task CreateAsync(SheetStyleElementInfos generatedElementInfos, CancellationToken cancellationToken = default) + { + const bool isAsync = +#if SYNC_ONLY + false; +#else + true; +#endif + + SheetStyleElementInfos infos; + var styleEntry = _archive.Mode == ZipArchiveMode.Update + ? _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) + : null; + + if (styleEntry is not null) + { +#if NET8_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, XmlReaderHelper.GetXmlReaderSettings(isAsync)); + infos = await ReadSheetStyleElementInfosAsync(reader, cancellationToken).ConfigureAwait(false); + } + else + { + infos = new SheetStyleElementInfos(); + } + + SheetStyleFormatsCache.SetCurrentIndex(infos.CellXfCount + generatedElementInfos.CellXfCount); + } [CreateSyncVersion] - public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, CancellationToken cancellationToken = default) + public async Task InitializeAsync(SheetStyleElementInfos generatedElementInfos, CancellationToken cancellationToken = default) { if (_initialized) throw new InvalidOperationException("The context has already been initialized."); - cancellationToken.ThrowIfCancellationRequested(); + const bool isAsync = +#if SYNC_ONLY + false; +#else + true; +#endif + + GeneratedElementInfos = generatedElementInfos; _oldStyleXmlZipEntry = _archive.Mode == ZipArchiveMode.Update ? _archive.Entries.SingleOrDefault(s => s.FullName == ExcelFileNames.Styles) : null; + var xmlReaderSettings = XmlReaderHelper.GetXmlReaderSettings(isAsync); if (_oldStyleXmlZipEntry is not null) { #if NET8_0_OR_GREATER @@ -66,7 +95,7 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C using (var oldStyleXmlStream = _oldStyleXmlZipEntry.Open()) #endif { - using var reader = XmlReader.Create(oldStyleXmlStream, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); + using var reader = XmlReader.Create(oldStyleXmlStream, xmlReaderSettings); OldElementInfos = await ReadSheetStyleElementInfosAsync(reader, cancellationToken).ConfigureAwait(false); } @@ -75,14 +104,14 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C #else _oldXmlReaderStream = _oldStyleXmlZipEntry.Open(); #endif - OldXmlReader = XmlReader.Create(_oldXmlReaderStream, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); + OldXmlReader = XmlReader.Create(_oldXmlReaderStream, xmlReaderSettings); _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles + ".temp", CompressionLevel.Fastest); } else { OldElementInfos = new SheetStyleElementInfos(); _emptyStylesXmlStringReader = new StringReader(EmptyStylesXml); - OldXmlReader = XmlReader.Create(_emptyStylesXmlStringReader, new XmlReaderSettings { IgnoreWhitespace = true, Async = true }); + OldXmlReader = XmlReader.Create(_emptyStylesXmlStringReader, xmlReaderSettings); _newStyleXmlZipEntry = _archive.CreateEntry(ExcelFileNames.Styles, CompressionLevel.Fastest); } @@ -92,15 +121,16 @@ public async Task InitializeAsync(SheetStyleElementInfos generateElementInfos, C #else _newXmlWriterStream = _newStyleXmlZipEntry.Open(); #endif - NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = _encoding, Async = true }); - - GenerateElementInfos = generateElementInfos; - ColumnsToApply = SheetStyleBuilderHelper.GenerateStyleIds(OldElementInfos.CellXfCount + generateElementInfos.CellXfCount, _columns).ToArray(); - CustomFormatCount = ColumnsToApply.Length; + NewXmlWriter = XmlWriter.Create(_newXmlWriterStream, new XmlWriterSettings { Indent = true, Encoding = _encoding, Async = isAsync }); _initialized = true; } - + + public void UpdateFormatIds(ICollection mappings) + { + SheetStyleFormatsCache.AddMappings(mappings); + } + [CreateSyncVersion] public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancellationToken = default) { @@ -113,8 +143,6 @@ public async Task FinalizeAndUpdateZipDictionaryAsync(CancellationToken cancella try { - cancellationToken.ThrowIfCancellationRequested(); - OldXmlReader?.Dispose(); OldXmlReader = null; #if NET8_0_OR_GREATER @@ -261,7 +289,6 @@ public void Dispose() _disposed = true; } -#if NET8_0_OR_GREATER public async ValueTask DisposeAsync() { if (_disposed) @@ -284,5 +311,4 @@ static async ValueTask CastAndDispose(IDisposable? resource) resource?.Dispose(); } } -#endif } diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildResult.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildResult.cs deleted file mode 100644 index 53a504d4..00000000 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuildResult.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace MiniExcelLib.OpenXml.Styles.Builder; - -internal class SheetStyleBuildResult(Dictionary cellXfIdMap) -{ - public Dictionary CellXfIdMap { get; set; } = cellXfIdMap; -} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs index 5440893c..534f9737 100644 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderBase.cs @@ -7,8 +7,8 @@ internal abstract partial class SheetStyleBuilderBase(SheetStyleBuildContext con //todo: these may actually be null if used when the context is not initialized private XmlReader OldReader => _context.OldXmlReader!; private XmlWriter NewWriter => _context.NewXmlWriter!; - - internal static readonly Dictionary AllElements = new() + + private static readonly Dictionary AllElements = new() { ["numFmts"] = 0, ["fonts"] = 1, @@ -22,13 +22,11 @@ internal abstract partial class SheetStyleBuilderBase(SheetStyleBuildContext con ["extLst"] = 9 }; - // Todo: add CancellationToken to all methods called inside of BuildAsync [CreateSyncVersion] - public virtual async Task BuildAsync(CancellationToken cancellationToken = default) + public virtual async Task BuildAsync(CancellationToken cancellationToken = default) { - await _context.InitializeAsync(GetGenerateElementInfos(), cancellationToken).ConfigureAwait(false); - + await _context.InitializeAsync(GetGeneratedElementInfos(), cancellationToken).ConfigureAwait(false); while (await OldReader.ReadAsync().ConfigureAwait(false)) { cancellationToken.ThrowIfCancellationRequested(); @@ -75,11 +73,9 @@ public virtual async Task BuildAsync(CancellationToken ca } await _context.FinalizeAndUpdateZipDictionaryAsync(cancellationToken).ConfigureAwait(false); - - return new SheetStyleBuildResult(GetCellXfIdMap()); } - protected abstract SheetStyleElementInfos GetGenerateElementInfos(); + protected internal abstract SheetStyleElementInfos GetGeneratedElementInfos(); [CreateSyncVersion] protected virtual async Task WriteAttributesAsync(string element, CancellationToken cancellationToken = default) @@ -110,12 +106,12 @@ protected virtual async Task WriteAttributesAsync(string element, CancellationTo { var value = element switch { - "numFmts" => (_context.OldElementInfos.NumFmtCount + _context.GenerateElementInfos.NumFmtCount + _context.CustomFormatCount).ToString(), - "fonts" => (_context.OldElementInfos.FontCount + _context.GenerateElementInfos.FontCount).ToString(), - "fills" => (_context.OldElementInfos.FillCount + _context.GenerateElementInfos.FillCount).ToString(), - "borders" => (_context.OldElementInfos.BorderCount + _context.GenerateElementInfos.BorderCount).ToString(), - "cellStyleXfs" => (_context.OldElementInfos.CellStyleXfCount + _context.GenerateElementInfos.CellStyleXfCount).ToString(), - "cellXfs" => (_context.OldElementInfos.CellXfCount + _context.GenerateElementInfos.CellXfCount + _context.CustomFormatCount).ToString(), + "numFmts" => (_context.OldElementInfos.NumFmtCount + _context.GeneratedElementInfos.NumFmtCount + _context.CustomFormatCount).ToString(), + "fonts" => (_context.OldElementInfos.FontCount + _context.GeneratedElementInfos.FontCount).ToString(), + "fills" => (_context.OldElementInfos.FillCount + _context.GeneratedElementInfos.FillCount).ToString(), + "borders" => (_context.OldElementInfos.BorderCount + _context.GeneratedElementInfos.BorderCount).ToString(), + "cellStyleXfs" => (_context.OldElementInfos.CellStyleXfCount + _context.GeneratedElementInfos.CellStyleXfCount).ToString(), + "cellXfs" => (_context.OldElementInfos.CellXfCount + _context.GeneratedElementInfos.CellXfCount + _context.CustomFormatCount).ToString(), _ => OldReader.Value }; await NewWriter.WriteStringAsync(value).ConfigureAwait(false); @@ -137,35 +133,35 @@ protected virtual async Task GenerateElementBeforStartElementAsync() if (!AllElements.TryGetValue(OldReader.LocalName, out var elementIndex)) return; - if (!_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts && AllElements["numFmts"] < elementIndex) + if (!_context.OldElementInfos.ExistsNumFmts && !_context.GeneratedElementInfos.ExistsNumFmts && AllElements["numFmts"] < elementIndex) { await GenerateNumFmtsAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsNumFmts = true; + _context.GeneratedElementInfos.ExistsNumFmts = true; } - else if (!_context.OldElementInfos.ExistsFonts && !_context.GenerateElementInfos.ExistsFonts && AllElements["fonts"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFonts && !_context.GeneratedElementInfos.ExistsFonts && AllElements["fonts"] < elementIndex) { await GenerateFontsAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsFonts = true; + _context.GeneratedElementInfos.ExistsFonts = true; } - else if (!_context.OldElementInfos.ExistsFills && !_context.GenerateElementInfos.ExistsFills && AllElements["fills"] < elementIndex) + else if (!_context.OldElementInfos.ExistsFills && !_context.GeneratedElementInfos.ExistsFills && AllElements["fills"] < elementIndex) { await GenerateFillsAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsFills = true; + _context.GeneratedElementInfos.ExistsFills = true; } - else if (!_context.OldElementInfos.ExistsBorders && !_context.GenerateElementInfos.ExistsBorders && AllElements["borders"] < elementIndex) + else if (!_context.OldElementInfos.ExistsBorders && !_context.GeneratedElementInfos.ExistsBorders && AllElements["borders"] < elementIndex) { await GenerateBordersAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsBorders = true; + _context.GeneratedElementInfos.ExistsBorders = true; } - else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GenerateElementInfos.ExistsCellStyleXfs && AllElements["cellStyleXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellStyleXfs && !_context.GeneratedElementInfos.ExistsCellStyleXfs && AllElements["cellStyleXfs"] < elementIndex) { await GenerateCellStyleXfsAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsCellStyleXfs = true; + _context.GeneratedElementInfos.ExistsCellStyleXfs = true; } - else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GenerateElementInfos.ExistsCellXfs && AllElements["cellXfs"] < elementIndex) + else if (!_context.OldElementInfos.ExistsCellXfs && !_context.GeneratedElementInfos.ExistsCellXfs && AllElements["cellXfs"] < elementIndex) { await GenerateCellXfsAsync().ConfigureAwait(false); - _context.GenerateElementInfos.ExistsCellXfs = true; + _context.GeneratedElementInfos.ExistsCellXfs = true; } } @@ -174,7 +170,7 @@ protected virtual async Task GenerateElementBeforEndElementAsync() { switch (OldReader.LocalName) { - case "styleSheet" when !_context.OldElementInfos.ExistsNumFmts && !_context.GenerateElementInfos.ExistsNumFmts: + case "styleSheet" when !_context.OldElementInfos.ExistsNumFmts && !_context.GeneratedElementInfos.ExistsNumFmts: await GenerateNumFmtsAsync().ConfigureAwait(false); break; case "numFmts": @@ -202,7 +198,7 @@ protected virtual async Task GenerateElementBeforEndElementAsync() protected virtual async Task GenerateNumFmtsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "numFmts", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.NumFmtCount + _context.GenerateElementInfos.NumFmtCount + _context.CustomFormatCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.NumFmtCount + _context.GeneratedElementInfos.NumFmtCount + _context.CustomFormatCount).ToString()).ConfigureAwait(false); await GenerateNumFmtAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -219,7 +215,7 @@ protected virtual async Task GenerateNumFmtsAsync() protected virtual async Task GenerateFontsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "fonts", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.FontCount + _context.GenerateElementInfos.FontCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.FontCount + _context.GeneratedElementInfos.FontCount).ToString()).ConfigureAwait(false); await GenerateFontAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -236,7 +232,7 @@ protected virtual async Task GenerateFontsAsync() protected virtual async Task GenerateFillsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "fills", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.FillCount + _context.GenerateElementInfos.FillCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.FillCount + _context.GeneratedElementInfos.FillCount).ToString()).ConfigureAwait(false); await GenerateFillAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -253,7 +249,7 @@ protected virtual async Task GenerateFillsAsync() protected virtual async Task GenerateBordersAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "borders", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.BorderCount + _context.GenerateElementInfos.BorderCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.BorderCount + _context.GeneratedElementInfos.BorderCount).ToString()).ConfigureAwait(false); await GenerateBorderAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -270,7 +266,7 @@ protected virtual async Task GenerateBordersAsync() protected virtual async Task GenerateCellStyleXfsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "cellStyleXfs", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.CellStyleXfCount + _context.GenerateElementInfos.CellStyleXfCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.CellStyleXfCount + _context.GeneratedElementInfos.CellStyleXfCount).ToString()).ConfigureAwait(false); await GenerateCellStyleXfAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); @@ -287,21 +283,11 @@ protected virtual async Task GenerateCellStyleXfsAsync() protected virtual async Task GenerateCellXfsAsync() { await NewWriter.WriteStartElementAsync(OldReader.Prefix, "cellXfs", OldReader.NamespaceURI).ConfigureAwait(false); - await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.CellXfCount + _context.GenerateElementInfos.CellXfCount + _context.CustomFormatCount).ToString()).ConfigureAwait(false); + await NewWriter.WriteAttributeStringAsync(null, "count", null, (_context.OldElementInfos.CellXfCount + _context.GeneratedElementInfos.CellXfCount + _context.CustomFormatCount).ToString()).ConfigureAwait(false); await GenerateCellXfAsync().ConfigureAwait(false); await NewWriter.WriteFullEndElementAsync().ConfigureAwait(false); } [CreateSyncVersion] 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/Builder/SheetStyleBuilderHelper.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderHelper.cs deleted file mode 100644 index 7bacddd6..00000000 --- a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleBuilderHelper.cs +++ /dev/null @@ -1,28 +0,0 @@ -using MiniExcelLib.Core.Attributes; - -namespace MiniExcelLib.OpenXml.Styles.Builder; - -public static class SheetStyleBuilderHelper -{ - public static IEnumerable GenerateStyleIds(int startUpCellXfs, ICollection? dynamicColumns) - { - if (dynamicColumns is null) - yield break; - - int index = 0; - var cols = dynamicColumns - .Where(x => !string.IsNullOrWhiteSpace(x.Format) && new OpenXmlNumberFormatHelper(x.Format).IsValid) - .GroupBy(x => x.Format); - - foreach (var group in cols) - { - foreach (var col in group) - { - col.SetFormatId(startUpCellXfs + index); - } - - yield return group.First(); - index++; - } - } -} \ No newline at end of file diff --git a/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs new file mode 100644 index 00000000..e7ec1ca9 --- /dev/null +++ b/src/MiniExcel.OpenXml/Styles/Builder/SheetStyleFormatsCache.cs @@ -0,0 +1,29 @@ +namespace MiniExcelLib.OpenXml.Styles.Builder; + +internal class SheetStyleFormatsCache +{ + private readonly Dictionary _formatMappings = []; + private int _stylesCount; + + internal int FormatMappingsCount => _formatMappings.Count; + internal IEnumerable<(string Format, int FormatId)> FormatMappings => _formatMappings.Select(x => (x.Key, x.Value)); + + public void AddMappings(IEnumerable mappings) + { + foreach (var mapping in mappings.Where(map => map is { ExcelIgnoreColumn: false })) + { + if (!string.IsNullOrWhiteSpace(mapping!.ExcelFormat) && new OpenXmlNumberFormatHelper(mapping.ExcelFormat).IsValid) + { + if (!_formatMappings.TryGetValue(mapping.ExcelFormat, out var formatId)) + { + formatId = _stylesCount++; + _formatMappings.Add(mapping.ExcelFormat, formatId); + } + + mapping.SetFormatId(formatId); + } + } + } + + internal void SetCurrentIndex(int index) => _stylesCount = index; +} diff --git a/src/MiniExcel.OpenXml/Utils/OpenXmlNumberFormatHelper.cs b/src/MiniExcel.OpenXml/Utils/OpenXmlNumberFormatHelper.cs index aa65d8e0..800b6afd 100644 --- a/src/MiniExcel.OpenXml/Utils/OpenXmlNumberFormatHelper.cs +++ b/src/MiniExcel.OpenXml/Utils/OpenXmlNumberFormatHelper.cs @@ -59,12 +59,10 @@ internal static class Evaluator // Positive;Negative;Zero;Text return value switch { - string s => sections.Count >= 4 ? sections[3] : null, - DateTime dt => GetFirstSection(sections, SectionType.Date), // TODO: Check date conditions need date helpers and Date1904 knowledge - TimeSpan ts => GetNumericSection(sections), - double d => GetNumericSection(sections), - int i => GetNumericSection(sections), - short s => GetNumericSection(sections), + string => sections.Count >= 4 ? sections[3] : null, + DateTime => GetFirstSection(sections, SectionType.Date), // TODO: Check date conditions need date helpers and Date1904 knowledge + TimeSpan => GetNumericSection(sections), + double or int or short => GetNumericSection(sections), _ => null }; } @@ -365,7 +363,7 @@ public static bool TryParse(List tokens, out DecimalSection? format) private static double GetPercentMultiplier(List tokens) { // If there is a percentage literal in the part list, multiply the result by 100 - return tokens.Any(token => token == "%") ? 100 : 1; + return tokens.Exists(token => token == "%") ? 100 : 1; } private static double GetTrailingCommasDivisor(List tokens, out bool thousandSeparator) @@ -699,7 +697,7 @@ internal static int ParseNumberTokens(List tokens, int startPosition, ou decimalSeparator = false; List remainder = []; - var index = 0; + int index; for (index = 0; index < tokens.Count; ++index) { var token = tokens[index]; @@ -770,8 +768,7 @@ private static void ParseMilliseconds(List tokens, out List resu private static string? ReadToken(Tokenizer reader, out bool syntaxError) { var offset = reader.Position; - if ( - ReadLiteral(reader) || + if (ReadLiteral(reader) || reader.ReadEnclosed('[', ']') || // Symbols @@ -818,6 +815,14 @@ private static bool ReadLiteral(Tokenizer reader) return true; } + // Treat any unrecognized character as a literal + // This allows international currency symbols and other characters + if (reader.Peek() is var currentChar and not -1 && !Token.IsFormatSpecifier((char)currentChar)) + { + reader.Advance(); + return true; + } + return false; } } @@ -935,50 +940,45 @@ public bool ReadEnclosed(char open, char close) internal static class Token { + // Characters that Excel uses for formatting logic + private static readonly HashSet ReservedChars = + [ + '0', '#', '?', '.', ',', '%', ';', + + // Scientific notation + 'e', 'E', + + // Date and Time codes + 'm', 'M', 'd', 'D', 'y', 'Y', 'h', 'H', 's', 'S', + + // AM/PM indicators + 'a', 'A', 'p', 'P', + + // Text placeholder + '@', + + // Control & Structural characters + '[', ']', // Colors, conditions, or elapsed time + '"', // String wrapper + '\\', // Escape character + '*', // Fill space character + '_' + ]; + public static bool IsExponent(string token) => string.Compare(token, "e+", StringComparison.OrdinalIgnoreCase) == 0 || string.Compare(token, "e-", StringComparison.OrdinalIgnoreCase) == 0; - public static bool IsLiteral(string token) => - token.StartsWith("_", StringComparison.Ordinal) || - token.StartsWith("\\", StringComparison.Ordinal) || - token.StartsWith("\"", StringComparison.Ordinal) || - token.StartsWith("*", StringComparison.Ordinal) || - token == "," || - token == "!" || - token == "&" || - token == "%" || - token == "+" || - token == "-" || - token == "$" || - token == "€" || - token == "£" || - token == "1" || - token == "2" || - token == "3" || - token == "4" || - token == "5" || - token == "6" || - token == "7" || - token == "8" || - token == "9" || - token == "{" || - token == "}" || - token == "(" || - token == ")" || - token == " "; - public static bool IsNumberLiteral(string token) => IsPlaceholder(token) || - IsLiteral(token) || + (!IsDatePart(token) && !IsDurationPart(token)) || token == "."; - public static bool IsPlaceholder(string token) => token is "0" or "#" or "?"; + public static bool IsPlaceholder(string token) + => token is "0" or "#" or "?"; - public static bool IsGeneral(string token) - { - return string.Compare(token, "general", StringComparison.OrdinalIgnoreCase) == 0; - } + public static bool IsGeneral(string token) + => string.Compare(token, "general", StringComparison.OrdinalIgnoreCase) == 0; public static bool IsDatePart(string token) => token.StartsWith("y", StringComparison.OrdinalIgnoreCase) || @@ -996,14 +996,14 @@ public static bool IsDurationPart(string token) => token.StartsWith("[m", StringComparison.OrdinalIgnoreCase) || token.StartsWith("[s", StringComparison.OrdinalIgnoreCase); - public static bool IsDigit09(string token) - { - return token == "0" || IsDigit19(token); - } + public static bool IsDigit09(string token) => token == "0" || IsDigit19(token); public static bool IsDigit19(string token) => token switch { "1" or "2" or "3" or "4" or "5" or "6" or "7" or "8" or "9" => true, _ => false }; + + // Check if character is a known format specifier that should NOT be treated as literal + public static bool IsFormatSpecifier(char @char) => ReservedChars.Contains(@char); } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs index f1f2862f..e3aede7f 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueAsyncTests.cs @@ -11,7 +11,12 @@ public class MiniExcelIssueAsyncTests(ITestOutputHelper output) private readonly OpenXmlImporter _excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); private readonly OpenXmlTemplater _excelTemplater = MiniExcel.Templaters.GetOpenXmlTemplater(); - + + static MiniExcelIssueAsyncTests() + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + } + /// /// [SaveAsByTemplate support DateTime custom format · Issue #255 · mini-software/MiniExcel] /// (https://github.com/mini-software/MiniExcel/issues/255) @@ -19,44 +24,47 @@ public class MiniExcelIssueAsyncTests(ITestOutputHelper output) [Fact] public async Task Issue255() { - //tempalte + var dt1 = new DateTime(2021, 01, 01); + var dt2 = new DateTime(2022, 01, 01); + + //template { var templatePath = PathHelper.GetFile("xlsx/TestsIssue255_Template.xlsx"); - using var path = AutoDeletingPath.Create(); + await using var ms = new MemoryStream(); var value = new { - Issue255DTO = new[] - { - new Issue255DTO { Time = new DateTime(2021, 01, 01), Time2 = new DateTime(2021, 01, 01) } - } + Issue255DTO = new[] { new Issue255DTO { Time = dt1, Time2 = dt2 } } }; - await _excelTemplater.FillTemplateAsync(path.ToString(), templatePath, value); - var q = _excelImporter.QueryAsync(path.ToString()).ToBlockingEnumerable(); - var rows = q.ToList(); - - Assert.Equal("2021", rows[1].A.ToString()); - Assert.Equal("2021", rows[1].B.ToString()); + await _excelTemplater.FillTemplateAsync(ms, templatePath, value); + + ms.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets[0].Cells; + + Assert.Equal("2021", cells["A2"].Text); + Assert.Equal("2022", cells["B2"].Text); } - //saveas + //export { - 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 _excelExporter.ExportAsync(path.ToString(), value); + await using var ms = new MemoryStream(); + Issue255DTO[] value = + [ + new() { Time = dt1, Time2 = dt2 } + ]; + + var rowsWritten = await _excelExporter.ExportAsync(ms, value); Assert.Single(rowsWritten); Assert.Equal(1, rowsWritten[0]); - - var q = _excelImporter.QueryAsync(path.ToString()).ToBlockingEnumerable(); - 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); } } @@ -77,7 +85,7 @@ private class Issue255DTO public async Task Issue256() { var path = PathHelper.GetFile("xlsx/TestIssue256.xlsx"); - var q = _excelImporter.QueryAsync(path, false).ToBlockingEnumerable(); + var q = await _excelImporter.QueryAsync(path).ToListAsync(); var rows = q.ToList(); Assert.Equal(new DateTime(2003, 4, 16), rows[1].A); @@ -104,37 +112,29 @@ public async Task Issue242() [Fact] public async Task Issue241() { + var date1 = new DateTime(2021, 01, 04); + var date2 = new DateTime(2020, 04, 05); + Issue241Dto[] value = [ - new() { Name="Jack",InDate=new DateTime(2021,01,04) }, - new() { Name="Henry",InDate=new DateTime(2020,04,05) } + new() { Name = "Jack", InDate = date1 }, + new() { Name = "Henry", InDate = date2 } ]; - // xlsx - { - using var file = AutoDeletingPath.Create(); - var path = file.ToString(); - var rowsWritten = await _excelExporter.ExportAsync(path, value); + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); + var rowsWritten = await _excelExporter.ExportAsync(path, value); - Assert.Single(rowsWritten); - Assert.Equal(2, rowsWritten[0]); + Assert.Single(rowsWritten); + Assert.Equal(2, rowsWritten[0]); - { - var q = _excelImporter.QueryAsync(path, true).ToBlockingEnumerable(); - 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[0].Cells; - { - var q = _excelImporter.QueryAsync(path).ToBlockingEnumerable(); - 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); } private class Issue241Dto @@ -1174,17 +1174,17 @@ public async Task Issue157() } ]; - var rowsWritten = await _excelExporter.ExportAsync(path, data); + var rowsWritten = await _excelExporter.ExportAsync(path, data); Assert.Single(rowsWritten); Assert.Equal(5, rowsWritten[0]); - var q = _excelImporter.QueryAsync(path, sheetName: "Sheet1").ToBlockingEnumerable(); + var q = await _excelImporter.QueryAsync(path, sheetName: "Sheet1").ToListAsync(); var rows = q.ToList(); Assert.Equal(6, rows.Count); - Assert.Equal("Sheet1", _excelImporter.GetSheetNames(path).First()); + Assert.Equal("Sheet1", (await _excelImporter.GetSheetNamesAsync(path))[0]); using var p = new ExcelPackage(new FileInfo(path)); - var ws = p.Workbook.Worksheets.First(); + var ws = p.Workbook.Worksheets[0]; Assert.Equal("Sheet1", ws.Name); Assert.Equal("Sheet1", p.Workbook.Worksheets["Sheet1"].Name); } @@ -1194,7 +1194,7 @@ public async Task Issue157() var q = _excelImporter.QueryAsync(path, sheetName: "Sheet1").ToBlockingEnumerable(); var rows = q.ToList(); Assert.Equal(6, rows.Count); - Assert.Equal("Sheet1", _excelImporter.GetSheetNames(path).First()); + Assert.Equal("Sheet1", (await _excelImporter.GetSheetNamesAsync(path))[0]); } using (var p = new ExcelPackage(new FileInfo(path))) { @@ -1476,7 +1476,37 @@ private class Issue138ExcelRow public double? 波段 { get; set; } public double? 當沖 { get; set; } } - + + [Fact] + public async Task Issue520() + { + await using var ms = new MemoryStream(); + + Issue520Dto[] data = [new(542, DateTime.Today, 300)]; + + await _excelExporter.ExportAsync(ms, data); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets.First().Cells; + + Assert.Equal(542.0, cells["A2"].Value); + Assert.Equal(DateTime.Today, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal(300.0, cells["C2"].Value); + } + + class Issue520Dto(long l1, DateTime dt, long l2) + { + [MiniExcelColumn(Format = "R$ #,##0.00", Width = 15)] + public long PaymentValue { get; set; } = l1; + + [MiniExcelColumn(Format = "dd/MM/yyyy", Width = 15)] + public DateTime PaymentDate { get; set; } = dt; + + [MiniExcelColumn(Format = "R$ #,##0.00", Width = 15)] + public long ValueToSettle { get; set; } = l2; + } + [Fact] public async Task TestIssue951() { @@ -1491,7 +1521,7 @@ public async Task TestIssue951() Points = 123 }; - // must not throw + // must not throw because of indexer await _excelTemplater.FillTemplateAsync(path.ToString(), templatePath, value); } diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs index 4193bac7..739722fb 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelIssueTests.cs @@ -6,6 +6,7 @@ using MiniExcelLib.OpenXml.Tests.Utils; using MiniExcelLib.Tests.Common.Utils; using NPOI.XSSF.UserModel; +using LicenseContext = OfficeOpenXml.LicenseContext; namespace MiniExcelLib.OpenXml.Tests; @@ -19,7 +20,7 @@ public class MiniExcelIssueTests(ITestOutputHelper output) static MiniExcelIssueTests() { - EpplusLicence.SetContext(); + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; } /// @@ -130,18 +131,19 @@ private enum DescriptionEnum public void TestIssue430() { using var path = AutoDeletingPath.Create(); - var value = new[] - { - new TestIssue430Dto{ Date=DateTimeOffset.Parse("2021-01-31 10:03:00 +05:00")} - }; + TestIssue430Dto[] value = + [ + new() { Date = new DateTimeOffset(2021, 1, 31, 10, 3, 0, TimeSpan.FromHours(5)) } + ]; _excelExporter.Export(path.ToString(), value); - var rows = _excelImporter.Query(path.ToString()).ToArray(); - Assert.Equal("2021-01-31 10:03:00 +05:00", rows[0].Date.ToString("yyyy-MM-dd HH:mm:ss zzz")); + + var testValue = _excelImporter.Query(path.ToString(), useHeaderRow: true).First(); + Assert.Equal("2021-01-31 10:03:00", testValue.Date.ToString("yyyy-MM-dd HH:mm:ss")); } private class TestIssue430Dto { - [MiniExcelFormat("yyyy-MM-dd HH:mm:ss zzz")] + [MiniExcelFormat("yyyy-MM-dd HH:mm:ss")] public DateTimeOffset Date { get; set; } } @@ -269,15 +271,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 }]; _excelExporter.Export(path.ToString(), value); - var rows = _excelImporter.Query(path.ToString()).ToList(); - Assert.Equal("2022-10", rows[1].B); - 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); } @@ -840,25 +843,29 @@ public void TestIssue325() /// https://github.com/mini-software/MiniExcel/issues/305 /// [Fact] - public void TestIssueI49RZH() + public async Task 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} - }; - _excelExporter.Export(path.ToString(), value); + TestIssueI49RZHDto[] value = + [ + new() { dd = dt }, + new() { dd = null } + ]; + await _excelExporter.ExportAsync(path.FilePath, value, overwriteFile: true); - var rows = _excelImporter.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); } private class TestIssueI49RZHDto { - [MiniExcelFormat("yyyy-MM-dd")] - public DateTimeOffset? dd { get; set; } + [MiniExcelFormat("dd-MM-yyyy")] + public DateTime? dd { get; set; } } /// @@ -870,13 +877,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 } ]; _excelExporter.Export(path.ToString(), value); - var rows = _excelImporter.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); } private class TestIssue312Dto @@ -1400,32 +1411,47 @@ private class IssueI3X2ZLDTO [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"); - using var path = AutoDeletingPath.Create(); + using var ms = new MemoryStream(); var value = new { - Issue255DTO = new[] - { - new Issue255DTO { Time = new DateTime(2021, 01, 01), Time2 = new DateTime(2021, 01, 01) } - } + Issue255DTO = new[] { new Issue255DTO { Time = dt1, Time2 = dt2 } } }; - _excelTemplater.FillTemplate(path.ToString(), templatePath, value); - var rows = _excelImporter.Query(path.ToString()).ToList(); - Assert.Equal("2021", rows[1].A.ToString()); - Assert.Equal("2021", rows[1].B.ToString()); + + _excelTemplater.FillTemplate(ms, templatePath, value); + + ms.Seek(0, SeekOrigin.Begin); + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets[0].Cells; + + Assert.Equal("2021", cells["A2"].Text); + Assert.Equal("2022", cells["B2"].Text); } - //saveas + //export { - using var path = AutoDeletingPath.Create(); - var value = new[] - { - new Issue255DTO { Time = new DateTime(2021, 01, 01) } - }; - _excelExporter.Export(path.ToString(), value); - var rows = _excelImporter.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 = _excelExporter.Export(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); } } @@ -1470,22 +1496,29 @@ public void Issue242() [Fact] public void Issue241() { + var date1 = new DateTime(2021, 01, 04); + var date2 = new DateTime(2020, 04, 05); + Issue241Dto[] value = [ - new() { Name = "Jack", InDate = new DateTime(2021,01,04) }, - new() { Name = "Henry", InDate = new DateTime(2020,04,05) } + new() { Name = "Jack", InDate = date1 }, + new() { Name = "Henry", InDate = date2 } ]; - using var path = AutoDeletingPath.Create(); - _excelExporter.Export(path.ToString(), value); + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); + var rowsWritten = _excelExporter.Export(path, value); + + Assert.Single(rowsWritten); + Assert.Equal(2, rowsWritten[0]); - var rows1 = _excelImporter.Query(path.ToString(), true).ToList(); - Assert.Equal(rows1[0].InDate, "01 04, 2021"); - Assert.Equal(rows1[1].InDate, "04 05, 2020"); + using var package = new ExcelPackage(path); + var cells = package.Workbook.Worksheets.First().Cells; - var rows2 = _excelImporter.Query(path.ToString()).ToList(); - Assert.Equal(rows2[0].InDate, new DateTime(2021, 01, 04)); - Assert.Equal(rows2[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); } private class Issue241Dto @@ -2819,6 +2852,37 @@ public void Issue459() _excelTemplater.FillTemplate(ms, template, values); } + [Fact] + public void Issue520() + { + using var ms = new MemoryStream(); + Issue520Dto[] data = [new(542, DateTime.Today, 300)]; + _excelExporter.Export(ms, data); + ms.Seek(0, SeekOrigin.Begin); + + using var package = new ExcelPackage(ms); + var cells = package.Workbook.Worksheets.First().Cells; + + Assert.Equal(542.0, cells["A2"].Value); + Assert.Equal(542.0.ToString("R$ #,##0.00"), cells["A2"].Text); + Assert.Equal(DateTime.Today, DateTime.FromOADate((double)cells["B2"].Value)); + Assert.Equal(DateTime.Today.ToString("dd/MM/yyyy"), cells["B2"].Text); + Assert.Equal(300.0, cells["C2"].Value); + Assert.Equal(300.0.ToString("R$ #,##0.00"), cells["C2"].Text); + } + + class Issue520Dto(long l1, DateTime dt, long l2) + { + [MiniExcelColumn(Format = "R$ #,##0.00", Width = 15)] + public long PaymentValue { get; set; } = l1; + + [MiniExcelColumn(Format = "dd/MM/yyyy", Width = 15)] + public DateTime PaymentDate { get; set; } = dt; + + [MiniExcelColumn(Format = "R$ #,##0.00", Width = 15)] + public long ValueToSettle { get; set; } = l2; + } + [Fact] public void Issue527() { @@ -2832,7 +2896,7 @@ public void Issue527() var template = PathHelper.GetFile("xlsx/Issue527Template.xlsx"); using var path = AutoDeletingPath.Create(); - _excelTemplater.FillTemplate(path.FilePath, template, value); + _excelTemplater.FillTemplate(path.FilePath, template, value); var rows = _excelImporter.Query(path.FilePath).ToList(); Assert.Equal("General User", rows[1].B); @@ -3385,11 +3449,12 @@ public void TestIssue771() public void TestIssue772() { var path = PathHelper.GetFile("xlsx/TestIssue772.xlsx"); - var rows = _excelImporter.Query(path, sheetName: "Supply plan(daily)", startCell: "A1") - .Cast>() - .ToArray(); + var testValue = _excelImporter.Query(path, sheetName: "Supply plan(daily)", startCell: "A1") + .Skip(19) + .First().C + .ToString(); - Assert.Equal("01108083-1Delta", (string)rows[19]["C"]); + Assert.Equal("01108083-1Delta", testValue); } /// diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs index a1471543..9e6da8a0 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -11,6 +11,11 @@ public class MiniExcelOpenXmlAsyncTests private readonly OpenXmlImporter _excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); + static MiniExcelOpenXmlAsyncTests() + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + } + [Fact] public async Task SaveAsControlChracter() { @@ -28,7 +33,7 @@ public async Task SaveAsControlChracter() '\u0017','\u0018','\u0019','\u001A','\u001B','\u001C','\u001D','\u001E','\u001F','\u007F' ]; var input = chars.Select(s => new { Test = s.ToString() }); - await _excelExporter.ExportAsync(path, input); + await _excelExporter.ExportAsync(path, input); var rows2 = _excelImporter.QueryAsync(path, true).ToBlockingEnumerable().ToArray(); var rows1 = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToArray(); @@ -75,17 +80,14 @@ private class ExcelAttributeDemo2 public async Task CustomAttributeWihoutVaildPropertiesTest() { var path = PathHelper.GetFile("xlsx/TestCustomExcelColumnAttribute.xlsx"); - await Assert.ThrowsAsync(async () => - { - _ = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToList(); - }); + await Assert.ThrowsAsync(async () => await _excelImporter.QueryAsync(path).ToListAsync()); } [Fact] public async Task QueryCustomAttributesTest() { var path = PathHelper.GetFile("xlsx/TestCustomExcelColumnAttribute.xlsx"); - var rows = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(path).ToListAsync(); Assert.Equal("Column1", rows[0].Test1); Assert.Equal("Column2", rows[0].Test2); @@ -100,7 +102,7 @@ public async Task QueryCustomAttributesTest() public async Task QueryCustomAttributes2Test() { var path = PathHelper.GetFile("xlsx/TestCustomExcelColumnAttribute.xlsx"); - var rows = _excelImporter.QueryAsync(path).ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(path).ToListAsync(); Assert.Equal("Column1", rows[0].Test1); Assert.Equal("Column2", rows[0].Test2); @@ -124,9 +126,8 @@ public async Task SaveAsCustomAttributesTest() Test4 = "Test4", }); - await _excelExporter.ExportAsync(path.ToString(), input); - var d = _excelImporter.QueryAsync(path.ToString(), true).ToBlockingEnumerable(); - var rows = d.ToList(); + await _excelExporter.ExportAsync(path.ToString(), input); + var rows = await _excelImporter.QueryAsync(path.ToString(), true).ToListAsync(); var first = rows[0] as IDictionary; Assert.Equal(3, rows.Count); @@ -151,9 +152,8 @@ public async Task SaveAsCustomAttributes2Test() Test4 = "Test4", }); - await _excelExporter.ExportAsync(path.ToString(), input); - var d = _excelImporter.QueryAsync(path.ToString(), true).ToBlockingEnumerable(); - var rows = d.ToList(); + await _excelExporter.ExportAsync(path.ToString(), input); + var rows = await _excelImporter.QueryAsync(path.ToString(), true).ToListAsync(); var first = rows[0] as IDictionary; Assert.Equal(3, rows.Count); @@ -314,8 +314,7 @@ public async Task QueryStrongTypeMapping_Test() var path = PathHelper.GetFile("xlsx/TestTypeMapping.xlsx"); await using (var stream = File.OpenRead(path)) { - var d = _excelImporter.QueryAsync(stream); - var rows = d.ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(stream).ToListAsync(); Assert.Equal(100, rows.Count); Assert.Equal(Guid.Parse("78DE23D2-DCB6-BD3D-EC67-C112BBC322A2"), rows[0].ID); @@ -355,7 +354,7 @@ public async Task AutoCheckTypeTest() { var path = PathHelper.GetFile("xlsx/TestTypeMapping_AutoCheckFormat.xlsx"); await using var stream = FileHelper.OpenRead(path); - _ = _excelImporter.QueryAsync(stream).ToBlockingEnumerable().ToList(); + _ = _excelImporter.QueryAsync(stream).ToListAsync(); } [Fact] @@ -477,7 +476,7 @@ public async Task SaveAsFileWithDimensionByICollection() using (var file = AutoDeletingPath.Create()) { var path = file.ToString(); - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using (var stream = File.OpenRead(path)) { var d = _excelImporter.QueryAsync(stream, useHeaderRow: false).ToBlockingEnumerable().Cast>(); @@ -501,7 +500,7 @@ public async Task SaveAsFileWithDimensionByICollection() } using var newPath = AutoDeletingPath.Create(); - await _excelExporter.ExportAsync(newPath.ToString(), values, false); + await _excelExporter.ExportAsync(newPath.ToString(), values, false); Assert.Equal("A1:B2", SheetHelper.GetFirstSheetDimensionRefValue(newPath.ToString())); } @@ -511,7 +510,7 @@ public async Task SaveAsFileWithDimensionByICollection() using (var file = AutoDeletingPath.Create()) { var path = file.ToString(); - await _excelExporter.ExportAsync(path, values, false); + await _excelExporter.ExportAsync(path, values, false); await using (var stream = File.OpenRead(path)) { var d = _excelImporter.QueryAsync(stream, useHeaderRow: false).ToBlockingEnumerable(); @@ -525,7 +524,7 @@ public async Task SaveAsFileWithDimensionByICollection() using (var file = AutoDeletingPath.Create()) { var path = file.ToString(); - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); { await using var stream = File.OpenRead(path); var d = _excelImporter.QueryAsync(stream, useHeaderRow: false).ToBlockingEnumerable(); @@ -547,7 +546,7 @@ public async Task SaveAsFileWithDimensionByICollection() using (var file = AutoDeletingPath.Create()) { var path = file.ToString(); - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using (var stream = File.OpenRead(path)) { @@ -573,7 +572,7 @@ public async Task SaveAsFileWithDimensionByICollection() using (var path = AutoDeletingPath.Create()) { - await _excelExporter.ExportAsync(path.ToString(), values, false); + await _excelExporter.ExportAsync(path.ToString(), values, false); Assert.Equal("A1:B2", SheetHelper.GetFirstSheetDimensionRefValue(path.ToString())); } } @@ -594,15 +593,13 @@ public async Task SaveAsFileWithDimension() var path = file.ToString(); using var table = new DataTable(); - await _excelExporter.ExportAsync(path, table); + await _excelExporter.ExportAsync(path, table); Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); - { - await using var stream = File.OpenRead(path); - var d = _excelImporter.QueryAsync(stream).ToBlockingEnumerable(); - var rows = d.ToList(); - Assert.Single(rows); - } - await _excelExporter.ExportAsync(path, table, printHeader: false, overwriteFile: true); + + var rows = await _excelImporter.QueryAsync(path).ToListAsync(); + Assert.Empty(rows); + + await _excelExporter.ExportAsync(path, table, printHeader: false, overwriteFile: true); Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); } { @@ -617,13 +614,16 @@ public async Task SaveAsFileWithDimension() table.Rows.Add(@"""<>+-*//}{\\n", 1234567890); table.Rows.Add("Hello World", -1234567890, false, DateTime.Now); - await _excelExporter.ExportAsync(path, table); + await _excelExporter.ExportAsync(path, table); Assert.Equal("A1:D3", SheetHelper.GetFirstSheetDimensionRefValue(path)); await using (var stream = File.OpenRead(path)) { - var d = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().Cast>(); - var rows = d.ToList(); + var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true) + .ToBlockingEnumerable() + .Cast>() + .ToList(); + Assert.Equal(2, rows.Count); Assert.Equal(@"""<>+-*//}{\\n", rows[0]["a"]); Assert.Equal(1234567890d, rows[0]["b"]); @@ -633,8 +633,11 @@ public async Task SaveAsFileWithDimension() await using (var stream = File.OpenRead(path)) { - var d = _excelImporter.QueryAsync(stream).ToBlockingEnumerable().Cast>(); - var rows = d.ToList(); + var rows = _excelImporter.QueryAsync(stream) + .ToBlockingEnumerable() + .Cast>() + .ToList(); + Assert.Equal(3, rows.Count); Assert.Equal("a", rows[0]["A"]); Assert.Equal("b", rows[0]["B"]); @@ -642,7 +645,7 @@ public async Task SaveAsFileWithDimension() Assert.Equal("d", rows[0]["D"]); } - await _excelExporter.ExportAsync(path, table, printHeader: false, overwriteFile: true); + await _excelExporter.ExportAsync(path, table, printHeader: false, overwriteFile: true); Assert.Equal("A1:D2", SheetHelper.GetFirstSheetDimensionRefValue(path)); } @@ -653,7 +656,7 @@ public async Task SaveAsFileWithDimension() table.Rows.Add("A"); table.Rows.Add("B"); - await _excelExporter.ExportAsync(path.ToString(), table); + await _excelExporter.ExportAsync(path.ToString(), table); Assert.Equal("A1:A3", SheetHelper.GetFirstSheetDimensionRefValue(path.ToString())); } } @@ -661,45 +664,32 @@ public async Task SaveAsFileWithDimension() [Fact] public async Task SaveAsByDataTableTest() { - { - using var file = AutoDeletingPath.Create(); - var path = file.ToString(); - - var now = DateTime.Now; - - using var table = new DataTable(); - table.Columns.Add("a", typeof(string)); - table.Columns.Add("b", typeof(decimal)); - table.Columns.Add("c", typeof(bool)); - table.Columns.Add("d", typeof(DateTime)); - table.Rows.Add(@"""<>+-*//}{\\n", 1234567890, true, now); - table.Rows.Add("Hello World", -1234567890, false, now.Date); + using var file = AutoDeletingPath.Create(); + var path = file.ToString(); - await _excelExporter.ExportAsync(path, table); + var now = DateTime.Now; + var dt = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); - using var p = new ExcelPackage(new FileInfo(path)); - var ws = p.Workbook.Worksheets.First(); + using var table = new DataTable(); + table.Columns.Add("a", typeof(string)); + table.Columns.Add("b", typeof(decimal)); + table.Columns.Add("c", typeof(bool)); + table.Columns.Add("d", typeof(DateTime)); + table.Rows.Add(@"""<>+-*//}{\\n", 1234567890, true, dt); + await _excelExporter.ExportAsync(path, table); - Assert.True(ws.Cells["A1"].Value.ToString() == "a"); - Assert.True(ws.Cells["B1"].Value.ToString() == "b"); - Assert.True(ws.Cells["C1"].Value.ToString() == "c"); - Assert.True(ws.Cells["D1"].Value.ToString() == "d"); + using var p = new ExcelPackage(path); + var cells = p.Workbook.Worksheets[0].Cells; - Assert.True(ws.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cells["B2"].Value.ToString() == @"1234567890"); - Assert.True(ws.Cells["C2"].Value.ToString() == true.ToString()); - Assert.True(ws.Cells["D2"].Value.ToString() == now.ToString()); - } - { - using var path = AutoDeletingPath.Create(); - using var table = new DataTable(); - table.Columns.Add("Column1", typeof(string)); - table.Columns.Add("Column2", typeof(int)); - table.Rows.Add("MiniExcel", 1); - table.Rows.Add("Github", 2); + Assert.Equal("a", cells["A1"].Text); + Assert.Equal("b", cells["B1"].Text); + Assert.Equal("c", cells["C1"].Text); + Assert.Equal("d", cells["D1"].Text); - await _excelExporter.ExportAsync(path.ToString(), table); - } + Assert.Equal(@"""<>+-*//}{\\n", cells["A2"].Value); + Assert.Equal(1234567890, (double)cells["B2"].Value); + Assert.True((bool)cells["C2"].Value); + Assert.Equal(dt, (DateTime)cells["D2"].Value); } [Fact] @@ -730,16 +720,15 @@ public async Task QueryByLINQExtensionsVoidTaskLargeFileOOMTest() public async Task EmptyTest() { using var path = AutoDeletingPath.Create(); - - await using (var connection = Db.GetConnection("Data Source=:memory:")) + await using (var connection = Db.GetConnection()) { var rows = await connection.QueryAsync("with cte as (select 1 id,2 val) select * from cte where 1=2"); - await _excelExporter.ExportAsync(path.ToString(), rows); + await _excelExporter.ExportAsync(path.ToString(), rows); } await using (var stream = File.OpenRead(path.ToString())) { - var row = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable(); + var row = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); Assert.Empty(row); } } @@ -747,7 +736,6 @@ public async Task EmptyTest() [Fact] public async Task SaveAsByIEnumerableIDictionary() { - { using var file = AutoDeletingPath.Create(); var path = file.ToString(); @@ -756,7 +744,7 @@ public async Task SaveAsByIEnumerableIDictionary() new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, new() { { "Column1", "Github" }, { "Column2", 2 } } ]; - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using (var stream = File.OpenRead(path)) { @@ -793,7 +781,7 @@ public async Task SaveAsByIEnumerableIDictionary() new() { { 1, "MiniExcel" }, { 2, 1 } }, new() { { 1, "Github" }, { 2, 2 } } ]; - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using (var stream = File.OpenRead(path)) { @@ -825,7 +813,7 @@ public async Task SaveAsByIEnumerableIDictionaryWithDynamicConfiguration() new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, new() { { "Column1", "Github" }, { "Column2", 2 } } ]; - await _excelExporter.ExportAsync(path, values, configuration: config); + await _excelExporter.ExportAsync(path, values, configuration: config); await using (var stream = File.OpenRead(path)) { @@ -856,7 +844,7 @@ public async Task SaveAsFrozenRowsAndColumnsTest() using var file = AutoDeletingPath.Create(); var path = file.ToString(); - await _excelExporter.ExportAsync( + await _excelExporter.ExportAsync( path, new[] { @@ -868,7 +856,7 @@ await _excelExporter.ExportAsync( await using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); Assert.Equal("MiniExcel", rows[0].Column1); Assert.Equal(1, rows[0].Column2); @@ -887,13 +875,13 @@ await _excelExporter.ExportAsync( table.Rows.Add("Hello World", -1234567890, false, DateTime.Now.Date); using var pathTable = AutoDeletingPath.Create(); - await _excelExporter.ExportAsync(pathTable.ToString(), table, configuration: config); + await _excelExporter.ExportAsync(pathTable.ToString(), table, configuration: config); Assert.Equal("A1:D3", SheetHelper.GetFirstSheetDimensionRefValue(pathTable.ToString())); // data reader await using var reader = table.CreateDataReader(); using var pathReader = AutoDeletingPath.Create(); - await _excelExporter.ExportAsync(pathReader.ToString(), reader, configuration: config); + await _excelExporter.ExportAsync(pathReader.ToString(), reader, configuration: config); Assert.Equal("A1:D3", SheetHelper.GetFirstSheetDimensionRefValue(pathTable.ToString())); } @@ -904,10 +892,10 @@ public async Task SaveAsByDapperRows() var path = file.ToString(); // Dapper Query - await using (var connection = Db.GetConnection("Data Source=:memory:")) + await using (var connection = Db.GetConnection()) { var rows = await connection.QueryAsync("select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2"); - await _excelExporter.ExportAsync(path, rows); + await _excelExporter.ExportAsync(path, rows); } Assert.Equal("A1:B3", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -921,30 +909,30 @@ public async Task SaveAsByDapperRows() } // Empty - await using (var connection = Db.GetConnection("Data Source=:memory:")) + await using (var connection = Db.GetConnection()) { - var rows = (await connection.QueryAsync("with cte as (select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2)select * from cte where 1=2")).ToList(); - await _excelExporter.ExportAsync(path, rows, overwriteFile: true); + var rows = await connection.QueryAsync("with cte as (select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2)select * from cte where 1=2"); + await _excelExporter.ExportAsync(path, rows.AsList(), overwriteFile: true); } await using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.QueryAsync(stream, useHeaderRow: false).ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: false).ToListAsync(); Assert.Empty(rows); } await using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().ToList(); + var rows = await _excelImporter.QueryAsync(stream, useHeaderRow: true).ToListAsync(); Assert.Empty(rows); } Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); // ToList - await using (var connection = Db.GetConnection("Data Source=:memory:")) + await using (var connection = Db.GetConnection()) { var rows = (await connection.QueryAsync("select 'MiniExcel' as Column1,1 as Column2 union all select 'Github',2")).ToList(); - await _excelExporter.ExportAsync(path, rows, overwriteFile: true); + await _excelExporter.ExportAsync(path, rows, overwriteFile: true); } Assert.Equal("A1:B3", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -985,7 +973,7 @@ public async Task QueryByStrongTypeParameterTest() new() { Column1 = "MiniExcel", Column2 = 1 }, new() { Column1 = "Github", Column2 = 2 } ]; - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using var stream = File.OpenRead(path); var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().Cast>().ToList(); @@ -1006,7 +994,7 @@ public async Task QueryByDictionaryStringAndObjectParameterTest() new() { { "Column1", "MiniExcel" }, { "Column2", 1 } }, new() { { "Column1", "Github" }, { "Column2", 2 } } ]; - await _excelExporter.ExportAsync(path, values); + await _excelExporter.ExportAsync(path, values); await using var stream = File.OpenRead(path); var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().Cast>().ToList(); @@ -1061,7 +1049,7 @@ public async Task SaveAsBasicCreateTest() using var file = AutoDeletingPath.Create(); var path = file.ToString(); - await _excelExporter.ExportAsync(path, new[] + await _excelExporter.ExportAsync(path, new[] { new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} @@ -1086,19 +1074,24 @@ public async Task SaveAsBasicStreamTest() using var file = AutoDeletingPath.Create(); var path = file.ToString(); - var values = new[] - { + object[] values = + [ new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} - }; + ]; + await using (var stream = new FileStream(path, FileMode.CreateNew)) { - await _excelExporter.ExportAsync(stream, values); + await _excelExporter.ExportAsync(stream, values); } await using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().Cast>().ToList(); + var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true) + .ToBlockingEnumerable() + .Cast>() + .ToList(); + Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -1113,17 +1106,22 @@ public async Task SaveAsBasicStreamTest() new { Column1 = "MiniExcel", Column2 = 1 }, new { Column1 = "Github", Column2 = 2} }; + await using (var stream = new MemoryStream()) await using (var fileStream = new FileStream(path, FileMode.Create)) { - await _excelExporter.ExportAsync(stream, values); + await _excelExporter.ExportAsync(stream, values); stream.Seek(0, SeekOrigin.Begin); await stream.CopyToAsync(fileStream); } await using (var stream = File.OpenRead(path)) { - var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable().Cast>().ToList(); + var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true) + .ToBlockingEnumerable() + .Cast>() + .ToList(); + Assert.Equal("MiniExcel", rows[0]["Column1"]); Assert.Equal(1d, rows[0]["Column2"]); Assert.Equal("Github", rows[1]["Column1"]); @@ -1136,7 +1134,7 @@ public async Task SaveAsBasicStreamTest() public async Task SaveAsSpecialAndTypeCreateTest() { using var path = AutoDeletingPath.Create(); - await _excelExporter.ExportAsync(path.ToString(), new[] + await _excelExporter.ExportAsync(path.ToString(), new[] { new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = DateTime.Now }, new { a = "Hello World", b = -1234567890, c = false, d = DateTime.Now.Date} @@ -1149,51 +1147,60 @@ await _excelExporter.ExportAsync(path.ToString(), new[] public async Task SaveAsFileEpplusCanReadTest() { using var path = AutoDeletingPath.Create(); + var now = DateTime.Now; + var dt = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); - await _excelExporter.ExportAsync(path.ToString(), new[] + await _excelExporter.ExportAsync(path.ToString(), new[] { - new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = now}, - new { a = "Hello World", b = -1234567890, c = false, d = now.Date} + new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = dt}, + new { a = "Hello World", b = -1234567890, c = false, d = dt.Date} }); - using var p = new ExcelPackage(new FileInfo(path.ToString())); - var ws = p.Workbook.Worksheets.First(); + using var p = new ExcelPackage(path.ToString()); + var cells = p.Workbook.Worksheets[0].Cells; + + Assert.Equal("a", cells["A1"].Value.ToString()); + Assert.Equal("b", cells["B1"].Value.ToString()); + Assert.Equal("c", cells["C1"].Value.ToString()); + Assert.Equal("d", cells["D1"].Value.ToString()); - Assert.True(ws.Cells["A1"].Value.ToString() == "a"); - Assert.True(ws.Cells["B1"].Value.ToString() == "b"); - Assert.True(ws.Cells["C1"].Value.ToString() == "c"); - Assert.True(ws.Cells["D1"].Value.ToString() == "d"); + Assert.Equal(@"""<>+-*//}{\\n", cells["A2"].Value); + Assert.Equal(1234567890, (double)cells["B2"].Value); + Assert.True((bool)cells["C2"].Value); + Assert.Equal(dt, (DateTime)cells["D2"].Value); - Assert.True(ws.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cells["B2"].Value.ToString() == "1234567890"); - Assert.True(ws.Cells["C2"].Value.ToString() == true.ToString()); - Assert.True(ws.Cells["D2"].Value.ToString() == now.ToString()); + Assert.Equal("Hello World", cells["A3"].Value); + Assert.Equal(-1234567890, (double)cells["B3"].Value); + Assert.False((bool)cells["C3"].Value); + Assert.Equal(dt.Date, (DateTime)cells["D3"].Value); } [Fact] public async Task SavaAsClosedXmlCanReadTest() { var now = DateTime.Now; - using var path = AutoDeletingPath.Create(); + var dt = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); - await _excelExporter.ExportAsync(path.ToString(), new[] + using var path = AutoDeletingPath.Create(); + await _excelExporter.ExportAsync(path.ToString(), new[] { - new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = now}, - new { a = "Hello World", b = -1234567890, c = false, d = now.Date} + new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = dt }, + new { a = "Hello World", b = -1234567890, c = false, d = dt.Date } }); + using var workbook = new XLWorkbook(path.ToString()); var ws = workbook.Worksheets.First(); - Assert.True(ws.Cell("A1").Value.ToString() == "a"); - Assert.True(ws.Cell("D1").Value.ToString() == "d"); - Assert.True(ws.Cell("B1").Value.ToString() == "b"); - Assert.True(ws.Cell("C1").Value.ToString() == "c"); + Assert.Equal(@"""<>+-*//}{\\n", ws.Cell("A2").Value); + Assert.Equal(1234567890, (double)ws.Cell("B2").Value); + Assert.True((bool)ws.Cell("C2").Value); + Assert.Equal(dt, ws.Cell("D2").Value); - Assert.True(ws.Cell("A2").Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cell("B2").Value.ToString() == "1234567890"); - Assert.Equal(bool.TrueString, ws.Cell("C2").Value.ToString(), ignoreCase: true); - Assert.True(ws.Cell("D2").Value.ToString() == now.ToString()); + Assert.Equal("Hello World", ws.Cell("A3").Value); + Assert.Equal(-1234567890, (double)ws.Cell("B3").Value); + Assert.False((bool)ws.Cell("C3").Value); + Assert.Equal(dt.Date, ws.Cell("D3").Value); } [Fact] @@ -1202,7 +1209,7 @@ public async Task ContentTypeUriContentTypeReadCheckTest() var now = DateTime.Now; using var path = AutoDeletingPath.Create(); - await _excelExporter.ExportAsync(path.ToString(), new[] + await _excelExporter.ExportAsync(path.ToString(), new[] { new { a = @"""<>+-*//}{\\n", b = 1234567890, c = true, d = now}, new { a = "Hello World", b = -1234567890, c = false, d = now.Date} @@ -1212,11 +1219,11 @@ await _excelExporter.ExportAsync(path.ToString(), new[] .Select(s => new { s.CompressionOption, s.ContentType, s.Uri, s.Package.GetType().Name }) .ToDictionary(s => s.Uri.ToString(), s => s); - Assert.True(allParts["/xl/styles.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"); - Assert.True(allParts["/xl/workbook.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"); - Assert.True(allParts["/xl/worksheets/sheet1.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"); - Assert.True(allParts["/xl/_rels/workbook.xml.rels"].ContentType == "application/vnd.openxmlformats-package.relationships+xml"); - Assert.True(allParts["/_rels/.rels"].ContentType == "application/vnd.openxmlformats-package.relationships+xml"); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", allParts["/xl/styles.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", allParts["/xl/workbook.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", allParts["/xl/worksheets/sheet1.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-package.relationships+xml", allParts["/xl/_rels/workbook.xml.rels"].ContentType); + Assert.Equal("application/vnd.openxmlformats-package.relationships+xml", allParts["/_rels/.rels"].ContentType); } [Fact] @@ -1228,9 +1235,8 @@ await Assert.ThrowsAsync(async () => using var cts = new CancellationTokenSource(); await cts.CancelAsync(); - await using var stream = FileHelper.OpenRead(path); - var rows = _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToBlockingEnumerable(cts.Token).ToList(); + _ = await _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToListAsync(cts.Token); }); } @@ -1242,17 +1248,15 @@ await Assert.ThrowsAsync(async () => var path = PathHelper.GetFile("xlsx/bigExcel.xlsx"); var cts = new CancellationTokenSource(); - var cancelTask = Task.Run(async () => + _ = Task.Run(async () => { - await Task.Delay(2000, cts.Token); + await Task.Delay(500); await cts.CancelAsync(); cts.Token.ThrowIfCancellationRequested(); }); await using var stream = FileHelper.OpenRead(path); - var d = _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToBlockingEnumerable(cts.Token); - await cancelTask; - _ = d.ToList(); + _ = await _excelImporter.QueryAsync(stream, cancellationToken: cts.Token).ToListAsync(cts.Token); }); } @@ -1298,7 +1302,7 @@ public async Task DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingIDataRe ] }; await using var reader = table.CreateDataReader(); - await _excelExporter.ExportAsync(path.ToString(), reader, configuration: configuration); + await _excelExporter.ExportAsync(path.ToString(), reader, configuration: configuration); await using var stream = File.OpenRead(path.ToString()); var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable() @@ -1364,7 +1368,7 @@ public async Task DynamicColumnsConfigurationIsUsedWhenCreatingExcelUsingDataTab } ] }; - await _excelExporter.ExportAsync(path.ToString(), table, configuration: configuration); + await _excelExporter.ExportAsync(path.ToString(), table, configuration: configuration); await using var stream = File.OpenRead(path.ToString()); var rows = _excelImporter.QueryAsync(stream, useHeaderRow: true).ToBlockingEnumerable() @@ -1400,25 +1404,27 @@ public async Task SaveAsByMiniExcelDataReader() new() { Column1= "MiniExcel" ,Column2 = 1 }, new() { Column1 = "Github", Column2 = 2 } }; - await _excelExporter.ExportAsync(path1.ToString(), values); + await _excelExporter.ExportAsync(path1.ToString(), values); using var path2 = AutoDeletingPath.Create(); - await using IMiniExcelDataReader? reader = _excelImporter.GetDataReader(path1.ToString(), true); + await using var reader = _excelImporter.GetDataReader(path1.ToString(), true); - await _excelExporter.ExportAsync(path2.ToString(), reader); - var results = _excelImporter.QueryAsync(path2.ToString()).ToBlockingEnumerable().ToList(); - - Assert.True(results.Count == 2); - Assert.True(results.First().Column1 == "MiniExcel"); - Assert.True(results.First().Column2 == 1); - Assert.True(results.Last().Column1 == "Github"); - Assert.True(results.Last().Column2 == 2); + await _excelExporter.ExportAsync(path2.ToString(), reader); + var results = await _excelImporter.QueryAsync(path2.ToString()).ToListAsync(); + + Assert.Equal(2, results.Count); + Assert.Equal("MiniExcel", results.First().Column1); + Assert.Equal(1, results.First().Column2); + Assert.Equal("Github", results.Last().Column1); + Assert.Equal(2, results.Last().Column2); } [Fact] public async Task InsertSheetTest() { var now = DateTime.Now; + var dt = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second); + using var file = AutoDeletingPath.Create(); var path = file.ToString(); @@ -1428,24 +1434,28 @@ public async Task InsertSheetTest() table.Columns.Add("b", typeof(decimal)); table.Columns.Add("c", typeof(bool)); table.Columns.Add("d", typeof(DateTime)); - table.Rows.Add(@"""<>+-*//}{\\n", 1234567890, true, now); - table.Rows.Add("Hello World", -1234567890, false, now.Date); + table.Rows.Add(@"""<>+-*//}{\\n", 1234567890, true, dt); + table.Rows.Add("Hello World", -1234567890, false, dt.Date); + await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet1"); - await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet1"); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet1 = p.Workbook.Worksheets[0]; - Assert.True(sheet1.Cells["A1"].Value.ToString() == "a"); - Assert.True(sheet1.Cells["B1"].Value.ToString() == "b"); - Assert.True(sheet1.Cells["C1"].Value.ToString() == "c"); - Assert.True(sheet1.Cells["D1"].Value.ToString() == "d"); - - Assert.True(sheet1.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(sheet1.Cells["B2"].Value.ToString() == "1234567890"); - Assert.True(sheet1.Cells["C2"].Value.ToString() == true.ToString()); - Assert.True(sheet1.Cells["D2"].Value.ToString() == now.ToString()); - - Assert.True(sheet1.Name == "Sheet1"); + Assert.Equal("Sheet1", sheet1.Name); + Assert.Equal("a", sheet1.Cells["A1"].Value.ToString()); + Assert.Equal("b", sheet1.Cells["B1"].Value.ToString()); + Assert.Equal("c", sheet1.Cells["C1"].Value.ToString()); + Assert.Equal("d", sheet1.Cells["D1"].Value.ToString()); + + Assert.Equal(@"""<>+-*//}{\\n", sheet1.Cells["A2"].Value); + Assert.Equal(1234567890, (double)sheet1.Cells["B2"].Value); + Assert.True((bool)sheet1.Cells["C2"].Value); + Assert.Equal(dt, (DateTime)sheet1.Cells["D2"].Value); + + Assert.Equal("Hello World", sheet1.Cells["A3"].Value); + Assert.Equal(-1234567890, (double)sheet1.Cells["B3"].Value); + Assert.False((bool)sheet1.Cells["C3"].Value); + Assert.Equal(dt.Date, (DateTime)sheet1.Cells["D3"].Value); } { using var table = new DataTable(); @@ -1454,28 +1464,28 @@ public async Task InsertSheetTest() table.Rows.Add("MiniExcel", 1); table.Rows.Add("Github", 2); - await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet2"); - using var p = new ExcelPackage(new FileInfo(path)); + await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet2"); + using var p = new ExcelPackage(path); var sheet2 = p.Workbook.Worksheets[1]; - Assert.True(sheet2.Cells["A1"].Value.ToString() == "Column1"); - Assert.True(sheet2.Cells["B1"].Value.ToString() == "Column2"); + Assert.Equal("Column1", sheet2.Cells["A1"].Value.ToString()); + Assert.Equal("Column2", sheet2.Cells["B1"].Value.ToString()); - Assert.True(sheet2.Cells["A2"].Value.ToString() == "MiniExcel"); - Assert.True(sheet2.Cells["B2"].Value.ToString() == "1"); + Assert.Equal("MiniExcel", sheet2.Cells["A2"].Value.ToString()); + Assert.Equal(1, (double)sheet2.Cells["B2"].Value); - Assert.True(sheet2.Cells["A3"].Value.ToString() == "Github"); - Assert.True(sheet2.Cells["B3"].Value.ToString() == "2"); + Assert.Equal("Github", sheet2.Cells["A3"].Value.ToString()); + Assert.Equal(2, (double)sheet2.Cells["B3"].Value); - Assert.True(sheet2.Name == "Sheet2"); + Assert.Equal("Sheet2", sheet2.Name); } { using var table = new DataTable(); table.Columns.Add("Column1", typeof(string)); table.Columns.Add("Column2", typeof(DateTime)); - table.Rows.Add("Test", now); + table.Rows.Add("Test", dt); - await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet2", printHeader: false, configuration: new OpenXmlConfiguration + await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet2", printHeader: false, configuration: new OpenXmlConfiguration { FastMode = true, AutoFilter = false, @@ -1484,29 +1494,29 @@ public async Task InsertSheetTest() [ new DynamicExcelColumn("Column2") { - Name = "Its Date", + Name = "Date", Index = 1, Width = 150, - Format = "dd.mm.yyyy hh:mm:ss", + Format = "dd.mm.yyyy hh:mm:ss" } ] }, overwriteSheet: true); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet2 = p.Workbook.Worksheets[1]; - Assert.True(sheet2.Cells["A1"].Value.ToString() == "Test"); - Assert.True(sheet2.Cells["B1"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); - Assert.True(sheet2.Name == "Sheet2"); + Assert.Equal("Sheet2", sheet2.Name); + Assert.Equal("Test", sheet2.Cells["A1"].Value); + Assert.Equal(dt.ToString("dd.MM.yyyy HH:mm:ss"), sheet2.Cells["B1"].Text ); } { using var table = new DataTable(); table.Columns.Add("Column1", typeof(string)); table.Columns.Add("Column2", typeof(DateTime)); - table.Rows.Add("MiniExcel", now); - table.Rows.Add("Github", now); + table.Rows.Add("MiniExcel", dt); + table.Rows.Add("Github", dt); - await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet3", configuration: new OpenXmlConfiguration + await _excelExporter.InsertSheetAsync(path, table, sheetName: "Sheet3", configuration: new OpenXmlConfiguration { FastMode = true, AutoFilter = false, @@ -1515,27 +1525,27 @@ public async Task InsertSheetTest() [ new DynamicExcelColumn("Column2") { - Name = "Its Date", + Name = "Date", Index = 1, Width = 150, - Format = "dd.mm.yyyy hh:mm:ss", + Format = "dd.mm.yyyy hh:mm:ss" } ] }); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet3 = p.Workbook.Worksheets[2]; - Assert.True(sheet3.Cells["A1"].Value.ToString() == "Column1"); - Assert.True(sheet3.Cells["B1"].Value.ToString() == "Its Date"); + Assert.Equal("Column1", sheet3.Cells["A1"].Value); + Assert.Equal("Date", sheet3.Cells["B1"].Value); - Assert.True(sheet3.Cells["A2"].Value.ToString() == "MiniExcel"); - Assert.True(sheet3.Cells["B2"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); + Assert.Equal("MiniExcel", sheet3.Cells["A2"].Value); + Assert.Equal(dt.ToString("dd.MM.yyyy HH:mm:ss"), sheet3.Cells["B2"].Text); - Assert.True(sheet3.Cells["A3"].Value.ToString() == "Github"); - Assert.True(sheet3.Cells["B3"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); + Assert.Equal("Github", sheet3.Cells["A3"].Value); + Assert.Equal(dt.ToString("dd.MM.yyyy HH:mm:ss"), sheet3.Cells["B3"].Text); - Assert.True(sheet3.Name == "Sheet3"); + Assert.Equal("Sheet3", sheet3.Name); } } @@ -1544,22 +1554,23 @@ public async Task SaveAsByAsyncEnumerable() { using var path = AutoDeletingPath.Create(); -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - static async IAsyncEnumerable GetValues() - { - yield return new Demo { Column1 = "MiniExcel", Column2 = 1 }; - yield return new Demo { Column1 = "Github", Column2 = 2 }; - } -#pragma warning restore CS1998 - - await _excelExporter.ExportAsync(path.ToString(), GetValues()); - var results = _excelImporter.Query(path.ToString(), useHeaderRow: true).ToList(); + await _excelExporter.ExportAsync(path.ToString(), GetValues()); + var results = await _excelImporter.QueryAsync(path.ToString(), useHeaderRow: true).ToListAsync(); Assert.Equal(2, results.Count); Assert.Equal("MiniExcel", results[0].Column1); Assert.Equal(1, results[0].Column2); Assert.Equal("Github", results[^1].Column1); Assert.Equal(2, results[^1].Column2); + return; + + static async IAsyncEnumerable GetValues() + { + await Task.CompletedTask; + yield return new Demo { Column1 = "MiniExcel", Column2 = 1 }; + await Task.CompletedTask; + yield return new Demo { Column1 = "Github", Column2 = 2 }; + } } [Fact] @@ -1598,4 +1609,325 @@ public async Task ExportDataTableWithProgressTest() } } } -} \ 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 _excelExporter.ExportAsync(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 + /// + [MiniExcelFormat("\"$\"#,##0.00")] + public decimal Currency { get; set; } = currency; + + /// + /// Currency format with 2 decimal places, parentheses for negatives + /// + [MiniExcelFormat("$#,##0.00_);($#,##0.00)")] + public decimal AlignedCurrency { get; set; } = alignedCurrency; + + /// + /// Percentage format with 0 decimal places + /// + [MiniExcelFormat("0%")] + public decimal Percentage { get; set; } = percentage; + + /// + /// Scientific notation format with 2 decimal places + /// + [MiniExcelFormat("0.00E+00")] + public double ScientificNotation { get; set; } = scientificNotation; + + [MiniExcelFormat("0.00E+00"), MiniExcelHidden] + public double ScientificNotationDuplicate { get; set; } = scientificNotation; + + /// + /// Fixed decimal places (6 decimal places) + /// + [MiniExcelFormat("0.000000")] + public decimal FixedDecimal { get; set; } = fixedDecimal; + + /// + /// Phone number format + /// + [MiniExcelFormat("[<=9999999]###-####;(###) ###-####")] + public long PhoneNumber { get; set; } = phoneNumber; + + /// + /// Simple integer format that shows the number in its full length (no scientific notation) + /// + [MiniExcelFormat("#")] + public long VeryLongNumber { get; set; } = veryLongNumber; + + /// + /// Simple decimal format with 3 decimal places + /// + [MiniExcelFormat("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 _excelExporter.ExportAsync(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) + /// + [MiniExcelFormat("mm/dd/yyyy")] + public DateTime ShortDate { get; set; } = shortDate; + + /// + /// Long date format (dddd, mmmm dd, yyyy) + /// + [MiniExcelFormat("dddd, mmmm dd, yyyy")] + public DateTime LongDate { get; set; } = longDate; + + /// + /// Date with time format (yyyy-mm-dd hh:mm:ss) + /// + [MiniExcelFormat("yyyy-mm-dd hh:mm:ss")] + public DateTime DateWithTime { get; set; } = dateWithTime; + + /// + /// Time only format ([h]:mm:ss) + /// + [MiniExcelFormat("[h]:mm:ss")] + public TimeSpan TimeOnly { get; set; } = timeOnly; + + /// + /// ISO 8601 datetime format (yyyy-mm-ddThh:mm:ss) + /// + [MiniExcelFormat("yyyy-mm-dd\"T\"hh:mm:ss")] + public DateTime IsoDateTime { get; set; } = isoDateTime; + + /// + /// Custom European format (dd.mm.yyyy hh:mm) + /// + [MiniExcelFormat("dd.mm.yyyy hh:mm")] + public DateTime CustomDateTime { get; set; } = customDateTime; + + /// + /// Month and year format (mmmm yyyy) + /// + [MiniExcelFormat("mmmm yyyy")] + public DateTime MonthYear { get; set; } = monthYear; + } +} diff --git a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs index 0a1027e1..44bbcccc 100644 --- a/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs +++ b/tests/MiniExcel.OpenXml.Tests/MiniExcelOpenXmlTests.cs @@ -15,6 +15,11 @@ public class MiniExcelOpenXmlTests(ITestOutputHelper output) private readonly OpenXmlImporter _excelImporter = MiniExcel.Importers.GetOpenXmlImporter(); private readonly OpenXmlExporter _excelExporter = MiniExcel.Exporters.GetOpenXmlExporter(); + static MiniExcelOpenXmlTests() + { + ExcelPackage.LicenseContext = LicenseContext.NonCommercial; + } + [Fact] public void GetColumnsTest() { @@ -631,15 +636,13 @@ public void SaveAsFileWithDimension() var path = file.ToString(); var table = new DataTable(); - _excelExporter.Export(path, table); + _excelExporter.Export(path, table); Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); - { - using var stream = File.OpenRead(path); - var rows = _excelImporter.Query(stream).ToList(); - Assert.Single(rows); - } + + var rows = _excelImporter.Query(path).ToList(); + Assert.Empty(rows); - _excelExporter.Export(path, table, printHeader: false, overwriteFile: true); + _excelExporter.Export(path, table, printHeader: false, overwriteFile: true); Assert.Equal("A1", SheetHelper.GetFirstSheetDimensionRefValue(path)); } @@ -655,28 +658,22 @@ public void SaveAsFileWithDimension() table.Rows.Add(@"""<>+-*//}{\\n", 1234567890); table.Rows.Add("Hello World", -1234567890, false, DateTime.Now); - _excelExporter.Export(path, table); + _excelExporter.Export(path, table); Assert.Equal("A1:D3", SheetHelper.GetFirstSheetDimensionRefValue(path)); - using (var stream = File.OpenRead(path)) - { - var rows = _excelImporter.Query(stream, useHeaderRow: true).ToList(); - Assert.Equal(2, rows.Count); - Assert.Equal(@"""<>+-*//}{\\n", rows[0].a); - Assert.Equal(1234567890, rows[0].b); - Assert.Null(rows[0].c); - Assert.Null(rows[0].d); - } + var rowsWithHeader = _excelImporter.Query(path, useHeaderRow: true).ToList(); + Assert.Equal(2, rowsWithHeader.Count); + Assert.Equal(@"""<>+-*//}{\\n", rowsWithHeader[0].a); + Assert.Equal(1234567890, rowsWithHeader[0].b); + Assert.Null(rowsWithHeader[0].c); + Assert.Null(rowsWithHeader[0].d); - using (var stream = File.OpenRead(path)) - { - var rows = _excelImporter.Query(stream).ToList(); - Assert.Equal(3, rows.Count); - Assert.Equal("a", rows[0].A); - Assert.Equal("b", rows[0].B); - Assert.Equal("c", rows[0].C); - Assert.Equal("d", rows[0].D); - } + var rowsNoHeader = _excelImporter.Query(path).ToList(); + Assert.Equal(3, rowsNoHeader.Count); + Assert.Equal("a", rowsNoHeader[0].A); + Assert.Equal("b", rowsNoHeader[0].B); + Assert.Equal("c", rowsNoHeader[0].C); + Assert.Equal("d", rowsNoHeader[0].D); _excelExporter.Export(path, table, printHeader: false, overwriteFile: true); Assert.Equal("A1:D2", SheetHelper.GetFirstSheetDimensionRefValue(path)); @@ -717,17 +714,17 @@ public void SaveAsByDataTableTest() using var p = new ExcelPackage(new FileInfo(path)); var ws = p.Workbook.Worksheets.First(); - Assert.True(ws.Cells["A1"].Value.ToString() == "a"); - Assert.True(ws.Cells["B1"].Value.ToString() == "b"); - Assert.True(ws.Cells["C1"].Value.ToString() == "c"); - Assert.True(ws.Cells["D1"].Value.ToString() == "d"); + Assert.Equal("a", ws.Cells["A1"].Value.ToString()); + Assert.Equal("b", ws.Cells["B1"].Value.ToString()); + Assert.Equal("c", ws.Cells["C1"].Value.ToString()); + Assert.Equal("d", ws.Cells["D1"].Value.ToString()); - Assert.True(ws.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cells["B2"].Value.ToString() == "1234567890"); + Assert.Equal(@"""<>+-*//}{\\n", ws.Cells["A2"].Value.ToString()); + Assert.Equal("1234567890", ws.Cells["B2"].Value.ToString()); Assert.True(ws.Cells["C2"].Value.ToString() == true.ToString()); Assert.True(ws.Cells["D2"].Value.ToString() == now.ToString()); - Assert.True(ws.Name == "R&D"); + Assert.Equal("R&D", ws.Name); } { using var path = AutoDeletingPath.Create(); @@ -1169,13 +1166,13 @@ public void SaveAsFileEpplusCanReadTest() using var p = new ExcelPackage(new FileInfo(path.ToString())); var ws = p.Workbook.Worksheets.First(); - Assert.True(ws.Cells["A1"].Value.ToString() == "a"); - Assert.True(ws.Cells["B1"].Value.ToString() == "b"); - Assert.True(ws.Cells["C1"].Value.ToString() == "c"); - Assert.True(ws.Cells["D1"].Value.ToString() == "d"); + Assert.Equal("a", ws.Cells["A1"].Value.ToString()); + Assert.Equal("b", ws.Cells["B1"].Value.ToString()); + Assert.Equal("c", ws.Cells["C1"].Value.ToString()); + Assert.Equal("d", ws.Cells["D1"].Value.ToString()); - Assert.True(ws.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cells["B2"].Value.ToString() == "1234567890"); + Assert.Equal(@"""<>+-*//}{\\n", ws.Cells["A2"].Value.ToString()); + Assert.Equal("1234567890", ws.Cells["B2"].Value.ToString()); Assert.True(ws.Cells["C2"].Value.ToString() == true.ToString()); Assert.True(ws.Cells["D2"].Value.ToString() == now.ToString()); } @@ -1197,17 +1194,17 @@ public void SavaAsClosedXmlCanReadTest() using var workbook = new XLWorkbook(path.ToString()); var ws = workbook.Worksheets.First(); - Assert.True(ws.Cell("A1").Value.ToString() == "a"); - Assert.True(ws.Cell("D1").Value.ToString() == "d"); - Assert.True(ws.Cell("B1").Value.ToString() == "b"); - Assert.True(ws.Cell("C1").Value.ToString() == "c"); + Assert.Equal("a", ws.Cell("A1").Value.ToString()); + Assert.Equal("d", ws.Cell("D1").Value.ToString()); + Assert.Equal("b", ws.Cell("B1").Value.ToString()); + Assert.Equal("c", ws.Cell("C1").Value.ToString()); - Assert.True(ws.Cell("A2").Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(ws.Cell("B2").Value.ToString() == "1234567890"); + Assert.Equal(@"""<>+-*//}{\\n", ws.Cell("A2").Value.ToString()); + Assert.Equal("1234567890", ws.Cell("B2").Value.ToString()); Assert.Equal(bool.TrueString, ws.Cell("C2").Value.ToString(), ignoreCase: true); Assert.True(ws.Cell("D2").Value.ToString() == now.ToString()); - Assert.True(ws.Name == "R&D"); + Assert.Equal("R&D", ws.Name); } [Fact] @@ -1228,11 +1225,11 @@ public void ContentTypeUriContentTypeReadCheckTest() .Select(s => new { s.CompressionOption, s.ContentType, s.Uri, s.Package.GetType().Name }) .ToDictionary(s => s.Uri.ToString(), s => s); - Assert.True(allParts["/xl/styles.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"); - Assert.True(allParts["/xl/workbook.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"); - Assert.True(allParts["/xl/worksheets/sheet1.xml"].ContentType == "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"); - Assert.True(allParts["/xl/_rels/workbook.xml.rels"].ContentType == "application/vnd.openxmlformats-package.relationships+xml"); - Assert.True(allParts["/_rels/.rels"].ContentType == "application/vnd.openxmlformats-package.relationships+xml"); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml", allParts["/xl/styles.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml", allParts["/xl/workbook.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml", allParts["/xl/worksheets/sheet1.xml"].ContentType); + Assert.Equal("application/vnd.openxmlformats-package.relationships+xml", allParts["/xl/_rels/workbook.xml.rels"].ContentType); + Assert.Equal("application/vnd.openxmlformats-package.relationships+xml", allParts["/_rels/.rels"].ContentType); } [Fact] @@ -1428,23 +1425,23 @@ public void InsertSheetTest() table.Rows.Add(@"""<>+-*//}{\\n", 1234567890, true, now); table.Rows.Add("Hello World", -1234567890, false, now.Date); - var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet1"); + var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet1"); Assert.Equal(2, rowsWritten); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet1 = p.Workbook.Worksheets[0]; - Assert.True(sheet1.Cells["A1"].Value.ToString() == "a"); - Assert.True(sheet1.Cells["B1"].Value.ToString() == "b"); - Assert.True(sheet1.Cells["C1"].Value.ToString() == "c"); - Assert.True(sheet1.Cells["D1"].Value.ToString() == "d"); + Assert.Equal("a", sheet1.Cells["A1"].Value.ToString()); + Assert.Equal("b", sheet1.Cells["B1"].Value.ToString()); + Assert.Equal("c", sheet1.Cells["C1"].Value.ToString()); + Assert.Equal("d", sheet1.Cells["D1"].Value.ToString()); - Assert.True(sheet1.Cells["A2"].Value.ToString() == @"""<>+-*//}{\\n"); - Assert.True(sheet1.Cells["B2"].Value.ToString() == "1234567890"); + Assert.Equal(@"""<>+-*//}{\\n", sheet1.Cells["A2"].Value.ToString()); + Assert.Equal("1234567890", sheet1.Cells["B2"].Value.ToString()); Assert.True(sheet1.Cells["C2"].Value.ToString() == true.ToString()); Assert.True(sheet1.Cells["D2"].Value.ToString() == now.ToString()); - Assert.True(sheet1.Name == "Sheet1"); + Assert.Equal("Sheet1", sheet1.Name); } { var table = new DataTable(); @@ -1453,30 +1450,30 @@ public void InsertSheetTest() table.Rows.Add("MiniExcel", 1); table.Rows.Add("Github", 2); - var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet2"); + var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet2"); Assert.Equal(2, rowsWritten); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet2 = p.Workbook.Worksheets[1]; - Assert.True(sheet2.Cells["A1"].Value.ToString() == "Column1"); - Assert.True(sheet2.Cells["B1"].Value.ToString() == "Column2"); + Assert.Equal("Column1", sheet2.Cells["A1"].Value.ToString()); + Assert.Equal("Column2", sheet2.Cells["B1"].Value.ToString()); - Assert.True(sheet2.Cells["A2"].Value.ToString() == "MiniExcel"); - Assert.True(sheet2.Cells["B2"].Value.ToString() == "1"); + Assert.Equal("MiniExcel", sheet2.Cells["A2"].Value.ToString()); + Assert.Equal("1", sheet2.Cells["B2"].Value.ToString()); - Assert.True(sheet2.Cells["A3"].Value.ToString() == "Github"); - Assert.True(sheet2.Cells["B3"].Value.ToString() == "2"); + Assert.Equal("Github", sheet2.Cells["A3"].Value.ToString()); + Assert.Equal("2", sheet2.Cells["B3"].Value.ToString()); - Assert.True(sheet2.Name == "Sheet2"); + Assert.Equal("Sheet2", sheet2.Name); } { var table = new DataTable(); table.Columns.Add("Column1", typeof(string)); table.Columns.Add("Column2", typeof(DateTime)); table.Rows.Add("Test", now); - - var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet2", printHeader: false, configuration: new OpenXmlConfiguration + + var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet2", printHeader: false, configuration: new OpenXmlConfiguration { FastMode = true, AutoFilter = false, @@ -1492,15 +1489,15 @@ public void InsertSheetTest() } ] }, overwriteSheet: true); - + Assert.Equal(1, rowsWritten); - - using var p = new ExcelPackage(new FileInfo(path)); + + using var p = new ExcelPackage(path); var sheet2 = p.Workbook.Worksheets[1]; - - Assert.True(sheet2.Cells["A1"].Value.ToString() == "Test"); + + Assert.Equal("Test", sheet2.Cells["A1"].Value.ToString()); Assert.True(sheet2.Cells["B1"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); - Assert.True(sheet2.Name == "Sheet2"); + Assert.Equal("Sheet2", sheet2.Name); } { var table = new DataTable(); @@ -1509,7 +1506,7 @@ public void InsertSheetTest() table.Rows.Add("MiniExcel", now); table.Rows.Add("Github", now); - var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet3", configuration: new OpenXmlConfiguration + var rowsWritten = _excelExporter.InsertSheet(path, table, sheetName: "Sheet3", configuration: new OpenXmlConfiguration { FastMode = true, AutoFilter = false, @@ -1527,19 +1524,19 @@ public void InsertSheetTest() }); Assert.Equal(2, rowsWritten); - using var p = new ExcelPackage(new FileInfo(path)); + using var p = new ExcelPackage(path); var sheet3 = p.Workbook.Worksheets[2]; - Assert.True(sheet3.Cells["A1"].Value.ToString() == "Column1"); - Assert.True(sheet3.Cells["B1"].Value.ToString() == "Its Date"); + Assert.Equal("Column1", sheet3.Cells["A1"].Value.ToString()); + Assert.Equal("Its Date", sheet3.Cells["B1"].Value.ToString()); - Assert.True(sheet3.Cells["A2"].Value.ToString() == "MiniExcel"); + Assert.Equal("MiniExcel", sheet3.Cells["A2"].Value.ToString()); Assert.True(sheet3.Cells["B2"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); - Assert.True(sheet3.Cells["A3"].Value.ToString() == "Github"); + Assert.Equal("Github", sheet3.Cells["A3"].Value.ToString()); Assert.True(sheet3.Cells["B3"].Text == now.ToString("dd.MM.yyyy HH:mm:ss")); - Assert.True(sheet3.Name == "Sheet3"); + Assert.Equal("Sheet3", sheet3.Name); } } @@ -1712,10 +1709,8 @@ public void ExportAndQueryFieldsWithoutAttributeTest() _excelExporter.Export(path.ToString(), input); - var rows = _excelImporter.Query(path.ToString(), true).ToList(); - var first = rows[0] as IDictionary; - - Assert.Contains("Mapped", first?.Keys); - Assert.DoesNotContain("NotMappedField", first?.Keys); + var rows = _excelImporter.Query(path.ToString(), true).Cast>().ToList(); + Assert.Contains("Mapped", rows[0].Keys); + Assert.DoesNotContain("NotMappedField", rows[0].Keys); } -} \ No newline at end of file +} diff --git a/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs b/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs deleted file mode 100644 index e61a8f0d..00000000 --- a/tests/MiniExcel.OpenXml.Tests/Utils/EpplusLicense.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MiniExcelLib.OpenXml.Tests.Utils; - -internal static class EpplusLicence -{ - internal static void SetContext() - => ExcelPackage.LicenseContext = LicenseContext.NonCommercial; -} \ No newline at end of file diff --git a/tests/data/xlsx/TestIssue772.xlsx b/tests/data/xlsx/TestIssue772.xlsx index 9907c648..2b7961a1 100644 Binary files a/tests/data/xlsx/TestIssue772.xlsx and b/tests/data/xlsx/TestIssue772.xlsx differ