diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index 3ccc64e..ed98eba 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -10,43 +10,52 @@ jobs: build-ubuntu: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 name: Checkout Code - name: Setup .NET - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - 5.0.x - 6.0.x + 8.0.x + 9.0.x + + # As per https://github.com/NuGet/setup-nuget/issues/168#issuecomment-2576628231 + # We are now required to install mono separately + - name: Install Mono + run: sudo apt install mono-complete + - name: Setup Nuget + uses: nuget/setup-nuget@v2 + with: + nuget-api-key: ${{ secrets.NUGET_API_KEY }} + nuget-version: '5.x' + - name: Install NUnit.ConsoleRunner 3.4.0 (compatibility) run: nuget install NUnit.ConsoleRunner -Version 3.4.0 -DirectDownload -OutputDirectory . - name: Install NUnit 3.11.0 (compatibility) run: nuget install NUnit -Version 3.11.0 -DirectDownload -OutputDirectory ./packages - name: Build (Framework 2.0) - run: msbuild ./src/net20/src.net20.csproj + run: xbuild ./src/net20/src.net20.csproj - name: Build (Framework 2.0 Tests) - run: msbuild ./tests/net20/tests.net20.csproj + run: xbuild ./tests/net20/tests.net20.csproj - name: Test (net20) working-directory: ./tests/net20/bin/Debug/ run: ../../../../NUnit.ConsoleRunner.3.4.0/tools/nunit3-console.exe ./tests.net20.dll - name: Build (Framework 4.0) - run: msbuild ./src/net40/src.net40.csproj + run: xbuild ./src/net40/src.net40.csproj - name: Build (Framework 4.0 Tests) - run: msbuild ./tests/net40/tests.net40.csproj + run: xbuild ./tests/net40/tests.net40.csproj - name: Test (net40) working-directory: ./tests/net40/bin/Debug run: ../../../../NUnit.ConsoleRunner.3.4.0/tools/nunit3-console.exe ./tests.net40.dll - name: Build (Framework 4.5) - run: msbuild ./src/net45/src.net45.csproj + run: xbuild ./src/net45/src.net45.csproj - name: Build (Framework 4.5 Tests) - run: msbuild ./tests/net45/tests.net45.csproj + run: xbuild ./tests/net45/tests.net45.csproj - name: Test (net45) working-directory: ./tests/net45/bin/Debug/ run: ../../../../NUnit.ConsoleRunner.3.4.0/tools/nunit3-console.exe ./tests.net45.dll - - name: Build (DotNet Core 5.0 and NetStandard 2.0) + - name: Build (DotNet Core 8.0 and NetStandard 2.0) run: dotnet build ./csharp-csv-reader.sln - - name: Test (net50) - run: dotnet test ./tests/net50/tests.net50.csproj - name: SonarCloud Install run: dotnet tool update dotnet-sonarscanner --tool-path /tmp/sonar @@ -56,9 +65,9 @@ jobs: - name: SonarCloud Start run: /tmp/sonar/dotnet-sonarscanner begin /k:"tspence_csharp-csv-reader" /o:"tspence" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" /d:sonar.cs.vscoveragexml.reportsPaths=coverage.xml - - name: Test (net60) + - name: Test (net80) run: - /tmp/coverage/dotnet-coverage collect "dotnet test ./tests/net60/tests.net60.csproj" -f xml -o "coverage.xml" + /tmp/coverage/dotnet-coverage collect "dotnet test ./tests/net80/tests.net80.csproj" -f xml -o "coverage.xml" - name: SonarCloud End run: /tmp/sonar/dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" diff --git a/.github/workflows/nuget-publish.yml b/.github/workflows/nuget-publish.yml index 391a618..c274272 100644 --- a/.github/workflows/nuget-publish.yml +++ b/.github/workflows/nuget-publish.yml @@ -13,32 +13,34 @@ jobs: name: Update NuGet package steps: - name: Checkout repository - uses: actions/checkout@v1 + uses: actions/checkout@v5 - name: Setup .NET Core @ Latest - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v5 with: dotnet-version: | - 5.0.x - 6.0.x - 7.0.x + 8.0.x - name: Build (Framework 2.0) - run: msbuild ./src/net20/src.net20.csproj /property:Configuration=Release + run: xbuild ./src/net20/src.net20.csproj /property:Configuration=Release - name: Build (Framework 4.0) - run: msbuild ./src/net40/src.net40.csproj /property:Configuration=Release + run: xbuild ./src/net40/src.net40.csproj /property:Configuration=Release - name: Build (Framework 4.5) - run: msbuild ./src/net45/src.net45.csproj /property:Configuration=Release + run: xbuild ./src/net45/src.net45.csproj /property:Configuration=Release - name: Build (DotNetCore 5.0) run: dotnet build -c Release ./src/net50/src.net50.csproj - name: Build (NetStandard 2.0) run: dotnet build -c Release ./src/netstandard20/src.netstandard20.csproj + # As per https://github.com/NuGet/setup-nuget/issues/168#issuecomment-2576628231 + # We are now required to install mono separately + - name: Install Mono + run: sudo apt install mono-complete - name: Setup Nuget - uses: nuget/setup-nuget@v1 + uses: nuget/setup-nuget@v2 with: nuget-api-key: ${{ secrets.NUGET_API_KEY }} - nuget-version: "5.x" + nuget-version: '5.x' - name: Run Nuget pack run: nuget pack CSVFile.nuspec diff --git a/CSVFile.nuspec b/CSVFile.nuspec index 38c4850..b130c52 100644 --- a/CSVFile.nuspec +++ b/CSVFile.nuspec @@ -2,7 +2,7 @@ CSVFile - 3.2.0 + 3.2.1 CSVFile Ted Spence Ted Spence @@ -13,13 +13,13 @@ Tiny and fast CSV and TSV parsing library (40KB) with zero dependencies. Compatible with DotNetFramework (2.0 onwards) and DotNetCore. docs/icons8-spreadsheet-96.png - August 5, 2024 + October 9, 2025 - * Fix issue with Windows-style newlines crossing chunks found by @joelverhagen - * Fix issue with endless loops reported by @wvvegt + * Fix issue with loops during text qualifiers, reported by @AlainBartmanDilaw and @james-perfectserve, fix by james + * Update to Net80 for mainstream build, Net50 is no longer relevant docs/README.md - Copyright 2006 - 2024 + Copyright 2006 - 2025 fast csv parser serialization deserialization streaming async @@ -27,7 +27,7 @@ - + @@ -38,6 +38,6 @@ - + diff --git a/csharp-csv-reader.sln b/csharp-csv-reader.sln index 96852a1..5ad1751 100644 --- a/csharp-csv-reader.sln +++ b/csharp-csv-reader.sln @@ -18,11 +18,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{E92F982D .github\workflows\nuget-publish.yml = .github\workflows\nuget-publish.yml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "src.net50", "src\net50\src.net50.csproj", "{C78A66F7-113D-452A-989B-306CD6534E7B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "src.net80", "src\net80\src.net80.csproj", "{C78A66F7-113D-452A-989B-306CD6534E7B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests.net50", "tests\net50\tests.net50.csproj", "{D0F7CD1F-EEA9-4727-94C8-71F69FB456BF}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests.net60", "tests\net60\tests.net60.csproj", "{1F5483FB-F3A2-4F92-874C-EE28EDACDF64}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "tests.net80", "tests\net80\tests.net80.csproj", "{D0F7CD1F-EEA9-4727-94C8-71F69FB456BF}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -42,10 +40,6 @@ Global {D0F7CD1F-EEA9-4727-94C8-71F69FB456BF}.Debug|Any CPU.Build.0 = Debug|Any CPU {D0F7CD1F-EEA9-4727-94C8-71F69FB456BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {D0F7CD1F-EEA9-4727-94C8-71F69FB456BF}.Release|Any CPU.Build.0 = Release|Any CPU - {1F5483FB-F3A2-4F92-874C-EE28EDACDF64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1F5483FB-F3A2-4F92-874C-EE28EDACDF64}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1F5483FB-F3A2-4F92-874C-EE28EDACDF64}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1F5483FB-F3A2-4F92-874C-EE28EDACDF64}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -54,7 +48,6 @@ Global {29D87C9E-D9D0-4EF7-895C-48B36C4CFCC0} = {0CDFDACD-6043-48E8-8AA3-7122461F7C7A} {C78A66F7-113D-452A-989B-306CD6534E7B} = {0CDFDACD-6043-48E8-8AA3-7122461F7C7A} {D0F7CD1F-EEA9-4727-94C8-71F69FB456BF} = {0CDFDACD-6043-48E8-8AA3-7122461F7C7A} - {1F5483FB-F3A2-4F92-874C-EE28EDACDF64} = {0CDFDACD-6043-48E8-8AA3-7122461F7C7A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0A368276-2291-4A3B-B4EE-1C2D8042E97D} diff --git a/package.cmd b/package.cmd deleted file mode 100644 index 810732f..0000000 --- a/package.cmd +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -msbuild csharp-csv-reader.sln /p:configuration=release -..\nuget pack csvfile.nuspec -move *.nupkg ..\nuget-temp-folder diff --git a/src/CSV.cs b/src/CSV.cs index ea77921..1974613 100644 --- a/src/CSV.cs +++ b/src/CSV.cs @@ -503,12 +503,16 @@ internal static string ItemsToCsv(IEnumerable items, CSVSettings settings, char[ /// The separator public static char? ParseSepLine(string line) { - if (line.StartsWith("sep", StringComparison.OrdinalIgnoreCase)) + // We can't trim whitespace since the separator might be a tab + string spacesRemoved = line.Replace(" ", ""); + if (spacesRemoved.StartsWith("sep", StringComparison.OrdinalIgnoreCase)) { - var equals = line.Substring(3).Trim(); + var equals = spacesRemoved.Substring(3); + + // for compatibility with dotnet framework 2.0, this cannot be a char if (equals.StartsWith("=")) { - var separator = equals.Substring(1).Trim(); + var separator = equals.Substring(1); if (separator.Length > 1) { throw new Exception("Separator in 'sep=' line must be a single character"); diff --git a/src/CSVReader.cs b/src/CSVReader.cs index a03727a..d2e3ee2 100644 --- a/src/CSVReader.cs +++ b/src/CSVReader.cs @@ -345,7 +345,11 @@ public CSVReader(StreamReader source, CSVSettings settings = null) } } +#if NET2_0 || NET4_0 || NET4_5 + Headers = CSV.ParseLine(line, _settings) ?? new string[] {}; +#else Headers = CSV.ParseLine(line, _settings) ?? Array.Empty(); +#endif } else { @@ -382,7 +386,11 @@ public CSVReader(Stream source, CSVSettings settings = null) } } +#if NET2_0 || NET4_0 || NET4_5 + Headers = CSV.ParseLine(line, _settings) ?? new string[] {}; +#else Headers = CSV.ParseLine(line, _settings) ?? Array.Empty(); +#endif } else { diff --git a/src/CSVStateMachine.cs b/src/CSVStateMachine.cs index 0b97fcc..f92d083 100644 --- a/src/CSVStateMachine.cs +++ b/src/CSVStateMachine.cs @@ -65,6 +65,13 @@ public class CSVStateMachine /// public bool NeedsMoreText() { + // https://github.com/tspence/csharp-csv-reader/issues/68 + // If we're inside a text qualifier, we always need more text + if (_inTextQualifier) + { + return true; + } + return String.IsNullOrEmpty(_line) || _position + _settings.LineSeparator.Length >= _line.Length; } diff --git a/src/net40/src.net40.csproj b/src/net40/src.net40.csproj index e93e89d..0a7e138 100644 --- a/src/net40/src.net40.csproj +++ b/src/net40/src.net40.csproj @@ -34,6 +34,7 @@ + diff --git a/src/net50/src.net50.csproj b/src/net80/src.net80.csproj similarity index 93% rename from src/net50/src.net50.csproj rename to src/net80/src.net80.csproj index 6fb1ca0..ed1496b 100644 --- a/src/net50/src.net50.csproj +++ b/src/net80/src.net80.csproj @@ -4,7 +4,7 @@ CSVFile CSVFile true - net5.0 + net8.0 HAS_ASYNC;HAS_ASYNC_IENUM; diff --git a/tests/AsyncReaderTest.cs b/tests/AsyncReaderTest.cs index 7bcfdc0..9e4b3eb 100644 --- a/tests/AsyncReaderTest.cs +++ b/tests/AsyncReaderTest.cs @@ -66,7 +66,7 @@ public async Task TestBasicReader() Assert.AreEqual("x100", line[2]); break; default: - Assert.IsTrue(false, "Should not get here"); + Assert.Fail("Should not get here"); break; } @@ -139,7 +139,7 @@ public async Task TestDanglingFields() Assert.AreEqual("", line[3]); break; default: - Assert.IsTrue(false, "Should not get here"); + Assert.Fail("Should not get here"); break; } @@ -195,5 +195,61 @@ public async Task TestAlternateDelimiterQualifiers() } } } + + [Test] + public async Task TestChunking() + { + var source = "sep=\t\n" + + "Name\tTitle\tPhone\n" + + "JD\t\"Tallest doctor in the whole wide world\"\tx221\n" + + "Janitor\tJanitor\tx235\n" + + "\"Dr. Reed, " + Environment.NewLine + "Eliot\"\t\"Private \"\"Practice\"\"\"\tx236\n" + + "Dr. Kelso\tChief of Medicine\tx100"; + + // Convert into stream + var settings = new CSVSettings() { + AllowSepLine = true, + HeaderRowIncluded = true, + FieldDelimiter = '\t', + TextQualifier = '\"', + BufferSize = 10, + LineSeparator = "\n" + }; + using (var cr = CSVReader.FromString(source, settings)) + { + Assert.AreEqual("Name", cr.Headers[0]); + Assert.AreEqual("Title", cr.Headers[1]); + Assert.AreEqual("Phone", cr.Headers[2]); + var i = 1; + await foreach (var line in cr) + { + switch (i) + { + case 1: + Assert.AreEqual("JD", line[0]); + Assert.AreEqual("Tallest doctor in the whole wide world", line[1]); + Assert.AreEqual("x221", line[2]); + break; + case 2: + Assert.AreEqual("Janitor", line[0]); + Assert.AreEqual("Janitor", line[1]); + Assert.AreEqual("x235", line[2]); + break; + case 3: + Assert.AreEqual("Dr. Reed, " + Environment.NewLine + "Eliot", line[0]); + Assert.AreEqual("Private \"Practice\"", line[1]); + Assert.AreEqual("x236", line[2]); + break; + case 4: + Assert.AreEqual("Dr. Kelso", line[0]); + Assert.AreEqual("Chief of Medicine", line[1]); + Assert.AreEqual("x100", line[2]); + break; + } + + i++; + } + } + } } } diff --git a/tests/BasicParseTests.cs b/tests/BasicParseTests.cs index 6eea3ab..0185e28 100644 --- a/tests/BasicParseTests.cs +++ b/tests/BasicParseTests.cs @@ -187,6 +187,7 @@ public void ParseSepLineTest() Assert.AreEqual(null, CSV.ParseSepLine("sep=")); Assert.AreEqual(null, CSV.ParseSepLine("sep= ")); Assert.AreEqual(null, CSV.ParseSepLine("sep = ")); + Assert.AreEqual('\t', CSV.ParseSepLine("sep=\t")); Assert.Throws(() => { CSV.ParseSepLine("sep= this is a test since separators can't be more than a single character"); diff --git a/tests/net20/tests.net20.csproj b/tests/net20/tests.net20.csproj index d6a6e99..c31e0f0 100644 --- a/tests/net20/tests.net20.csproj +++ b/tests/net20/tests.net20.csproj @@ -44,6 +44,7 @@ + diff --git a/tests/net40/tests.net40.csproj b/tests/net40/tests.net40.csproj index e6219c8..8f30b31 100644 --- a/tests/net40/tests.net40.csproj +++ b/tests/net40/tests.net40.csproj @@ -41,6 +41,7 @@ + diff --git a/tests/net45/tests.net45.csproj b/tests/net45/tests.net45.csproj index 1bc8747..b23e492 100644 --- a/tests/net45/tests.net45.csproj +++ b/tests/net45/tests.net45.csproj @@ -41,6 +41,7 @@ + diff --git a/tests/net60/tests.net60.csproj b/tests/net60/tests.net60.csproj deleted file mode 100644 index 0645c8b..0000000 --- a/tests/net60/tests.net60.csproj +++ /dev/null @@ -1,43 +0,0 @@ - - - - CSVFile.Tests.net60 - CSVFile.Tests - false - net6.0 - NETSTANDARD2_0;HAS_ASYNC; - CS1591 - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - PackageAssets.csv - PreserveNewest - - - - - diff --git a/tests/net50/tests.net50.csproj b/tests/net80/tests.net80.csproj similarity index 89% rename from tests/net50/tests.net50.csproj rename to tests/net80/tests.net80.csproj index c22f8f5..85856cd 100644 --- a/tests/net50/tests.net50.csproj +++ b/tests/net80/tests.net80.csproj @@ -1,10 +1,10 @@  - CSVFile.Tests.net50 + CSVFile.Tests.net80 CSVFile.Tests false - net5.0 + net8.0 HAS_ASYNC;HAS_ASYNC_IENUM; CS1591 @@ -27,7 +27,7 @@ - +