Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}"
22 changes: 12 additions & 10 deletions .github/workflows/nuget-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 7 additions & 7 deletions CSVFile.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package >
<metadata>
<id>CSVFile</id>
<version>3.2.0</version>
<version>3.2.1</version>
<title>CSVFile</title>
<authors>Ted Spence</authors>
<owners>Ted Spence</owners>
Expand All @@ -13,21 +13,21 @@
<description>Tiny and fast CSV and TSV parsing library (40KB) with zero dependencies. Compatible with DotNetFramework (2.0 onwards) and DotNetCore.</description>
<icon>docs/icons8-spreadsheet-96.png</icon>
<releaseNotes>
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
</releaseNotes>
<readme>docs/README.md</readme>
<copyright>Copyright 2006 - 2024</copyright>
<copyright>Copyright 2006 - 2025</copyright>
<tags>fast csv parser serialization deserialization streaming async</tags>
<repository type="git" url="https://github.com/tspence/csharp-csv-reader" />
<dependencies>
<group targetFramework=".NETFramework2.0"/>
<group targetFramework=".NETFramework4.0"/>
<group targetFramework=".NETFramework4.5"/>
<group targetFramework=".NETStandard2.0"/>
<group targetFramework="net5.0"/>
<group targetFramework="net8.0"/>
</dependencies>
</metadata>
<files>
Expand All @@ -38,6 +38,6 @@
<file src="src\net40\bin\Release\*" target="lib\net40" />
<file src="src\net45\bin\Release\*" target="lib\net45" />
<file src="src\netstandard20\bin\Release\netstandard2.0\*" target="lib\netstandard20" />
<file src="src\net50\bin\Release\net5.0\*" target="lib\net5.0" />
<file src="src\net80\bin\Release\net8.0\*" target="lib\net8.0" />
</files>
</package>
11 changes: 2 additions & 9 deletions csharp-csv-reader.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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}
Expand Down
4 changes: 0 additions & 4 deletions package.cmd

This file was deleted.

10 changes: 7 additions & 3 deletions src/CSV.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
/// <summary>
/// Root class that contains static functions for straightforward CSV parsing
/// </summary>
public static class CSV

Check warning on line 29 in src/CSV.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Rename class 'CSV' to match pascal case naming rules, consider using 'Csv'. (https://rules.sonarsource.com/csharp/RSPEC-101)
{
/// <summary>
/// Use this to determine what version of DotNet was used to build this library
Expand Down Expand Up @@ -214,7 +214,7 @@
/// <param name="list">The array of objects to serialize</param>
/// <param name="settings">The CSV settings to use when exporting this array (Default: CSV)</param>
/// <returns>The completed CSV string representing one line per element in list</returns>
public static string Serialize<T>(IEnumerable<T> list, CSVSettings settings = null) where T : class, new()

Check warning on line 217 in src/CSV.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

All 'Serialize' method overloads should be adjacent. (https://rules.sonarsource.com/csharp/RSPEC-4136)
{
if (settings == null)
{
Expand Down Expand Up @@ -503,12 +503,16 @@
/// <returns>The separator</returns>
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");
Expand Down
8 changes: 8 additions & 0 deletions src/CSVReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@
/// A reader that reads from a stream and emits CSV records
/// </summary>
#if HAS_ASYNC_IENUM
public class CSVReader : IAsyncEnumerable<string[]>, IEnumerable<string[]>, IDisposable

Check warning on line 263 in src/CSVReader.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Fix this implementation of 'IDisposable' to conform to the dispose pattern. (https://rules.sonarsource.com/csharp/RSPEC-3881)

Check warning on line 263 in src/CSVReader.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Rename class 'CSVReader' to match pascal case naming rules, consider using 'CsvReader'. (https://rules.sonarsource.com/csharp/RSPEC-101)
#else
public class CSVReader : IEnumerable<string[]>, IDisposable
#endif
Expand Down Expand Up @@ -345,7 +345,11 @@
}
}

#if NET2_0 || NET4_0 || NET4_5
Headers = CSV.ParseLine(line, _settings) ?? new string[] {};
#else
Headers = CSV.ParseLine(line, _settings) ?? Array.Empty<string>();
#endif
}
else
{
Expand Down Expand Up @@ -382,7 +386,11 @@
}
}

#if NET2_0 || NET4_0 || NET4_5
Headers = CSV.ParseLine(line, _settings) ?? new string[] {};
#else
Headers = CSV.ParseLine(line, _settings) ?? Array.Empty<string>();
#endif
}
else
{
Expand Down
7 changes: 7 additions & 0 deletions src/CSVStateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
/// <summary>
/// The current state of CSV processing, given the text that has been seen so far
/// </summary>
public enum CSVState

Check warning on line 19 in src/CSVStateMachine.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Rename the enumeration 'CSVState' to match the regular expression: '^([A-Z]{1,3}[a-z0-9]+)*([A-Z]{2})?$'. (https://rules.sonarsource.com/csharp/RSPEC-2342)
{
/// <summary>
/// We have reached the end of the CSV and everything is done
Expand All @@ -43,7 +43,7 @@
/// Since some CSV files have a single row of data that comprises multiple lines, this state machine may or may
/// not produce one row of data for each chunk of text received.
/// </summary>
public class CSVStateMachine

Check warning on line 46 in src/CSVStateMachine.cs

View workflow job for this annotation

GitHub Actions / build-ubuntu

Rename class 'CSVStateMachine' to match pascal case naming rules, consider using 'CsvStateMachine'. (https://rules.sonarsource.com/csharp/RSPEC-101)
{
private readonly CSVSettings _settings;
private string _line;
Expand All @@ -65,6 +65,13 @@
/// <returns></returns>
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;
}

Expand Down
1 change: 1 addition & 0 deletions src/net40/src.net40.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
2 changes: 1 addition & 1 deletion src/net50/src.net50.csproj → src/net80/src.net80.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<AssemblyName>CSVFile</AssemblyName>
<RootNamespace>CSVFile</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<TargetFramework>net5.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<DefineConstants>HAS_ASYNC;HAS_ASYNC_IENUM;</DefineConstants>
</PropertyGroup>

Expand Down
60 changes: 58 additions & 2 deletions tests/AsyncReaderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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++;
}
}
}
}
}
1 change: 1 addition & 0 deletions tests/BasicParseTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Exception>(() =>
{
CSV.ParseSepLine("sep= this is a test since separators can't be more than a single character");
Expand Down
1 change: 1 addition & 0 deletions tests/net20/tests.net20.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
</Reference>
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
1 change: 1 addition & 0 deletions tests/net40/tests.net40.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
1 change: 1 addition & 0 deletions tests/net45/tests.net45.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Folder Include="Properties\" />
Expand Down
Loading
Loading