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 @@
-
+