From dbc259528827a4518570f01c39c8a2390d69fafe Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 4 Oct 2025 20:17:29 -0400 Subject: [PATCH 1/5] expose IProgress for exports --- src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs | 2 +- src/MiniExcel.Core/Api/OpenXmlExporter.cs | 6 +++--- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 12 +++++++----- src/MiniExcel.Csv/Api/CsvExporter.cs | 8 ++++---- src/MiniExcel.Csv/CsvWriter.cs | 8 +++++--- src/MiniExcel/MiniExcel.cs | 12 ++++++------ 6 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs index b80f88dd..b165333e 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs @@ -3,7 +3,7 @@ public partial interface IMiniExcelWriter { [CreateSyncVersion] - Task SaveAsAsync(CancellationToken cancellationToken = default); + Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null); [CreateSyncVersion] Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default); diff --git a/src/MiniExcel.Core/Api/OpenXmlExporter.cs b/src/MiniExcel.Core/Api/OpenXmlExporter.cs index 409a96c7..c492e449 100644 --- a/src/MiniExcel.Core/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.Core/Api/OpenXmlExporter.cs @@ -40,7 +40,7 @@ public async Task InsertSheetAsync(Stream stream, object value, string? she [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, string? sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + CancellationToken cancellationToken = default, IProgress? progress = null) { if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase)) throw new NotSupportedException("MiniExcel's ExportExcel does not support the .xlsm format"); @@ -53,12 +53,12 @@ public async Task ExportAsync(string path, object value, bool printHeader [CreateSyncVersion] public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string? sheetName = "Sheet1", - OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) { var writer = await OpenXmlWriter .CreateAsync(stream, value, sheetName, printHeader, configuration, cancellationToken) .ConfigureAwait(false); - return await writer.SaveAsAsync(cancellationToken).ConfigureAwait(false); + return await writer.SaveAsAsync(cancellationToken, progress).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index c1ee9b8f..20e06677 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -52,7 +52,7 @@ internal static Task CreateAsync(Stream stream, object? value, st } [CreateSyncVersion] - public async Task SaveAsAsync(CancellationToken cancellationToken = default) + public async Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null) { try { @@ -69,7 +69,7 @@ public async Task SaveAsAsync(CancellationToken cancellationToken = defau _sheets.Add(sheet.Item1); //TODO:remove _currentSheetIndex = sheet.Item1.SheetIdx; - var rows = await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, cancellationToken).ConfigureAwait(false); + var rows = await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, cancellationToken, progress).ConfigureAwait(false); rowsWritten.Add(rows); } @@ -177,7 +177,7 @@ internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationTo } [CreateSyncVersion] - private async Task CreateSheetXmlAsync(object? values, string sheetPath, CancellationToken cancellationToken) + private async Task CreateSheetXmlAsync(object? values, string sheetPath, CancellationToken cancellationToken, IProgress? progress = null) { cancellationToken.ThrowIfCancellationRequested(); @@ -197,7 +197,7 @@ private async Task CreateSheetXmlAsync(object? values, string sheetPath, Ca } else { - rowsWritten = await WriteValuesAsync(writer, values, cancellationToken).ConfigureAwait(false); + rowsWritten = await WriteValuesAsync(writer, values, cancellationToken, progress).ConfigureAwait(false); } _zipDictionary.Add(sheetPath, new ZipPackageInfo(entry, ExcelContentTypes.Worksheet)); @@ -232,7 +232,7 @@ private static async Task WriteDimensionAsync(SafeStreamWriter writer, int maxRo } [CreateSyncVersion] - private async Task WriteValuesAsync(SafeStreamWriter writer, object values, CancellationToken cancellationToken) + private async Task WriteValuesAsync(SafeStreamWriter writer, object values, CancellationToken cancellationToken, IProgress? progress = null) { cancellationToken.ThrowIfCancellationRequested(); @@ -313,6 +313,7 @@ private async Task WriteValuesAsync(SafeStreamWriter writer, object values, { cancellationToken.ThrowIfCancellationRequested(); await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); } @@ -328,6 +329,7 @@ private async Task WriteValuesAsync(SafeStreamWriter writer, object values, await foreach (var cellValue in row.ConfigureAwait(false).WithCancellation(cancellationToken)) { await WriteCellAsync(writer, currentRowIndex, cellValue.CellIndex, cellValue.Value, cellValue.Prop, widths).ConfigureAwait(false); + progress?.Report(1); } await writer.WriteAsync(WorksheetXml.EndRow, cancellationToken).ConfigureAwait(false); } diff --git a/src/MiniExcel.Csv/Api/CsvExporter.cs b/src/MiniExcel.Csv/Api/CsvExporter.cs index c4c71702..de471363 100644 --- a/src/MiniExcel.Csv/Api/CsvExporter.cs +++ b/src/MiniExcel.Csv/Api/CsvExporter.cs @@ -37,18 +37,18 @@ public async Task AppendAsync(Stream stream, object value, CsvConfiguration [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) { using var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew); - return await ExportAsync(stream, value, printHeader, configuration, cancellationToken).ConfigureAwait(false); + return await ExportAsync(stream, value, printHeader, configuration, cancellationToken, progress).ConfigureAwait(false); } [CreateSyncVersion] public async Task ExportAsync(Stream stream, object value, bool printHeader = true, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) { using var writer = new CsvWriter(stream, value, printHeader, configuration); - return await writer.SaveAsAsync(cancellationToken).ConfigureAwait(false); + return await writer.SaveAsAsync(cancellationToken, progress).ConfigureAwait(false); } #endregion diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index bd66449b..330110bd 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -37,7 +37,7 @@ private static void RemoveTrailingSeparator(StringBuilder rowBuilder) } [CreateSyncVersion] - private async Task WriteValuesAsync(StreamWriter writer, object values, string separator, string newLine, CancellationToken cancellationToken = default) + private async Task WriteValuesAsync(StreamWriter writer, object values, string separator, string newLine, CancellationToken cancellationToken = default, IProgress? progress = null) { cancellationToken.ThrowIfCancellationRequested(); @@ -96,6 +96,7 @@ await _writer.WriteAsync(newLine { cancellationToken.ThrowIfCancellationRequested(); AppendColumn(rowBuilder, column); + progress?.Report(1); } RemoveTrailingSeparator(rowBuilder); @@ -124,6 +125,7 @@ await _writer.WriteAsync(newLine await foreach (var column in row.WithCancellation(cancellationToken).ConfigureAwait(false)) { AppendColumn(rowBuilder, column); + progress?.Report(1); } RemoveTrailingSeparator(rowBuilder); @@ -146,7 +148,7 @@ await _writer.WriteAsync(newLine } [CreateSyncVersion] - public async Task SaveAsAsync(CancellationToken cancellationToken = default) + public async Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null) { cancellationToken.ThrowIfCancellationRequested(); @@ -168,7 +170,7 @@ await _writer.FlushAsync( return []; } - var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken).ConfigureAwait(false); + var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken, progress).ConfigureAwait(false); await _writer.FlushAsync( #if NET5_0_OR_GREATER cancellationToken diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index ae2aea8e..743006b5 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -79,25 +79,25 @@ public static async Task InsertAsync(this Stream stream, object value, stri } [CreateSyncVersion] - public static async Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration? configuration = null, bool overwriteFile = false, CancellationToken cancellationToken = default) + public static async Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration? configuration = null, bool overwriteFile = false, CancellationToken cancellationToken = default, IProgress? progress = null) { var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, printHeader, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, printHeader, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, cancellationToken, progress).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration? configuration = null, CancellationToken cancellationToken = default) + public static async Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) { var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, cancellationToken, progress).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; } From 5afaab0df7d503ff220812642b18b62ec58e4836 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 4 Oct 2025 20:19:04 -0400 Subject: [PATCH 2/5] add tests --- .../MiniExcelWriterTests.cs | 55 +++++++++++++++ .../MiniExcelWriterTests.cs | 70 +++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs create mode 100644 tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs diff --git a/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs b/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs new file mode 100644 index 00000000..939eadd8 --- /dev/null +++ b/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs @@ -0,0 +1,55 @@ +namespace MiniExcelLib.Tests; + +public class MiniExcelWriterTests +{ + private readonly MiniExcelExporterProvider _excelExporterProvider = new(); + private readonly MiniExcelImporterProvider _excelImporterProvider = new(); + + [Fact] + public async Task ExportDataTableWithProgressTest() + { + var dataTable = new DataTable(); + dataTable.Columns.Add("Id", typeof(int)); + dataTable.Columns.Add("Name", typeof(string)); + dataTable.Columns.Add("Date", typeof(DateTime)); + dataTable.Rows.Add(1, "Alice", DateTime.Now); + dataTable.Rows.Add(2, DBNull.Value, DateTime.UtcNow); + dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); + + var progress = new SimpleProgress(); + using var ms = new MemoryStream(); + var exporter = _excelExporterProvider.GetOpenXmlExporter(); + var rowCounts = await exporter.ExportAsync(ms, dataTable, progress: progress); + Assert.Single(rowCounts); + Assert.Equal(3, rowCounts.First()); + + //Confirm the progress report is correct + var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; + Assert.Equal(cellCount, progress.Value); + + ms.Seek(0, SeekOrigin.Begin); + var importer = _excelImporterProvider.GetOpenXmlImporter(); + var resultDataTable = importer.QueryAsDataTable(ms); + + //Confirm the data is correct + Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); + Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); + for (var i = 0; i < dataTable.Rows.Count; i++) + { + for (var j = 0; j < dataTable.Columns.Count; j++) + { + //We compare string values because types change after writing and reading them back (e.g. int becomes double) + Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); + } + } + } + + private class SimpleProgress: IProgress + { + public int Value { get; private set; } + public void Report(int value) + { + Value += value; + } + } +} diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs new file mode 100644 index 00000000..5f830895 --- /dev/null +++ b/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs @@ -0,0 +1,70 @@ +namespace MiniExcelLib.Tests; + +public class MiniExcelWriterTests +{ + [Fact] + public async Task ExportDataTableWithProgressTest() + { + var dataTable = new DataTable(); + dataTable.Columns.Add("Id", typeof(int)); + dataTable.Columns.Add("Name", typeof(string)); + dataTable.Columns.Add("Date", typeof(DateTime)); + dataTable.Rows.Add(1, "Alice", new DateTime(1900, 1, 1, 1, 0, 0)); + dataTable.Rows.Add(2, DBNull.Value, new DateTime(1901, 2, 2, 2, 0, 0)); + dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); + + // We need to use the file system because the CsvExporter automatically disposes the stream + var tempFilePath = Path.GetTempFileName(); + + using (var fs = File.Create(tempFilePath)) + { + + var progress = new SimpleProgress(); + var exporter = new CsvExporter(); + var rowCounts = await exporter.ExportAsync(fs, dataTable, progress: progress); + Assert.Single(rowCounts); + Assert.Equal(3, rowCounts.First()); + + //Confirm the progress report is correct + var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; + Assert.Equal(cellCount, progress.Value); + } + + using (var fs = File.OpenRead(tempFilePath)) + { + var importer = new CsvImporter(); + var resultDataTable = importer.QueryAsDataTable(fs); + + //Confirm the data is correct + Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); + Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); + for (var i = 0; i < dataTable.Rows.Count; i++) + { + for (var j = 0; j < dataTable.Columns.Count; j++) + { + if (dataTable.Columns[j].DataType == typeof(DateTime)) + { + //We need to compare Dates properly as they will be formatted differently in CSV + //Note: if dates have millisecond precision that will be lost when saving to CSV + DateTime.TryParse(resultDataTable.Rows[i][j].ToString(), out var resultDate); + Assert.Equal((DateTime)dataTable.Rows[i][j], resultDate); + } + else + { + //We compare string values because types change after writing and reading them back + Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); + } + } + } + } + } + + private class SimpleProgress: IProgress + { + public int Value { get; private set; } + public void Report(int value) + { + Value += value; + } + } +} From db64540c7b060340dedb41b51eefbf0d53be38c5 Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 4 Oct 2025 20:48:49 -0400 Subject: [PATCH 3/5] address ai code review --- src/MiniExcel.Core/Api/OpenXmlExporter.cs | 2 +- src/MiniExcel/MiniExcel.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/MiniExcel.Core/Api/OpenXmlExporter.cs b/src/MiniExcel.Core/Api/OpenXmlExporter.cs index c492e449..4d1973a8 100644 --- a/src/MiniExcel.Core/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.Core/Api/OpenXmlExporter.cs @@ -48,7 +48,7 @@ public async Task ExportAsync(string path, object value, bool printHeader var filePath = path.EndsWith(".xlsx", StringComparison.InvariantCultureIgnoreCase) ? path : $"{path}.xlsx" ; using var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew); - return await ExportAsync(stream, value, printHeader, sheetName, configuration, cancellationToken).ConfigureAwait(false); + return await ExportAsync(stream, value, printHeader, sheetName, configuration, cancellationToken, progress).ConfigureAwait(false); } [CreateSyncVersion] diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index 743006b5..3201c110 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -84,7 +84,7 @@ public static async Task SaveAsAsync(string path, object value, bool prin var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, printHeader, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), + ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, cancellationToken, progress).ConfigureAwait(false), _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") }; From 0878c69ed6ddf7034b4331910d780d785cacdebb Mon Sep 17 00:00:00 2001 From: Sal Date: Sat, 4 Oct 2025 20:53:49 -0400 Subject: [PATCH 4/5] simplify csv test --- .../MiniExcelWriterTests.cs | 60 +++++++++---------- 1 file changed, 27 insertions(+), 33 deletions(-) diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs index 5f830895..c65f39c9 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs @@ -15,51 +15,45 @@ public async Task ExportDataTableWithProgressTest() // We need to use the file system because the CsvExporter automatically disposes the stream var tempFilePath = Path.GetTempFileName(); + File.Delete(tempFilePath); - using (var fs = File.Create(tempFilePath)) - { + var progress = new SimpleProgress(); + var exporter = new CsvExporter(); + var rowCounts = await exporter.ExportAsync(tempFilePath, dataTable, progress: progress); + Assert.Single(rowCounts); + Assert.Equal(3, rowCounts.First()); - var progress = new SimpleProgress(); - var exporter = new CsvExporter(); - var rowCounts = await exporter.ExportAsync(fs, dataTable, progress: progress); - Assert.Single(rowCounts); - Assert.Equal(3, rowCounts.First()); + //Confirm the progress report is correct + var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; + Assert.Equal(cellCount, progress.Value); - //Confirm the progress report is correct - var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; - Assert.Equal(cellCount, progress.Value); - } + var importer = new CsvImporter(); + var resultDataTable = importer.QueryAsDataTable(tempFilePath); - using (var fs = File.OpenRead(tempFilePath)) + //Confirm the data is correct + Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); + Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); + for (var i = 0; i < dataTable.Rows.Count; i++) { - var importer = new CsvImporter(); - var resultDataTable = importer.QueryAsDataTable(fs); - - //Confirm the data is correct - Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); - Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); - for (var i = 0; i < dataTable.Rows.Count; i++) + for (var j = 0; j < dataTable.Columns.Count; j++) { - for (var j = 0; j < dataTable.Columns.Count; j++) + if (dataTable.Columns[j].DataType == typeof(DateTime)) + { + //We need to compare Dates properly as they will be formatted differently in CSV + //Note: if dates have millisecond precision that will be lost when saving to CSV + DateTime.TryParse(resultDataTable.Rows[i][j].ToString(), out var resultDate); + Assert.Equal((DateTime)dataTable.Rows[i][j], resultDate); + } + else { - if (dataTable.Columns[j].DataType == typeof(DateTime)) - { - //We need to compare Dates properly as they will be formatted differently in CSV - //Note: if dates have millisecond precision that will be lost when saving to CSV - DateTime.TryParse(resultDataTable.Rows[i][j].ToString(), out var resultDate); - Assert.Equal((DateTime)dataTable.Rows[i][j], resultDate); - } - else - { - //We compare string values because types change after writing and reading them back - Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); - } + //We compare string values because types change after writing and reading them back + Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); } } } } - private class SimpleProgress: IProgress + private class SimpleProgress : IProgress { public int Value { get; private set; } public void Report(int value) From c9cdae66d9db3becd6eb3f81ee9618546f0bae9e Mon Sep 17 00:00:00 2001 From: Michele Bastione Date: Sun, 12 Oct 2025 23:11:31 +0200 Subject: [PATCH 5/5] Various adjustments - Added IProgress parameter to OpenXmlExporter.InsertSheet and CsvExporter.Append methods - Reworked parameters order in the changed methods - Moved new tests to OpenXmlAsyncTests and CsvAsyncTests classes and removed new superfluous files --- .../Abstractions/IMiniExcelWriter.cs | 4 +- src/MiniExcel.Core/Api/OpenXmlExporter.cs | 22 ++++--- src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs | 12 ++-- src/MiniExcel.Csv/Api/CsvExporter.cs | 22 +++---- src/MiniExcel.Csv/CsvWriter.cs | 11 ++-- src/MiniExcel/MiniExcel.cs | 62 ++++++++++-------- .../MiniExcelOpenXmlAsyncTests.cs | 37 +++++++++++ .../MiniExcelWriterTests.cs | 55 ---------------- .../MiniExcelCsvAsycTests.cs | 49 ++++++++++++++ .../MiniExcelWriterTests.cs | 64 ------------------- .../Utils/SimpleProgress.cs | 10 +++ 11 files changed, 168 insertions(+), 180 deletions(-) delete mode 100644 tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs delete mode 100644 tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs create mode 100644 tests/MiniExcel.Tests.Common/Utils/SimpleProgress.cs diff --git a/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs b/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs index b165333e..a900b26f 100644 --- a/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs +++ b/src/MiniExcel.Core/Abstractions/IMiniExcelWriter.cs @@ -3,8 +3,8 @@ public partial interface IMiniExcelWriter { [CreateSyncVersion] - Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null); + Task SaveAsAsync(IProgress? progress = null, CancellationToken cancellationToken = default); [CreateSyncVersion] - Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default); + Task InsertAsync(bool overwriteSheet = false, IProgress? progress = null, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/MiniExcel.Core/Api/OpenXmlExporter.cs b/src/MiniExcel.Core/Api/OpenXmlExporter.cs index 4d1973a8..84d34185 100644 --- a/src/MiniExcel.Core/Api/OpenXmlExporter.cs +++ b/src/MiniExcel.Core/Api/OpenXmlExporter.cs @@ -7,10 +7,12 @@ internal OpenXmlExporter() { } [CreateSyncVersion] - public async Task InsertSheetAsync(string path, object value, string? sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default) + public async Task InsertSheetAsync(string path, object value, string? sheetName = "Sheet1", + bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, + IProgress? progress = null, CancellationToken cancellationToken = default) { if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase)) - throw new NotSupportedException("MiniExcel's InsertExcelSheet does not support the .xlsm format"); + throw new NotSupportedException("MiniExcel's InsertSheet does not support the .xlsm format"); if (!File.Exists(path)) { @@ -19,13 +21,13 @@ public async Task InsertSheetAsync(string path, object value, string? sheet } using var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.SequentialScan); - return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, cancellationToken).ConfigureAwait(false); + return await InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration, progress, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] public async Task InsertSheetAsync(Stream stream, object value, string? sheetName = "Sheet1", bool printHeader = true, bool overwriteSheet = false, OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default) + IProgress? progress = null, CancellationToken cancellationToken = default) { stream.Seek(0, SeekOrigin.End); configuration ??= new OpenXmlConfiguration { FastMode = true }; @@ -34,31 +36,31 @@ public async Task InsertSheetAsync(Stream stream, object value, string? she .CreateAsync(stream, value, sheetName, printHeader, configuration, cancellationToken) .ConfigureAwait(false); - return await writer.InsertAsync(overwriteSheet, cancellationToken).ConfigureAwait(false); + return await writer.InsertAsync(overwriteSheet, cancellationToken: cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] public async Task ExportAsync(string path, object value, bool printHeader = true, string? sheetName = "Sheet1", bool overwriteFile = false, OpenXmlConfiguration? configuration = null, - CancellationToken cancellationToken = default, IProgress? progress = null) + IProgress? progress = null, CancellationToken cancellationToken = default) { if (Path.GetExtension(path).Equals(".xlsm", StringComparison.InvariantCultureIgnoreCase)) - throw new NotSupportedException("MiniExcel's ExportExcel does not support the .xlsm format"); + throw new NotSupportedException("MiniExcel's Export does not support the .xlsm format"); var filePath = path.EndsWith(".xlsx", StringComparison.InvariantCultureIgnoreCase) ? path : $"{path}.xlsx" ; using var stream = overwriteFile ? File.Create(filePath) : new FileStream(filePath, FileMode.CreateNew); - return await ExportAsync(stream, value, printHeader, sheetName, configuration, cancellationToken, progress).ConfigureAwait(false); + return await ExportAsync(stream, value, printHeader, sheetName, configuration, progress, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] public async Task ExportAsync(Stream stream, object value, bool printHeader = true, string? sheetName = "Sheet1", - OpenXmlConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) + OpenXmlConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { var writer = await OpenXmlWriter .CreateAsync(stream, value, sheetName, printHeader, configuration, cancellationToken) .ConfigureAwait(false); - return await writer.SaveAsAsync(cancellationToken, progress).ConfigureAwait(false); + return await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); } } \ No newline at end of file diff --git a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs index 20e06677..f5b562e3 100644 --- a/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs +++ b/src/MiniExcel.Core/OpenXml/OpenXmlWriter.cs @@ -52,7 +52,7 @@ internal static Task CreateAsync(Stream stream, object? value, st } [CreateSyncVersion] - public async Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null) + public async Task SaveAsAsync(IProgress? progress = null, CancellationToken cancellationToken = default) { try { @@ -69,7 +69,7 @@ public async Task SaveAsAsync(CancellationToken cancellationToken = defau _sheets.Add(sheet.Item1); //TODO:remove _currentSheetIndex = sheet.Item1.SheetIdx; - var rows = await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, cancellationToken, progress).ConfigureAwait(false); + var rows = await CreateSheetXmlAsync(sheet.Item2, sheet.Item1.Path, progress, cancellationToken).ConfigureAwait(false); rowsWritten.Add(rows); } @@ -87,7 +87,7 @@ public async Task SaveAsAsync(CancellationToken cancellationToken = defau } [CreateSyncVersion] - public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default) + public async Task InsertAsync(bool overwriteSheet = false, IProgress? progress = null, CancellationToken cancellationToken = default) { try { @@ -123,13 +123,13 @@ public async Task InsertAsync(bool overwriteSheet = false, CancellationToke var insertSheetInfo = GetSheetInfos(_defaultSheetName); var insertSheetDto = insertSheetInfo.ToDto(_currentSheetIndex); _sheets.Add(insertSheetDto); - rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, cancellationToken).ConfigureAwait(false); + rowsWritten = await CreateSheetXmlAsync(_value, insertSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); } else { _currentSheetIndex = existSheetDto.SheetIdx; _archive.Entries.Single(s => s.FullName == existSheetDto.Path).Delete(); - rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, cancellationToken).ConfigureAwait(false); + rowsWritten = await CreateSheetXmlAsync(_value, existSheetDto.Path, progress, cancellationToken).ConfigureAwait(false); } await AddFilesToZipAsync(cancellationToken).ConfigureAwait(false); @@ -177,7 +177,7 @@ internal async Task GenerateDefaultOpenXmlAsync(CancellationToken cancellationTo } [CreateSyncVersion] - private async Task CreateSheetXmlAsync(object? values, string sheetPath, CancellationToken cancellationToken, IProgress? progress = null) + private async Task CreateSheetXmlAsync(object? values, string sheetPath, IProgress? progress, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/MiniExcel.Csv/Api/CsvExporter.cs b/src/MiniExcel.Csv/Api/CsvExporter.cs index de471363..7ece7412 100644 --- a/src/MiniExcel.Csv/Api/CsvExporter.cs +++ b/src/MiniExcel.Csv/Api/CsvExporter.cs @@ -12,43 +12,43 @@ internal CsvExporter() { } [CreateSyncVersion] public async Task AppendAsync(string path, object value, bool printHeader = true, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { if (!File.Exists(path)) { - var rowsWritten = await ExportAsync(path, value, printHeader, false, configuration, cancellationToken: cancellationToken).ConfigureAwait(false); + var rowsWritten = await ExportAsync(path, value, printHeader, false, configuration, progress, cancellationToken).ConfigureAwait(false); return rowsWritten.FirstOrDefault(); } using var stream = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read, 4096, FileOptions.SequentialScan); - return await AppendAsync(stream, value, configuration, cancellationToken).ConfigureAwait(false); + return await AppendAsync(stream, value, configuration, progress, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task AppendAsync(Stream stream, object value, CsvConfiguration? configuration = null, CancellationToken cancellationToken = default) + public async Task AppendAsync(Stream stream, object value, CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { stream.Seek(0, SeekOrigin.End); var newValue = value is IEnumerable or IDataReader ? value : new[] { value }; using var writer = new CsvWriter(stream, newValue, false, configuration); - return await writer.InsertAsync(false, cancellationToken).ConfigureAwait(false); + return await writer.InsertAsync(false, progress, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) + public async Task ExportAsync(string path, object value, bool printHeader = true, bool overwriteFile = false, + CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { using var stream = overwriteFile ? File.Create(path) : new FileStream(path, FileMode.CreateNew); - return await ExportAsync(stream, value, printHeader, configuration, cancellationToken, progress).ConfigureAwait(false); + return await ExportAsync(stream, value, printHeader, configuration, progress, cancellationToken).ConfigureAwait(false); } [CreateSyncVersion] - public async Task ExportAsync(Stream stream, object value, bool printHeader = true, - CsvConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) + public async Task ExportAsync(Stream stream, object value, bool printHeader = true, + CsvConfiguration? configuration = null, IProgress? progress = null, CancellationToken cancellationToken = default) { using var writer = new CsvWriter(stream, value, printHeader, configuration); - return await writer.SaveAsAsync(cancellationToken, progress).ConfigureAwait(false); + return await writer.SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); } #endregion diff --git a/src/MiniExcel.Csv/CsvWriter.cs b/src/MiniExcel.Csv/CsvWriter.cs index 330110bd..49d5abe6 100644 --- a/src/MiniExcel.Csv/CsvWriter.cs +++ b/src/MiniExcel.Csv/CsvWriter.cs @@ -37,7 +37,8 @@ private static void RemoveTrailingSeparator(StringBuilder rowBuilder) } [CreateSyncVersion] - private async Task WriteValuesAsync(StreamWriter writer, object values, string separator, string newLine, CancellationToken cancellationToken = default, IProgress? progress = null) + private async Task WriteValuesAsync(StreamWriter writer, object values, string separator, string newLine, + IProgress? progress = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -148,7 +149,7 @@ await _writer.WriteAsync(newLine } [CreateSyncVersion] - public async Task SaveAsAsync(CancellationToken cancellationToken = default, IProgress? progress = null) + public async Task SaveAsAsync(IProgress? progress = null, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -170,7 +171,7 @@ await _writer.FlushAsync( return []; } - var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, cancellationToken, progress).ConfigureAwait(false); + var rowsWritten = await WriteValuesAsync(_writer, _value, seperator, newLine, progress, cancellationToken).ConfigureAwait(false); await _writer.FlushAsync( #if NET5_0_OR_GREATER cancellationToken @@ -181,9 +182,9 @@ await _writer.FlushAsync( } [CreateSyncVersion] - public async Task InsertAsync(bool overwriteSheet = false, CancellationToken cancellationToken = default) + public async Task InsertAsync(bool overwriteSheet = false, IProgress? progress = null, CancellationToken cancellationToken = default) { - var rowsWritten = await SaveAsAsync(cancellationToken).ConfigureAwait(false); + var rowsWritten = await SaveAsAsync(progress, cancellationToken).ConfigureAwait(false); return rowsWritten.FirstOrDefault(); } diff --git a/src/MiniExcel/MiniExcel.cs b/src/MiniExcel/MiniExcel.cs index 3201c110..ebdef08b 100644 --- a/src/MiniExcel/MiniExcel.cs +++ b/src/MiniExcel/MiniExcel.cs @@ -39,7 +39,7 @@ public static MiniExcelDataReader GetReader(string path, bool useHeaderRow = fal { ExcelType.XLSX => ExcelImporter.GetDataReader(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(path, useHeaderRow, configuration as Csv.CsvConfiguration), - _ => throw new NotSupportedException($"Excel type {type} is not a valid Excel type") + _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; } @@ -50,55 +50,63 @@ public static MiniExcelDataReader GetReader(this Stream stream, bool useHeaderRo { ExcelType.XLSX => ExcelImporter.GetDataReader(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration), ExcelType.CSV => CsvImporter.GetDataReader(stream, useHeaderRow, configuration as Csv.CsvConfiguration), - _ => throw new NotSupportedException($"Excel type {type} is not a valid Excel type") + _ => throw new NotSupportedException($"Type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task InsertAsync(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration? configuration = null, bool printHeader = true, bool overwriteSheet = false, CancellationToken cancellationToken = default) + public static async Task InsertAsync(string path, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, + IConfiguration? configuration = null, bool printHeader = true, bool overwriteSheet = false, + IProgress? progress = null, CancellationToken cancellationToken = default) { var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.AppendAsync(path, value, printHeader, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(path, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.AppendAsync(path, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task InsertAsync(this Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration? configuration = null, bool printHeader = true, bool overwriteSheet = false, CancellationToken cancellationToken = default) + public static async Task InsertAsync(this Stream stream, object value, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, + IConfiguration? configuration = null, bool printHeader = true, bool overwriteSheet = false, + IProgress? progress = null, CancellationToken cancellationToken = default) { var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.AppendAsync(stream, value, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + ExcelType.XLSX => await ExcelExporter.InsertSheetAsync(stream, value, sheetName, printHeader, overwriteSheet, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.AppendAsync(stream, value, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task SaveAsAsync(string path, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration? configuration = null, bool overwriteFile = false, CancellationToken cancellationToken = default, IProgress? progress = null) + public static async Task SaveAsAsync(string path, object value, bool printHeader = true, + string sheetName = "Sheet1", ExcelType excelType = ExcelType.UNKNOWN, IConfiguration? configuration = null, + bool overwriteFile = false, IProgress? progress = null, CancellationToken cancellationToken = default) { var type = path.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, cancellationToken, progress).ConfigureAwait(false), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + ExcelType.XLSX => await ExcelExporter.ExportAsync(path, value, printHeader, sheetName, overwriteFile, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.ExportAsync(path, value, printHeader, overwriteFile, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } [CreateSyncVersion] - public static async Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration? configuration = null, CancellationToken cancellationToken = default, IProgress? progress = null) + public static async Task SaveAsAsync(this Stream stream, object value, bool printHeader = true, + string sheetName = "Sheet1", ExcelType excelType = ExcelType.XLSX, IConfiguration? configuration = null, + IProgress? progress = null, CancellationToken cancellationToken = default) { var type = stream.GetExcelType(excelType); return type switch { - ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, cancellationToken, progress).ConfigureAwait(false), - ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, cancellationToken, progress).ConfigureAwait(false), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + ExcelType.XLSX => await ExcelExporter.ExportAsync(stream, value, printHeader, sheetName, configuration as OpenXmlConfiguration, progress, cancellationToken).ConfigureAwait(false), + ExcelType.CSV => await CsvExporter.ExportAsync(stream, value, printHeader, configuration as Csv.CsvConfiguration, progress, cancellationToken).ConfigureAwait(false), + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -110,7 +118,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo { ExcelType.XLSX => ExcelImporter.QueryAsync(path, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -122,7 +130,7 @@ public static async Task SaveAsAsync(this Stream stream, object value, bo { ExcelType.XLSX => ExcelImporter.QueryAsync(stream, sheetName, startCell, hasHeader, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, hasHeader, configuration as Csv.CsvConfiguration, cancellationToken), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -134,7 +142,7 @@ public static IAsyncEnumerable QueryAsync(string path, bool useHeaderRo { ExcelType.XLSX => ExcelImporter.QueryAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -146,7 +154,7 @@ public static IAsyncEnumerable QueryAsync(this Stream stream, bool useH { ExcelType.XLSX => ExcelImporter.QueryAsync(stream, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -167,7 +175,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea { ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -179,7 +187,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool { ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startCell, endCell, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => CsvImporter.QueryAsync(stream, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -191,7 +199,7 @@ public static IAsyncEnumerable QueryRangeAsync(string path, bool useHea { ExcelType.XLSX => ExcelImporter.QueryRangeAsync(path, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -203,7 +211,7 @@ public static IAsyncEnumerable QueryRangeAsync(this Stream stream, bool { ExcelType.XLSX => ExcelImporter.QueryRangeAsync(stream, useHeaderRow, sheetName, startRowIndex, startColumnIndex, endRowIndex, endColumnIndex, configuration as OpenXmlConfiguration, cancellationToken), ExcelType.CSV => throw new NotSupportedException("QueryRange is not supported for csv"), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } @@ -273,7 +281,7 @@ public static async Task QueryAsDataTableAsync(string path, bool useH { ExcelType.XLSX => await ExcelImporter.QueryAsDataTableAsync(path, useHeaderRow, sheetName, startCell, configuration as OpenXmlConfiguration, cancellationToken).ConfigureAwait(false), ExcelType.CSV => await CsvImporter.QueryAsDataTableAsync(path, useHeaderRow, configuration as Csv.CsvConfiguration, cancellationToken).ConfigureAwait(false), - _ => throw new InvalidDataException($"Excel type {type} is not a valid Excel type") + _ => throw new InvalidDataException($"Type {type} is not a valid Excel type") }; } diff --git a/tests/MiniExcel.Core.Tests/MiniExcelOpenXmlAsyncTests.cs b/tests/MiniExcel.Core.Tests/MiniExcelOpenXmlAsyncTests.cs index d0aef62f..bf541d1e 100644 --- a/tests/MiniExcel.Core.Tests/MiniExcelOpenXmlAsyncTests.cs +++ b/tests/MiniExcel.Core.Tests/MiniExcelOpenXmlAsyncTests.cs @@ -1559,4 +1559,41 @@ static async IAsyncEnumerable GetValues() Assert.Equal("Github", results[^1].Column1); Assert.Equal(2, results[^1].Column2); } + + [Fact] + public async Task ExportDataTableWithProgressTest() + { + var dataTable = new DataTable(); + dataTable.Columns.Add("Id", typeof(int)); + dataTable.Columns.Add("Name", typeof(string)); + dataTable.Columns.Add("Date", typeof(DateTime)); + dataTable.Rows.Add(1, "Alice", DateTime.Now); + dataTable.Rows.Add(2, DBNull.Value, DateTime.UtcNow); + dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); + + var progress = new SimpleProgress(); + using var ms = new MemoryStream(); + var rowCounts = await _excelExporter.ExportAsync(ms, dataTable, progress: progress); + Assert.Single(rowCounts); + Assert.Equal(3, rowCounts.First()); + + //Confirm the progress report is correct + var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; + Assert.Equal(cellCount, progress.Value); + + ms.Seek(0, SeekOrigin.Begin); + var resultDataTable = await _excelImporter.QueryAsDataTableAsync(ms); + + //Confirm the data is correct + Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); + Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); + for (var i = 0; i < dataTable.Rows.Count; i++) + { + for (var j = 0; j < dataTable.Columns.Count; j++) + { + //We compare string values because types change after writing and reading them back (e.g. int becomes double) + Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); + } + } + } } \ No newline at end of file diff --git a/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs b/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs deleted file mode 100644 index 939eadd8..00000000 --- a/tests/MiniExcel.Core.Tests/MiniExcelWriterTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace MiniExcelLib.Tests; - -public class MiniExcelWriterTests -{ - private readonly MiniExcelExporterProvider _excelExporterProvider = new(); - private readonly MiniExcelImporterProvider _excelImporterProvider = new(); - - [Fact] - public async Task ExportDataTableWithProgressTest() - { - var dataTable = new DataTable(); - dataTable.Columns.Add("Id", typeof(int)); - dataTable.Columns.Add("Name", typeof(string)); - dataTable.Columns.Add("Date", typeof(DateTime)); - dataTable.Rows.Add(1, "Alice", DateTime.Now); - dataTable.Rows.Add(2, DBNull.Value, DateTime.UtcNow); - dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); - - var progress = new SimpleProgress(); - using var ms = new MemoryStream(); - var exporter = _excelExporterProvider.GetOpenXmlExporter(); - var rowCounts = await exporter.ExportAsync(ms, dataTable, progress: progress); - Assert.Single(rowCounts); - Assert.Equal(3, rowCounts.First()); - - //Confirm the progress report is correct - var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; - Assert.Equal(cellCount, progress.Value); - - ms.Seek(0, SeekOrigin.Begin); - var importer = _excelImporterProvider.GetOpenXmlImporter(); - var resultDataTable = importer.QueryAsDataTable(ms); - - //Confirm the data is correct - Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); - Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); - for (var i = 0; i < dataTable.Rows.Count; i++) - { - for (var j = 0; j < dataTable.Columns.Count; j++) - { - //We compare string values because types change after writing and reading them back (e.g. int becomes double) - Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); - } - } - } - - private class SimpleProgress: IProgress - { - public int Value { get; private set; } - public void Report(int value) - { - Value += value; - } - } -} diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsycTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsycTests.cs index 95471ece..b345233f 100644 --- a/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsycTests.cs +++ b/tests/MiniExcel.Csv.Tests/MiniExcelCsvAsycTests.cs @@ -370,4 +370,53 @@ static async IAsyncEnumerable GetValues() Assert.Equal("A2", results[1].C1); Assert.Equal("B2", results[1].C2); } + + [Fact] + public async Task ExportDataTableWithProgressTest() + { + var dataTable = new DataTable(); + dataTable.Columns.Add("Id", typeof(int)); + dataTable.Columns.Add("Name", typeof(string)); + dataTable.Columns.Add("Date", typeof(DateTime)); + dataTable.Rows.Add(1, "Alice", new DateTime(1900, 1, 1, 1, 0, 0)); + dataTable.Rows.Add(2, DBNull.Value, new DateTime(1901, 2, 2, 2, 0, 0)); + dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); + + // We need to use the file system because the CsvExporter automatically disposes the stream + var tempFilePath = Path.GetTempFileName(); + File.Delete(tempFilePath); + + var progress = new SimpleProgress(); + var rowCounts = await _csvExporter.ExportAsync(tempFilePath, dataTable, progress: progress); + Assert.Single(rowCounts); + Assert.Equal(3, rowCounts.First()); + + //Confirm the progress report is correct + var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; + Assert.Equal(cellCount, progress.Value); + + var resultDataTable = await _csvImporter.QueryAsDataTableAsync(tempFilePath); + + //Confirm the data is correct + Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); + Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); + for (var i = 0; i < dataTable.Rows.Count; i++) + { + for (var j = 0; j < dataTable.Columns.Count; j++) + { + if (dataTable.Columns[j].DataType == typeof(DateTime)) + { + //We need to compare Dates properly as they will be formatted differently in CSV + //Note: if dates have millisecond precision that will be lost when saving to CSV + DateTime.TryParse(resultDataTable.Rows[i][j].ToString(), out var resultDate); + Assert.Equal((DateTime)dataTable.Rows[i][j], resultDate); + } + else + { + //We compare string values because types change after writing and reading them back + Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); + } + } + } + } } \ No newline at end of file diff --git a/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs b/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs deleted file mode 100644 index c65f39c9..00000000 --- a/tests/MiniExcel.Csv.Tests/MiniExcelWriterTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace MiniExcelLib.Tests; - -public class MiniExcelWriterTests -{ - [Fact] - public async Task ExportDataTableWithProgressTest() - { - var dataTable = new DataTable(); - dataTable.Columns.Add("Id", typeof(int)); - dataTable.Columns.Add("Name", typeof(string)); - dataTable.Columns.Add("Date", typeof(DateTime)); - dataTable.Rows.Add(1, "Alice", new DateTime(1900, 1, 1, 1, 0, 0)); - dataTable.Rows.Add(2, DBNull.Value, new DateTime(1901, 2, 2, 2, 0, 0)); - dataTable.Rows.Add(3, "Alice", DateTime.Now.Date); - - // We need to use the file system because the CsvExporter automatically disposes the stream - var tempFilePath = Path.GetTempFileName(); - File.Delete(tempFilePath); - - var progress = new SimpleProgress(); - var exporter = new CsvExporter(); - var rowCounts = await exporter.ExportAsync(tempFilePath, dataTable, progress: progress); - Assert.Single(rowCounts); - Assert.Equal(3, rowCounts.First()); - - //Confirm the progress report is correct - var cellCount = dataTable.Columns.Count * dataTable.Rows.Count; - Assert.Equal(cellCount, progress.Value); - - var importer = new CsvImporter(); - var resultDataTable = importer.QueryAsDataTable(tempFilePath); - - //Confirm the data is correct - Assert.Equal(dataTable.Rows.Count, resultDataTable.Rows.Count); - Assert.Equal(dataTable.Columns.Count, resultDataTable.Columns.Count); - for (var i = 0; i < dataTable.Rows.Count; i++) - { - for (var j = 0; j < dataTable.Columns.Count; j++) - { - if (dataTable.Columns[j].DataType == typeof(DateTime)) - { - //We need to compare Dates properly as they will be formatted differently in CSV - //Note: if dates have millisecond precision that will be lost when saving to CSV - DateTime.TryParse(resultDataTable.Rows[i][j].ToString(), out var resultDate); - Assert.Equal((DateTime)dataTable.Rows[i][j], resultDate); - } - else - { - //We compare string values because types change after writing and reading them back - Assert.Equal(dataTable.Rows[i][j].ToString(), resultDataTable.Rows[i][j].ToString()); - } - } - } - } - - private class SimpleProgress : IProgress - { - public int Value { get; private set; } - public void Report(int value) - { - Value += value; - } - } -} diff --git a/tests/MiniExcel.Tests.Common/Utils/SimpleProgress.cs b/tests/MiniExcel.Tests.Common/Utils/SimpleProgress.cs new file mode 100644 index 00000000..16a27672 --- /dev/null +++ b/tests/MiniExcel.Tests.Common/Utils/SimpleProgress.cs @@ -0,0 +1,10 @@ +namespace MiniExcelLib.Tests.Common.Utils; + +public class SimpleProgress: IProgress +{ + public int Value { get; private set; } + public void Report(int value) + { + Value += value; + } +}