Skip to content

Commit b9acbb7

Browse files
committed
Expose GetFieldSpan on IParser
1 parent 9848510 commit b9acbb7

File tree

8 files changed

+205
-141
lines changed

8 files changed

+205
-141
lines changed

src/CsvHelper/CsvParser.cs

Lines changed: 113 additions & 117 deletions
Large diffs are not rendered by default.

src/CsvHelper/CsvReader.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
using System.Globalization;
1515
using System.Runtime.CompilerServices;
1616
using System.Threading;
17-
using System.Configuration;
1817

1918
namespace CsvHelper
2019
{

src/CsvHelper/FieldCache.cs

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Diagnostics;
88
using System.Linq;
99
using System.Runtime.CompilerServices;
10+
using System.Runtime.InteropServices;
1011
using System.Text;
1112
using System.Threading.Tasks;
1213

@@ -34,26 +35,26 @@ public FieldCache(int initialSize = 128, int maxFieldSize = 128)
3435
entries = new Entry[size];
3536
}
3637

37-
public string GetField(char[] buffer, int start, int length)
38+
public string GetField(ReadOnlySpan<char> buffer)
3839
{
39-
if (length == 0)
40+
if (buffer.IsEmpty)
4041
{
4142
return string.Empty;
4243
}
4344

44-
if (length > maxFieldSize)
45+
if (buffer.Length > maxFieldSize)
4546
{
46-
return new string(buffer, start, length);
47+
return buffer.ToString();
4748
}
4849

49-
var hashCode = GetHashCode(buffer, start, length);
50+
var hashCode = GetHashCode(buffer);
5051
ref var bucket = ref GetBucket(hashCode);
5152
int i = bucket - 1;
5253
while ((uint)i < (uint)entries.Length)
5354
{
5455
ref var entry = ref entries[i];
5556

56-
if (entry.HashCode == hashCode && entry.Value.AsSpan().SequenceEqual(new Span<char>(buffer, start, length)))
57+
if (entry.HashCode == hashCode && entry.Value.AsSpan().SequenceEqual(buffer))
5758
{
5859
return entry.Value;
5960
}
@@ -70,25 +71,31 @@ public string GetField(char[] buffer, int start, int length)
7071
ref var reference = ref entries[count];
7172
reference.HashCode = hashCode;
7273
reference.Next = bucket - 1;
73-
reference.Value = new string(buffer, start, length);
74+
reference.Value = buffer.ToString();
7475
bucket = count + 1;
7576
count++;
7677

7778
return reference.Value;
7879
}
7980

8081
[MethodImpl(MethodImplOptions.AggressiveInlining)]
81-
private uint GetHashCode(char[] buffer, int start, int length)
82+
private static uint GetHashCode(ReadOnlySpan<char> buffer)
8283
{
8384
unchecked
8485
{
86+
#if NET6_0_OR_GREATER
87+
HashCode hash = new();
88+
hash.AddBytes(MemoryMarshal.AsBytes(buffer));
89+
return (uint)hash.ToHashCode();
90+
#else
8591
uint hash = 17;
86-
for (var i = start; i < start + length; i++)
92+
foreach (char c in buffer)
8793
{
88-
hash = hash * 31 + buffer[i];
94+
hash = hash * 31 + c;
8995
}
9096

9197
return hash;
98+
#endif
9299
}
93100
}
94101

src/CsvHelper/IParser.cs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,21 @@ public interface IParser : IDisposable
3333
/// <summary>
3434
/// Gets the field at the specified index for the current row.
3535
/// </summary>
36-
/// <param name="index">The index.</param>
37-
/// <returns>The field.</returns>
36+
/// <param name="index">The index of the field in the current row.</param>
37+
/// <returns>A <see cref="string"/> representing the field at the specified index in the current row.</returns>
3838
string this[int index] { get; }
3939

40+
/// <summary>
41+
/// Gets a span over the field at the specified index in the current row.
42+
/// </summary>
43+
/// <param name="index">The index of the field in the current row.</param>
44+
/// <returns>A span representing the field at the specified index in the current row.</returns>
45+
ReadOnlySpan<char> GetFieldSpan(int index)
46+
#if NET
47+
=> this[index]
48+
#endif
49+
;
50+
4051
/// <summary>
4152
/// Gets the record for the current row. Note:
4253
/// It is much more efficient to only get the fields you need. If
@@ -49,6 +60,16 @@ public interface IParser : IDisposable
4960
/// </summary>
5061
string RawRecord { get; }
5162

63+
/// <summary>
64+
/// Gets a span over the raw record for the current row.
65+
/// </summary>
66+
ReadOnlySpan<char> RawRecordSpan
67+
#if NET
68+
=> RawRecord;
69+
#else
70+
{ get; }
71+
#endif
72+
5273
/// <summary>
5374
/// Gets the CSV row the parser is currently on.
5475
/// </summary>

tests/CsvHelper.Tests/CsvParserTests.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1366,5 +1366,18 @@ public void RawRowCountWithSingleLineAndNoLineEndingTest()
13661366
Assert.Equal(1, parser.RawRow);
13671367
}
13681368
}
1369+
1370+
[Theory]
1371+
[InlineData(-1)]
1372+
[InlineData(2)]
1373+
public void Parser_IndexOutOfRangeException(int index)
1374+
{
1375+
using (var reader = new StringReader("1,2\r\n"))
1376+
using (var parser = new CsvParser(reader, CultureInfo.InvariantCulture))
1377+
{
1378+
Assert.True(parser.Read());
1379+
Assert.Throws<IndexOutOfRangeException>(() => parser[index]);
1380+
}
1381+
}
13691382
}
13701383
}

tests/CsvHelper.Tests/Mocks/ParserMock.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public class ParserMock : IParser, IEnumerable<string[]>
2929

3030
public string RawRecord => string.Empty;
3131

32+
public ReadOnlySpan<char> RawRecordSpan => ReadOnlySpan<char>.Empty;
33+
3234
public int Row => row;
3335

3436
public int RawRow => row;
@@ -41,6 +43,8 @@ public class ParserMock : IParser, IEnumerable<string[]>
4143

4244
public string this[int index] => record[index];
4345

46+
public ReadOnlySpan<char> GetFieldSpan(int index) => record[index];
47+
4448
public ParserMock() : this(new CsvConfiguration(CultureInfo.InvariantCulture)) { }
4549

4650
public ParserMock(CsvConfiguration configuration)

tests/CsvHelper.Tests/Parsing/BadDataTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,5 +149,36 @@ public void Read_AccessingParserRecordInBadDataFound_ThrowsParserException()
149149

150150
Assert.Throws<ParserException>(() => parser[1]);
151151
}
152+
153+
[Fact]
154+
public void ConsecutiveBadDataTest()
155+
{
156+
int badDataCount = 0;
157+
var config = new CsvConfiguration(CultureInfo.InvariantCulture)
158+
{
159+
BadDataFound = _ => badDataCount++,
160+
CacheFields = false,
161+
ProcessFieldBufferSize = 4
162+
};
163+
// These 3 fields each use the processFieldBuffer.
164+
// The test is to ensure consistency of the fields during a read,
165+
// i.e. the memory that each field points to is not overwritten
166+
// during the processing of the other fields in the same row.
167+
string csv = "\"\"\"\",\"two\" \"2,\"three\" \"3\r\n"; // """","two" "2,"three" "3
168+
using (var reader = new StringReader(csv))
169+
using (var parser = new CsvParser(reader, config))
170+
{
171+
Assert.True(parser.Read());
172+
173+
Assert.Equal(3, parser.Count);
174+
Assert.Equal("\"", parser.GetFieldSpan(0).ToString());
175+
Assert.Equal("two \"2", parser.GetFieldSpan(1).ToString());
176+
Assert.Equal("three \"3", parser.GetFieldSpan(2).ToString());
177+
Assert.Equal("two \"2", parser.GetFieldSpan(1).ToString());
178+
Assert.Equal("\"", parser.GetFieldSpan(0).ToString());
179+
180+
Assert.False(parser.Read());
181+
}
182+
}
152183
}
153184
}

tests/CsvHelper.Tests/Parsing/FieldCacheTests.cs

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -64,30 +64,23 @@ public void Read_WithFieldCacheDisabled_ReturnsDifferentFieldInstance()
6464
[Fact]
6565
public void Test1()
6666
{
67-
// "542008", "27721116", "98000820" have hash code 3769566006
68-
6967
var value1 = "542008";
7068
var value2 = "27721116";
7169
var value3 = "98000820";
7270
var value4 = "542008";
7371

7472
var cache = new FieldCache(1);
7573

76-
var field1 = cache.GetField(value1.ToCharArray(), 0, value1.Length);
77-
var field2 = cache.GetField(value2.ToCharArray(), 0, value2.Length);
78-
var field3 = cache.GetField(value3.ToCharArray(), 0, value3.Length);
79-
var field4 = cache.GetField(value4.ToCharArray(), 0, value4.Length);
74+
var field1 = cache.GetField(value1);
75+
var field2 = cache.GetField(value2);
76+
var field3 = cache.GetField(value3);
77+
var field4 = cache.GetField(value4);
8078

8179
Assert.Equal(value1, field1);
8280
Assert.Equal(value2, field2);
8381
Assert.Equal(value3, field3);
8482
Assert.Equal(value4, field4);
8583

86-
Assert.NotSame(value1, field1);
87-
Assert.NotSame(value2, field2);
88-
Assert.NotSame(value3, field3);
89-
Assert.NotSame(value4, field4);
90-
9184
Assert.Same(field1, field4);
9285
}
9386
}

0 commit comments

Comments
 (0)