diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index d2382fcf..edb30274 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -1,6 +1,7 @@ name: MiniExcel Benchmarks on: + workflow_dispatch: release: types: [published] @@ -29,19 +30,14 @@ jobs: env: BenchmarkMode: Automatic BenchmarkSection: query - - name: Commit report - working-directory: ./benchmarks - run: | - cp -r ./MiniExcel.Benchmarks/BenchmarkDotNet.Artifacts/results ./ - git config user.name github-actions - git config user.email github-actions@github.com - git pull - cd ./results - git add '*.md' - git commit -am "Automated benchmark report - query section" - git push --force - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Renaming result file + run: mv MiniExcelLibs.Benchmarks.XlsxBenchmark-report-github.md query-benchmark.md + working-directory: ./benchmarks/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: query-benchmark-result + path: ./benchmark/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results/*.md CreateBenchmark: runs-on: ubuntu-latest @@ -64,20 +60,15 @@ jobs: env: BenchmarkMode: Automatic BenchmarkSection: create - - name: Commit report - working-directory: ./benchmarks - run: | - cp -r ./MiniExcel.Benchmarks/BenchmarkDotNet.Artifacts/results ./ - git config user.name github-actions - git config user.email github-actions@github.com - git pull - cd ./results - git add '*.md' - git commit -am "Automated benchmark report - create section" - git push --force - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - + - name: Renaming result file + run: mv MiniExcelLibs.Benchmarks.XlsxBenchmark-report-github.md create-benchmark.md + working-directory: ./benchmarks/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: create-benchmark-result + path: ./benchmark/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results/*.md + TemplateBenchmark: runs-on: ubuntu-latest @@ -99,16 +90,33 @@ jobs: env: BenchmarkMode: Automatic BenchmarkSection: template - - name: Commit report - working-directory: ./benchmarks + - name: Renaming result file + run: mv MiniExcelLibs.Benchmarks.XlsxBenchmark-report-github.md template-benchmark.md + working-directory: ./benchmarks/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results + - name: Save benchmark results + uses: actions/upload-artifact@v4 + with: + name: template-benchmark-result + path: ./benchmark/MiniExcel.Benchmarks/BenchMarkDotNet.Artifacts/results/*.md + + PushBenchmarksResults: + runs-on: ubuntu-latest + needs: [ QueryBenchmark, CreateBenchmark, TemplateBenchmark ] + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch benchmark results + uses: actions/download-artifact@v4 + with: + path: ./benchmarks/results + merge-multiple: true + - name: Commit reports + working-directory: ./benchmarks/results run: | - cp -r ./MiniExcel.Benchmarks/BenchmarkDotNet.Artifacts/results ./ git config user.name github-actions git config user.email github-actions@github.com - git pull - cd ./results - git add '*.md' - git commit -am "Automated benchmark report - template section" - git push --force - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + git add ./*.md + git commit -am "Automated benchmark report - ${{ github.ref_name }}" + git push origin master --force-with-lease \ No newline at end of file diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 94859672..5cf65e17 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -14,11 +14,11 @@ jobs: steps: - uses: actions/checkout@v4 - name: Setup .NET 8 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 8.0.x - name: Setup .NET 9 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: 9.0.x - name: Restore dependencies diff --git a/MiniExcel.sln b/MiniExcel.sln index 89f3ac72..8d3d52f2 100644 --- a/MiniExcel.sln +++ b/MiniExcel.sln @@ -16,6 +16,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Docs and setting", "Docs an README.md = README.md README.zh-CN.md = README.zh-CN.md README.zh-Hant.md = README.zh-Hant.md + .github\workflows\benchmark.yml = .github\workflows\benchmark.yml + .github\workflows\codeql-analysis.yml = .github\workflows\codeql-analysis.yml EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CC1E0601-AEC9-42D7-8F6A-3FB3939EED16}" diff --git a/README.md b/README.md index 2351f4e2..288c5308 100644 --- a/README.md +++ b/README.md @@ -1517,7 +1517,7 @@ MiniExcel.AddPicture(path, images); #### 7. Get Sheets Dimension ```csharp -var dim = MiniExcel.GetSheetsDimensions(path); +var dim = MiniExcel.GetSheetDimensions(path); ``` ### Examples: diff --git a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj index 44eda503..cc7b5fc9 100644 --- a/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj +++ b/benchmarks/MiniExcel.Benchmarks/MiniExcel.Benchmarks.csproj @@ -10,13 +10,12 @@ - - - - - + + + + + - diff --git a/benchmarks/MiniExcel.Benchmarks/Program.cs b/benchmarks/MiniExcel.Benchmarks/Program.cs index 99b23519..792bcab9 100644 --- a/benchmarks/MiniExcel.Benchmarks/Program.cs +++ b/benchmarks/MiniExcel.Benchmarks/Program.cs @@ -1,5 +1,4 @@ -using System.ComponentModel; -using BenchmarkDotNet.Running; +using BenchmarkDotNet.Running; using MiniExcelLibs.Benchmarks; using MiniExcelLibs.Benchmarks.BenchmarkSections; @@ -11,12 +10,19 @@ "query" => typeof(QueryXlsxBenchmark), "create" => typeof(CreateXlsxBenchmark), "template" => typeof(TemplateXlsxBenchmark), - _ => throw new InvalidEnumArgumentException($"Benchmark section {section} does not exist") + _ => throw new ArgumentException($"Benchmark section {section} does not exist") }; BenchmarkRunner.Run(benchmark, new Config(), args); } else +{ BenchmarkSwitcher - .FromTypes([typeof(CreateXlsxBenchmark)]) - .Run(args, new Config()); \ No newline at end of file + .FromTypes( + [ + typeof(QueryXlsxBenchmark), + typeof(CreateXlsxBenchmark), + typeof(TemplateXlsxBenchmark) + ]) + .Run(args, new Config()); +} \ No newline at end of file diff --git a/src/MiniExcel/Csv/CsvConfiguration.cs b/src/MiniExcel/Csv/CsvConfiguration.cs index afee65fa..1d795c40 100644 --- a/src/MiniExcel/Csv/CsvConfiguration.cs +++ b/src/MiniExcel/Csv/CsvConfiguration.cs @@ -6,18 +6,19 @@ namespace MiniExcelLibs.Csv { public class CsvConfiguration : Configuration { - private static Encoding _defaultEncoding = new UTF8Encoding(true); + private static readonly Encoding DefaultEncoding = new UTF8Encoding(true); public char Seperator { get; set; } = ','; public string NewLine { get; set; } = "\r\n"; public bool ReadLineBreaksWithinQuotes { get; set; } = true; public bool ReadEmptyStringAsNull { get; set; } = false; public bool AlwaysQuote { get; set; } = false; + public bool QuoteWhitespaces { get; set; } = true; public Func SplitFn { get; set; } - public Func StreamReaderFunc { get; set; } = (stream) => new StreamReader(stream, _defaultEncoding); - public Func StreamWriterFunc { get; set; } = (stream) => new StreamWriter(stream, _defaultEncoding); + public Func StreamReaderFunc { get; set; } = (stream) => new StreamReader(stream, DefaultEncoding); + public Func StreamWriterFunc { get; set; } = (stream) => new StreamWriter(stream, DefaultEncoding); - internal readonly static CsvConfiguration DefaultConfiguration = new CsvConfiguration(); + internal static readonly CsvConfiguration DefaultConfiguration = new CsvConfiguration(); } } diff --git a/src/MiniExcel/Csv/CsvHelpers.cs b/src/MiniExcel/Csv/CsvHelpers.cs index a9b878a9..b6fd5a10 100644 --- a/src/MiniExcel/Csv/CsvHelpers.cs +++ b/src/MiniExcel/Csv/CsvHelpers.cs @@ -3,7 +3,7 @@ internal static class CsvHelpers { /// If content contains special characters then use "{value}" format - public static string ConvertToCsvValue(string value, bool alwaysQuote, char separator) + public static string ConvertToCsvValue(string value, CsvConfiguration configuration) { if (value == null) return string.Empty; @@ -13,16 +13,14 @@ public static string ConvertToCsvValue(string value, bool alwaysQuote, char sepa value = value.Replace("\"", "\"\""); return $"\"{value}\""; } - - if (value.Contains(separator.ToString()) || value.Contains(" ") || value.Contains("\n") || value.Contains("\r")) - { - return $"\"{value}\""; - } - - if (alwaysQuote) - return $"\"{value}\""; - - return value; + + var shouldQuote = configuration.AlwaysQuote || + (configuration.QuoteWhitespaces && value.Contains(" ")) || + value.Contains(configuration.Seperator.ToString()) || + value.Contains("\r") || + value.Contains("\n"); + + return shouldQuote ? $"\"{value}\"" : value; } } } diff --git a/src/MiniExcel/Csv/CsvWriter.cs b/src/MiniExcel/Csv/CsvWriter.cs index d65daa5e..efa46fd8 100644 --- a/src/MiniExcel/Csv/CsvWriter.cs +++ b/src/MiniExcel/Csv/CsvWriter.cs @@ -49,7 +49,7 @@ public int Insert(bool overwriteSheet = false) private void AppendColumn(StringBuilder rowBuilder, CellWriteInfo column) { - rowBuilder.Append(CsvHelpers.ConvertToCsvValue(ToCsvString(column.Value, column.Prop), _configuration.AlwaysQuote, _configuration.Seperator)); + rowBuilder.Append(CsvHelpers.ConvertToCsvValue(ToCsvString(column.Value, column.Prop), _configuration)); rowBuilder.Append(_configuration.Seperator); } @@ -63,7 +63,7 @@ private static void RemoveTrailingSeparator(StringBuilder rowBuilder) private string GetHeader(List props) => string.Join( _configuration.Seperator.ToString(), - props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration.AlwaysQuote, _configuration.Seperator))); + props.Select(s => CsvHelpers.ConvertToCsvValue(s?.ExcelColumnName, _configuration))); private int WriteValues(object values) { diff --git a/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs b/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs index fdb8a37a..34163c2d 100644 --- a/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs +++ b/src/MiniExcel/OpenXml/SharedStringsDiskCache.cs @@ -2,7 +2,6 @@ using System.Collections; using System.Collections.Generic; using System.IO; -using System.Runtime.CompilerServices; using System.Text; namespace MiniExcelLibs.OpenXml @@ -37,9 +36,11 @@ internal void Add(int index, string value) { if (index > _maxIndx) _maxIndx = index; - byte[] valueBs = _encoding.GetBytes(value); + + var valueBs = _encoding.GetBytes(value); if (value.Length > 32767) //check info length, becasue cell string max length is 47483647 - throw new ArgumentOutOfRangeException("Excel one cell max length is 32,767 characters"); + throw new ArgumentOutOfRangeException("", "Excel one cell max length is 32,767 characters"); + _positionFs.Write(BitConverter.GetBytes(_valueFs.Position), 0, 4); _lengthFs.Write(BitConverter.GetBytes(valueBs.Length), 0, 4); _valueFs.Write(valueBs, 0, valueBs.Length); @@ -49,16 +50,19 @@ private string GetValue(int index) { _positionFs.Position = index * 4; var bytes = new byte[4]; - _positionFs.Read(bytes, 0, 4); + _ = _positionFs.Read(bytes, 0, 4); var position = BitConverter.ToInt32(bytes, 0); + + bytes = new byte[4]; _lengthFs.Position = index * 4; - _lengthFs.Read(bytes, 0, 4); + _ = _lengthFs.Read(bytes, 0, 4); var length = BitConverter.ToInt32(bytes, 0); - _valueFs.Position = position; + bytes = new byte[length]; - _valueFs.Read(bytes, 0, length); - var v = _encoding.GetString(bytes); - return v; + _valueFs.Position = position; + _ = _valueFs.Read(bytes, 0, length); + + return _encoding.GetString(bytes); } protected virtual void Dispose(bool disposing) @@ -69,15 +73,19 @@ protected virtual void Dispose(bool disposing) { // TODO: dispose managed state (managed objects) } + _positionFs.Dispose(); if (File.Exists(_positionFs.Name)) File.Delete(_positionFs.Name); + _lengthFs.Dispose(); if (File.Exists(_lengthFs.Name)) File.Delete(_lengthFs.Name); + _valueFs.Dispose(); if (File.Exists(_valueFs.Name)) File.Delete(_valueFs.Name); + _disposedValue = true; } } diff --git a/src/MiniExcel/Utils/ExcelTypeHelper.cs b/src/MiniExcel/Utils/ExcelTypeHelper.cs index 92063e91..53e4926e 100644 --- a/src/MiniExcel/Utils/ExcelTypeHelper.cs +++ b/src/MiniExcel/Utils/ExcelTypeHelper.cs @@ -32,7 +32,10 @@ internal static ExcelType GetExcelType(Stream stream, ExcelType excelType) var probe = new byte[8]; stream.Seek(0, SeekOrigin.Begin); - stream.Read(probe, 0, probe.Length); + var read = stream.Read(probe, 0, probe.Length); + if (read != probe.Length) + throw new InvalidDataException("The file/stream does not contain enough data to process"); + stream.Seek(0, SeekOrigin.Begin); // New office format (can be any ZIP archive) @@ -41,7 +44,7 @@ internal static ExcelType GetExcelType(Stream stream, ExcelType excelType) return ExcelType.XLSX; } - throw new NotSupportedException("Stream cannot know the file type, please specify ExcelType manually"); + throw new InvalidDataException("The file type could not be inferred automatically, please specify ExcelType manually"); } } } diff --git a/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs b/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs index 295ca904..46dd2c55 100644 --- a/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs +++ b/tests/MiniExcelTests/MiniExcelCsvAsycTests.cs @@ -102,25 +102,23 @@ public async Task SaveAsByDictionary() { "c", false }, { "d", new DateTime(2021, 1, 2) } } - ]; var rowsWritten = await MiniExcel.SaveAsAsync(path, values); Assert.Equal(2, rowsWritten[0]); - using (var reader = new StreamReader(path)) - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - var records = csv.GetRecords().ToList(); - Assert.Equal(@"""<>+-*//}{\\n", records[0].a); - Assert.Equal("1234567890", records[0].b); - Assert.Equal("True", records[0].c); - Assert.Equal("2021-01-01 00:00:00", records[0].d); - - Assert.Equal("Hello World", records[1].a); - Assert.Equal("-1234567890", records[1].b); - Assert.Equal("False", records[1].c); - Assert.Equal("2021-01-02 00:00:00", records[1].d); - } + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + + Assert.Equal(@"""<>+-*//}{\\n", records[0].a); + Assert.Equal("1234567890", records[0].b); + Assert.Equal("True", records[0].c); + Assert.Equal("2021-01-01 00:00:00", records[0].d); + + Assert.Equal("Hello World", records[1].a); + Assert.Equal("-1234567890", records[1].b); + Assert.Equal("False", records[1].c); + Assert.Equal("2021-01-02 00:00:00", records[1].d); } { @@ -145,116 +143,104 @@ public async Task SaveAsByDictionary() { 4, new DateTime(2021, 1, 2) } } ]; + var rowsWritten = await MiniExcel.SaveAsAsync(path, values); Assert.Equal(2, rowsWritten[0]); - using (var reader = new StreamReader(path)) - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - var records = csv.GetRecords().ToList(); - { - var row = records[0] as IDictionary; - Assert.Equal(@"""<>+-*//}{\\n", row["1"]); - Assert.Equal("1234567890", row["2"]); - Assert.Equal("True", row["3"]); - Assert.Equal("2021-01-01 00:00:00", row["4"]); - } - { - var row = records[1] as IDictionary; - Assert.Equal("Hello World", row["1"]); - Assert.Equal("-1234567890", row["2"]); - Assert.Equal("False", row["3"]); - Assert.Equal("2021-01-02 00:00:00", row["4"]); - } - } + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + + var row1 = records[0] as IDictionary; + Assert.Equal(@"""<>+-*//}{\\n", row1!["1"]); + Assert.Equal("1234567890", row1["2"]); + Assert.Equal("True", row1["3"]); + Assert.Equal("2021-01-01 00:00:00", row1["4"]); + + var row2 = records[1] as IDictionary; + Assert.Equal("Hello World", row2!["1"]); + Assert.Equal("-1234567890", row2["2"]); + Assert.Equal("False", row2["3"]); + Assert.Equal("2021-01-02 00:00:00", row2["4"]); } } [Fact] public async Task SaveAsByDataTableTest() { - { - using var file = AutoDeletingPath.Create(ExcelType.CSV); - var path = file.ToString(); + using var file1 = AutoDeletingPath.Create(ExcelType.CSV); + var path1 = file1.ToString(); - var table = new DataTable(); - await MiniExcel.SaveAsAsync(path, table); + var emptyTable = new DataTable(); + await MiniExcel.SaveAsAsync(path1, emptyTable); - var text = File.ReadAllText(path); - Assert.Equal("\r\n", text); - } + var text = await File.ReadAllTextAsync(path1); + Assert.Equal("\r\n", text); - { - var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.csv"); - - 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, new DateTime(2021, 1, 1)); - table.Rows.Add("Hello World", -1234567890, false, new DateTime(2021, 1, 2)); - } + + using var file2= AutoDeletingPath.Create(ExcelType.CSV); + var path2 = file2.ToString(); + + 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, new DateTime(2021, 1, 1)); + table.Rows.Add("Hello World", -1234567890, false, new DateTime(2021, 1, 2)); + + var rowsWritten = await MiniExcel.SaveAsAsync(path2, table); + Assert.Equal(2, rowsWritten[0]); - var rowsWritten = await MiniExcel.SaveAsAsync(path, table); - Assert.Equal(2, rowsWritten[0]); - - using (var reader = new StreamReader(path)) - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - var records = csv.GetRecords().ToList(); - Assert.Equal(@"""<>+-*//}{\\n", records[0].a); - Assert.Equal("1234567890", records[0].b); - Assert.Equal("True", records[0].c); - Assert.Equal("2021-01-01 00:00:00", records[0].d); - - Assert.Equal("Hello World", records[1].a); - Assert.Equal("-1234567890", records[1].b); - Assert.Equal("False", records[1].c); - Assert.Equal("2021-01-02 00:00:00", records[1].d); - } - } + using var reader = new StreamReader(path2); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + + Assert.Equal(@"""<>+-*//}{\\n", records[0].a); + Assert.Equal("1234567890", records[0].b); + Assert.Equal("True", records[0].c); + Assert.Equal("2021-01-01 00:00:00", records[0].d); + + Assert.Equal("Hello World", records[1].a); + Assert.Equal("-1234567890", records[1].b); + Assert.Equal("False", records[1].c); + Assert.Equal("2021-01-02 00:00:00", records[1].d); } private class Test { - public string c1 { get; set; } - public string c2 { get; set; } + public string? c1 { get; set; } + public string? c2 { get; set; } } [Fact] public async Task CsvExcelTypeTest() { - { - using var file = AutoDeletingPath.Create(ExcelType.CSV); - var path = file.ToString(); + using var file = AutoDeletingPath.Create(ExcelType.CSV); + var path = file.ToString(); - var input = new[] { new { A = "Test1", B = "Test2" } }; - await MiniExcel.SaveAsAsync(path, input); + var input = new[] { new { A = "Test1", B = "Test2" } }; + await MiniExcel.SaveAsAsync(path, input); - var texts = File.ReadAllLines(path); - Assert.Equal("A,B", texts[0]); - Assert.Equal("Test1,Test2", texts[1]); + var texts = await File.ReadAllLinesAsync(path); + Assert.Equal("A,B", texts[0]); + Assert.Equal("Test1,Test2", texts[1]); - { - var q = await MiniExcel.QueryAsync(path); - var rows = q.ToList(); - Assert.Equal("A", rows[0].A); - Assert.Equal("B", rows[0].B); - Assert.Equal("Test1", rows[1].A); - Assert.Equal("Test2", rows[1].B); - } + var q = await MiniExcel.QueryAsync(path); + var rows1 = q.ToList(); - using (var reader = new StreamReader(path)) - using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) - { - var rows = csv.GetRecords().ToList(); - Assert.Equal("Test1", rows[0].A); - Assert.Equal("Test2", rows[0].B); - } - } + Assert.Equal("A", rows1[0].A); + Assert.Equal("B", rows1[0].B); + Assert.Equal("Test1", rows1[1].A); + Assert.Equal("Test2", rows1[1].B); + + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var rows2 = csv.GetRecords().ToList(); + + Assert.Equal("Test1", rows2[0].A); + Assert.Equal("Test2", rows2[0].B); } [Fact] @@ -263,14 +249,15 @@ public async Task Create2x2_Test() using var file = AutoDeletingPath.Create(ExcelType.CSV); var path = file.ToString(); - await MiniExcel.SaveAsAsync(path, new[] { - new { c1 = "A1" ,c2 = "B1"}, - new { c1 = "A2" ,c2 = "B2"}, + await MiniExcel.SaveAsAsync(path, new[] + { + new { c1 = "A1", c2 = "B1"}, + new { c1 = "A2", c2 = "B2"}, }); - using (var stream = File.OpenRead(path)) + await using (var stream = File.OpenRead(path)) { - var rows = stream.Query(useHeaderRow: true, excelType: ExcelType.CSV).ToList(); + var rows = (await stream.QueryAsync(useHeaderRow: true, excelType: ExcelType.CSV)).ToList(); Assert.Equal("A1", rows[0].c1); Assert.Equal("B1", rows[0].c2); Assert.Equal("A2", rows[1].c1); @@ -278,7 +265,7 @@ await MiniExcel.SaveAsAsync(path, new[] { } { - var rows = MiniExcel.Query(path, useHeaderRow: true, excelType: ExcelType.CSV).ToList(); + var rows = (await MiniExcel.QueryAsync(path, useHeaderRow: true, excelType: ExcelType.CSV)).ToList(); Assert.Equal("A1", rows[0].c1); Assert.Equal("B1", rows[0].c2); Assert.Equal("A2", rows[1].c1); @@ -292,12 +279,13 @@ public async Task CsvTypeMappingTest() using var file = AutoDeletingPath.Create(ExcelType.CSV); var path = file.ToString(); - await MiniExcel.SaveAsAsync(path, new[] { - new { c1 = "A1" ,c2 = "B1"}, - new { c1 = "A2" ,c2 = "B2"}, + await MiniExcel.SaveAsAsync(path, new[] + { + new { c1 = "A1", c2 = "B1"}, + new { c1 = "A2", c2 = "B2"} }); - using (var stream = File.OpenRead(path)) + await using (var stream = File.OpenRead(path)) { var rows = stream.Query(excelType: ExcelType.CSV).ToList(); Assert.Equal("A1", rows[0].c1); @@ -323,11 +311,11 @@ public async Task CsvReadEmptyStringAsNullTest() await MiniExcel.SaveAsAsync(path, new[] { - new { c1 = "A1", c2 = (string)null}, - new { c1 = (string)null, c2 = (string)null}, + new { c1 = (string?)"A1", c2 = (string?)null}, + new { c1 = (string?)null, c2 = (string?)null} }); - using (var stream = File.OpenRead(path)) + await using (var stream = File.OpenRead(path)) { var rows = stream.Query(excelType: ExcelType.CSV).ToList(); Assert.Equal("A1", rows[0].c1); @@ -345,7 +333,7 @@ await MiniExcel.SaveAsAsync(path, new[] } var config = new Csv.CsvConfiguration { ReadEmptyStringAsNull = true }; - using (var stream = File.OpenRead(path)) + await using (var stream = File.OpenRead(path)) { var rows = stream.Query(excelType: ExcelType.CSV, configuration: config).ToList(); Assert.Equal("A1", rows[0].c1); diff --git a/tests/MiniExcelTests/MiniExcelCsvTests.cs b/tests/MiniExcelTests/MiniExcelCsvTests.cs index 56eb6fde..ad1b8e70 100644 --- a/tests/MiniExcelTests/MiniExcelCsvTests.cs +++ b/tests/MiniExcelTests/MiniExcelCsvTests.cs @@ -60,6 +60,43 @@ public void SeperatorTest() """"; Assert.Equal(expected, File.ReadAllText(path)); } + + [Fact] + public void DontQuoteWhitespacesTest() + { + using var file = AutoDeletingPath.Create(ExcelType.CSV); + var path = file.ToString(); + + List> values = + [ + new() + { + { "a", @"""<>+-*//}{\\n" }, + { "b", 1234567890 }, + { "c", true }, + { "d", new DateTime(2021, 1, 1) } + }, + + new() + { + { "a", "Hello World" }, + { "b", -1234567890 }, + { "c", false }, + { "d", new DateTime(2021, 1, 2) } + } + ]; + var rowsWritten = MiniExcel.SaveAs(path, values, configuration: new Csv.CsvConfiguration { QuoteWhitespaces = false }); + Assert.Equal(2, rowsWritten[0]); + + const string expected = + """" + a,b,c,d + """<>+-*//}{\\n",1234567890,True,2021-01-01 00:00:00 + Hello World,-1234567890,False,2021-01-02 00:00:00 + + """"; + Assert.Equal(expected, File.ReadAllText(path)); + } [Fact] public void AlwaysQuoteTest() @@ -203,14 +240,14 @@ public void SaveAsByDictionary() var records = csv.GetRecords().ToList(); { var row = records[0] as IDictionary; - Assert.Equal(@"""<>+-*//}{\\n", row["1"]); + Assert.Equal(@"""<>+-*//}{\\n", row!["1"]); Assert.Equal("1234567890", row["2"]); Assert.Equal("True", row["3"]); Assert.Equal("2021-01-01 00:00:00", row["4"]); } { var row = records[1] as IDictionary; - Assert.Equal("Hello World", row["1"]); + Assert.Equal("Hello World", row!["1"]); Assert.Equal("-1234567890", row["2"]); Assert.Equal("False", row["3"]); Assert.Equal("2021-01-02 00:00:00", row["4"]); @@ -283,33 +320,27 @@ private class TestWithAlias [Fact] public void CsvExcelTypeTest() { - { - using var file = AutoDeletingPath.Create(ExcelType.CSV); - var path = file.ToString(); + using var file = AutoDeletingPath.Create(ExcelType.CSV); + var path = file.ToString(); - var input = new[] { new { A = "Test1", B = "Test2" } }; - MiniExcel.SaveAs(path.ToString(), input); - - var texts = File.ReadAllLines(path.ToString()); - Assert.Equal("A,B", texts[0]); - Assert.Equal("Test1,Test2", texts[1]); - - { - var rows = MiniExcel.Query(path.ToString()).ToList(); - Assert.Equal("A", rows[0].A); - Assert.Equal("B", rows[0].B); - Assert.Equal("Test1", rows[1].A); - Assert.Equal("Test2", rows[1].B); - } - - using var reader = new StreamReader(path.ToString()); - using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); - { - var rows = csv.GetRecords().ToList(); - Assert.Equal("Test1", rows[0].A); - Assert.Equal("Test2", rows[0].B); - } - } + var input = new[] { new { A = "Test1", B = "Test2" } }; + MiniExcel.SaveAs(path, input); + + var texts = File.ReadAllLines(path); + Assert.Equal("A,B", texts[0]); + Assert.Equal("Test1,Test2", texts[1]); + + var rows = MiniExcel.Query(path).ToList(); + Assert.Equal("A", rows[0].A); + Assert.Equal("B", rows[0].B); + Assert.Equal("Test1", rows[1].A); + Assert.Equal("Test2", rows[1].B); + + using var reader = new StreamReader(path); + using var csv = new CsvReader(reader, CultureInfo.InvariantCulture); + var records = csv.GetRecords().ToList(); + Assert.Equal("Test1", records[0].A); + Assert.Equal("Test2", records[0].B); } [Fact] @@ -388,7 +419,7 @@ public void CsvColumnNotFoundTest() Assert.Equal(2, exception.RowIndex); Assert.Null(exception.ColumnIndex); Assert.True(exception.RowValues is IDictionary); - Assert.Equal(1, ((IDictionary)exception.RowValues).Count); + Assert.Single((IDictionary)exception.RowValues); } { @@ -398,7 +429,7 @@ public void CsvColumnNotFoundTest() Assert.Equal(2, exception.RowIndex); Assert.Null(exception.ColumnIndex); Assert.True(exception.RowValues is IDictionary); - Assert.Equal(1, ((IDictionary)exception.RowValues).Count); + Assert.Single((IDictionary)exception.RowValues); } } @@ -417,7 +448,7 @@ public void CsvColumnNotFoundWithAliasTest() Assert.Equal(2, exception.RowIndex); Assert.Null(exception.ColumnIndex); Assert.True(exception.RowValues is IDictionary); - Assert.Equal(1, ((IDictionary)exception.RowValues).Count); + Assert.Single((IDictionary)exception.RowValues); } { @@ -427,7 +458,7 @@ public void CsvColumnNotFoundWithAliasTest() Assert.Equal(2, exception.RowIndex); Assert.Null(exception.ColumnIndex); Assert.True(exception.RowValues is IDictionary); - Assert.Equal(1, ((IDictionary)exception.RowValues).Count); + Assert.Single((IDictionary)exception.RowValues); } } diff --git a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs index be8933c3..62b41a9e 100644 --- a/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueAsyncTests.cs @@ -186,7 +186,7 @@ public async Task Issue242() await Assert.ThrowsAsync(async () => _ = (await MiniExcel.QueryAsync(path)).ToList()); await using var stream = File.OpenRead(path); - await Assert.ThrowsAsync(async () => _ = (await stream.QueryAsync()).ToList()); + await Assert.ThrowsAsync(async () => _ = (await stream.QueryAsync()).ToList()); } /// diff --git a/tests/MiniExcelTests/MiniExcelIssueTests.cs b/tests/MiniExcelTests/MiniExcelIssueTests.cs index 50efaf67..5e4cd908 100644 --- a/tests/MiniExcelTests/MiniExcelIssueTests.cs +++ b/tests/MiniExcelTests/MiniExcelIssueTests.cs @@ -152,8 +152,8 @@ public void TestIssueI4X92G() { var value = new[] { - new { ID=1,Name ="Jack",InDate=new DateTime(2021,01,03)}, - new { ID=2,Name ="Henry",InDate=new DateTime(2020,05,03)}, + new { ID = 1, Name = "Jack", InDate = new DateTime(2021,01,03)}, + new { ID = 2, Name = "Henry", InDate = new DateTime(2020,05,03)} }; MiniExcel.SaveAs(path, value); var content = File.ReadAllText(path); @@ -2012,7 +2012,7 @@ public void Issue242() Assert.Throws(() => MiniExcel.Query(path).ToList()); using var stream = File.OpenRead(path); - Assert.Throws(() => stream.Query().ToList()); + Assert.Throws(() => stream.Query().ToList()); } /// diff --git a/tests/MiniExcelTests/Utils/Helpers.cs b/tests/MiniExcelTests/Utils/Helpers.cs index 0ea61e4b..40695ac7 100644 --- a/tests/MiniExcelTests/Utils/Helpers.cs +++ b/tests/MiniExcelTests/Utils/Helpers.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.IO.Compression; -using System.Linq; +using System.IO.Compression; using System.Text; using System.Xml; using System.Xml.Linq; @@ -12,50 +8,43 @@ namespace MiniExcelLibs.Tests.Utils; internal static class Helpers { - private const int GENERAL_COLUMN_INDEX = 255; - private const int MAX_COLUMN_INDEX = 16383; - private static Dictionary? _IntMappingAlphabet; - private static Dictionary? _AlphabetMappingInt; + private const int GeneralColumnIndex = 255; + private const int MaxColumnIndex = 16383; - static Helpers() - { - if (_IntMappingAlphabet != null || _AlphabetMappingInt != null) - return; - - _IntMappingAlphabet = new Dictionary(); - _AlphabetMappingInt = new Dictionary(); - for (int i = 0; i <= GENERAL_COLUMN_INDEX; i++) - { - _IntMappingAlphabet.Add(i, IntToLetters(i)); - _AlphabetMappingInt.Add(IntToLetters(i), i); - } - } + private static readonly Dictionary IntMappingAlphabet = Enumerable + .Range(0, GeneralColumnIndex) + .ToDictionary(i => i, IntToLetters); + + private static readonly Dictionary AlphabetMappingInt = Enumerable + .Range(0, MaxColumnIndex) + .ToDictionary(IntToLetters, i => i); + public static string GetAlphabetColumnName(int columnIndex) { CheckAndSetMaxColumnIndex(columnIndex); - return _IntMappingAlphabet[columnIndex]; + return IntMappingAlphabet[columnIndex]; } public static int GetColumnIndex(string columnName) { - var columnIndex = _AlphabetMappingInt[columnName]; + var columnIndex = AlphabetMappingInt[columnName]; CheckAndSetMaxColumnIndex(columnIndex); return columnIndex; } private static void CheckAndSetMaxColumnIndex(int columnIndex) { - if (columnIndex < _IntMappingAlphabet.Count) + if (columnIndex < IntMappingAlphabet.Count) return; - if (columnIndex > MAX_COLUMN_INDEX) + if (columnIndex > MaxColumnIndex) throw new InvalidDataException($"ColumnIndex {columnIndex} is over Excel vaild max index."); - for (int i = _IntMappingAlphabet.Count; i <= columnIndex; i++) + for (int i = IntMappingAlphabet.Count; i <= columnIndex; i++) { - _IntMappingAlphabet.Add(i, IntToLetters(i)); - _AlphabetMappingInt.Add(IntToLetters(i), i); + IntMappingAlphabet.Add(i, IntToLetters(i)); + AlphabetMappingInt.Add(IntToLetters(i), i); } } @@ -87,7 +76,7 @@ internal static string GetZipFileContent(string zipPath, string filePath) return doc.ToString(); } - internal static string GetFirstSheetDimensionRefValue(string path) + internal static string? GetFirstSheetDimensionRefValue(string path) { var ns = new XmlNamespaceManager(new NameTable()); ns.AddNamespace("x", "http://schemas.openxmlformats.org/spreadsheetml/2006/main"); @@ -101,7 +90,7 @@ internal static string GetFirstSheetDimensionRefValue(string path) using var sheetStream = sheet.Open(); var doc = XDocument.Load(sheetStream); var dimension = doc.XPathSelectElement("/x:worksheet/x:dimension", ns); - var refV = dimension.Attribute("ref").Value; + var refV = dimension?.Attribute("ref")?.Value; return refV; } diff --git a/tests/MiniExcelTests/Utils/PathHelper.cs b/tests/MiniExcelTests/Utils/PathHelper.cs index c3181dbc..d09df59e 100644 --- a/tests/MiniExcelTests/Utils/PathHelper.cs +++ b/tests/MiniExcelTests/Utils/PathHelper.cs @@ -2,16 +2,13 @@ internal static class PathHelper { - public static string GetFile(string fileName) - { - return $"../../../../../samples/{fileName}"; - } + public static string GetFile(string fileName) => $"../../../../../samples/{fileName}"; public static string GetTempPath(string extension = "xlsx") { - var method = new System.Diagnostics.StackTrace().GetFrame(1).GetMethod(); + var method = new System.Diagnostics.StackTrace().GetFrame(1)?.GetMethod(); - var path = Path.Combine(Path.GetTempPath(), $"{method.DeclaringType.Name}_{method.Name}.{extension}") + var path = Path.Combine(Path.GetTempPath(), $"{method?.DeclaringType?.Name}_{method?.Name}.{extension}") .Replace("<", string.Empty) .Replace(">", string.Empty);