Skip to content

Commit 7a33c09

Browse files
committed
Type converters switched to use ReadOnlySpan<char> instead of string.
1 parent f447015 commit 7a33c09

39 files changed

+398
-161
lines changed

src/CsvHelper/TypeConversion/ArrayConverter.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ public class ArrayConverter : IEnumerableConverter
1818
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
1919
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
2020
/// <returns>The object created from the string.</returns>
21-
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
21+
public override object? ConvertFromString(ReadOnlySpan<char> text, IReaderRow row, MemberMapData memberMapData)
2222
{
2323
Array array;
2424
var type = memberMapData.Member!.MemberType().GetElementType()!;

src/CsvHelper/TypeConversion/BigIntegerConverter.cs

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,15 @@ public override ReadOnlySpan<char> ConvertToString(object? value, IWriterRow row
2424
{
2525
if (value is BigInteger bi && memberMapData.TypeConverterOptions.Formats?.FirstOrDefault() == null)
2626
{
27-
var format = "R";
28-
2927
#if NET8_0_OR_GREATER
30-
if (bi.TryFormat(Buffer, out int charsWritten, format.AsSpan(), memberMapData.TypeConverterOptions.CultureInfo))
28+
Span<char> format = ['R'];
29+
if (bi.TryFormat(Buffer, out int charsWritten, format, memberMapData.TypeConverterOptions.CultureInfo))
3130
{
3231
return Buffer.AsSpan(0, charsWritten);
3332
}
33+
#else
34+
return bi.ToString("R", memberMapData.TypeConverterOptions.CultureInfo).AsSpan();
3435
#endif
35-
36-
return bi.ToString(format, memberMapData.TypeConverterOptions.CultureInfo).AsSpan();
3736
}
3837

3938
return base.ConvertToString(value, row, memberMapData);
@@ -50,14 +49,13 @@ public override ReadOnlySpan<char> ConvertToString(object? value, IWriterRow row
5049
{
5150
var numberStyle = memberMapData.TypeConverterOptions.NumberStyles ?? NumberStyles.Integer;
5251

52+
if (BigInteger.TryParse(
5353
#if NET8_0_OR_GREATER
54-
if (BigInteger.TryParse(text, numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out var bi))
55-
{
56-
return bi;
57-
}
54+
text
55+
#else
56+
text.ToString()
5857
#endif
59-
60-
if (BigInteger.TryParse(text.ToString(), numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out var bi))
58+
, numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out var bi))
6159
{
6260
return bi;
6361
}

src/CsvHelper/TypeConversion/BooleanConverter.cs

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -15,31 +15,24 @@ public class BooleanConverter : DefaultTypeConverter
1515
/// <inheritdoc/>
1616
public override object? ConvertFromString(ReadOnlySpan<char> text, IReaderRow row, MemberMapData memberMapData)
1717
{
18-
#if NET8_0_OR_GREATER
19-
if (bool.TryParse(text, out var b))
20-
{
21-
return b;
22-
}
23-
24-
if (short.TryParse(text, out var sh))
25-
{
26-
if (sh == 0)
27-
{
28-
return false;
29-
}
30-
if (sh == 1)
31-
{
32-
return true;
33-
}
34-
}
18+
if (bool.TryParse(
19+
#if NET8_0
20+
text
21+
#else
22+
text.ToString()
3523
#endif
36-
37-
if (bool.TryParse(text.ToString(), out var b))
24+
, out var b))
3825
{
3926
return b;
4027
}
4128

42-
if (short.TryParse(text.ToString(), out var sh))
29+
if (short.TryParse(
30+
#if NET8_0
31+
text
32+
#else
33+
text.ToString()
34+
#endif
35+
, out var sh))
4336
{
4437
if (sh == 0)
4538
{
@@ -51,18 +44,34 @@ public class BooleanConverter : DefaultTypeConverter
5144
}
5245
}
5346

54-
var t = (text ?? string.Empty).Trim();
47+
#if NET8_0_OR_GREATER
48+
var t = text.Trim();
49+
#else
50+
var t = text.ToString().Trim().ToString();
51+
#endif
5552
foreach (var trueValue in memberMapData.TypeConverterOptions.BooleanTrueValues)
5653
{
57-
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(trueValue, t, CompareOptions.IgnoreCase) == 0)
54+
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(
55+
#if NET8_0_OR_GREATER
56+
trueValue.AsSpan(), t
57+
#else
58+
trueValue, t.ToString()
59+
#endif
60+
, CompareOptions.IgnoreCase) == 0)
5861
{
5962
return true;
6063
}
6164
}
6265

6366
foreach (var falseValue in memberMapData.TypeConverterOptions.BooleanFalseValues)
6467
{
65-
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(falseValue, t, CompareOptions.IgnoreCase) == 0)
68+
if (memberMapData.TypeConverterOptions.CultureInfo!.CompareInfo.Compare(
69+
#if NET8_0_OR_GREATER
70+
falseValue.AsSpan(), t
71+
#else
72+
falseValue, t.ToString()
73+
#endif
74+
, CompareOptions.IgnoreCase) == 0)
6675
{
6776
return false;
6877
}
@@ -72,16 +81,16 @@ public class BooleanConverter : DefaultTypeConverter
7281
}
7382

7483
/// <inheritdoc/>
75-
public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
84+
public override ReadOnlySpan<char> ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
7685
{
7786
var b = value as bool?;
7887
if (b == true && memberMapData.TypeConverterOptions.BooleanTrueValues.Count > 0)
7988
{
80-
return memberMapData.TypeConverterOptions.BooleanTrueValues.First();
89+
return memberMapData.TypeConverterOptions.BooleanTrueValues.First().AsSpan();
8190
}
8291
else if (b == false && memberMapData.TypeConverterOptions.BooleanFalseValues.Count > 0)
8392
{
84-
return memberMapData.TypeConverterOptions.BooleanFalseValues.First();
93+
return memberMapData.TypeConverterOptions.BooleanFalseValues.First().AsSpan();
8594
}
8695

8796
return base.ConvertToString(value, row, memberMapData);

src/CsvHelper/TypeConversion/ByteArrayConverter.cs

Lines changed: 104 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// See LICENSE.txt for details or visit http://www.opensource.org/licenses/ms-pl.html for MS-PL and http://opensource.org/licenses/Apache-2.0 for Apache 2.0.
44
// https://github.com/JoshClose/CsvHelper
55
using CsvHelper.Configuration;
6+
using System.Globalization;
67
using System.Text;
78

89
namespace CsvHelper.TypeConversion;
@@ -12,6 +13,7 @@ namespace CsvHelper.TypeConversion;
1213
/// </summary>
1314
public class ByteArrayConverter : DefaultTypeConverter
1415
{
16+
private readonly char[] HexDigits = "0123456789ABCDEF".ToCharArray();
1517
private readonly ByteArrayConverterOptions options;
1618
private readonly string HexStringPrefix;
1719
private readonly byte ByteLength;
@@ -37,13 +39,29 @@ public ByteArrayConverter(ByteArrayConverterOptions options = ByteArrayConverter
3739
/// <param name="row">The <see cref="IWriterRow"/> for the current record.</param>
3840
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being written.</param>
3941
/// <returns>The string representation of the object.</returns>
40-
public override string? ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
42+
public override ReadOnlySpan<char> ConvertToString(object? value, IWriterRow row, MemberMapData memberMapData)
4143
{
4244
if (value is byte[] byteArray)
4345
{
44-
return (options & ByteArrayConverterOptions.Base64) == ByteArrayConverterOptions.Base64
45-
? Convert.ToBase64String(byteArray)
46-
: ByteArrayToHexString(byteArray);
46+
if ((options & ByteArrayConverterOptions.Base64) == ByteArrayConverterOptions.Base64)
47+
{
48+
#if NET8_0_OR_GREATER
49+
var length = GetBase64Length(byteArray);
50+
EnsureBufferSize(length);
51+
if (Convert.TryToBase64Chars(byteArray.AsSpan(), Buffer.AsSpan(), out int charsWritten))
52+
{
53+
return Buffer.AsSpan(0, charsWritten);
54+
}
55+
else
56+
{
57+
throw new InvalidOperationException("Failed to convert byte array to Base64 string.");
58+
}
59+
#else
60+
return Convert.ToBase64String(byteArray).AsSpan();
61+
#endif
62+
}
63+
64+
return BytesToHex(byteArray);
4765
}
4866

4967
return base.ConvertToString(value, row, memberMapData);
@@ -56,56 +74,110 @@ public ByteArrayConverter(ByteArrayConverterOptions options = ByteArrayConverter
5674
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
5775
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
5876
/// <returns>The object created from the string.</returns>
59-
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
77+
public override object? ConvertFromString(ReadOnlySpan<char> text, IReaderRow row, MemberMapData memberMapData)
6078
{
61-
if (text != null)
79+
if ((options & ByteArrayConverterOptions.Base64) == ByteArrayConverterOptions.Base64)
6280
{
63-
return (options & ByteArrayConverterOptions.Base64) == ByteArrayConverterOptions.Base64
64-
? Convert.FromBase64String(text)
65-
: HexStringToByteArray(text);
81+
#if NET8_0_OR_GREATER
82+
var length = (text.Length * 3) / 4;
83+
var buffer = new byte[length];
84+
var bufferSpan = new Span<byte>(buffer);
85+
if (Convert.TryFromBase64Chars(text, bufferSpan, out int bytesWritten))
86+
{
87+
return bufferSpan.Slice(0, bytesWritten).ToArray();
88+
}
89+
else
90+
{
91+
throw new FormatException("Invalid Base64 string.");
92+
}
93+
#else
94+
return Convert.FromBase64String(text.ToString());
95+
#endif
6696
}
6797

68-
return base.ConvertFromString(text, row, memberMapData);
98+
return HexToBytes(text);
6999
}
70100

71-
private string ByteArrayToHexString(byte[] byteArray)
101+
private ReadOnlySpan<char> BytesToHex(byte[] bytes)
72102
{
73-
var hexString = new StringBuilder();
74-
75-
if ((options & ByteArrayConverterOptions.HexInclude0x) == ByteArrayConverterOptions.HexInclude0x)
103+
if (bytes.Length == 0)
76104
{
77-
hexString.Append("0x");
105+
return ReadOnlySpan<char>.Empty;
78106
}
79107

80-
if (byteArray.Length >= 1)
108+
var length = bytes.Length * 2;
109+
if ((options & ByteArrayConverterOptions.HexInclude0x) == ByteArrayConverterOptions.HexInclude0x)
81110
{
82-
hexString.Append(byteArray[0].ToString("X2"));
111+
length += 2;
83112
}
84113

85-
for (var i = 1; i < byteArray.Length; i++)
114+
var result = new char[length];
115+
result[0] = '0';
116+
result[1] = 'x';
117+
118+
for (int i = 0; i < bytes.Length; i++)
86119
{
87-
hexString.Append(HexStringPrefix + byteArray[i].ToString("X2"));
120+
byte b = bytes[i];
121+
result[i * 2] = HexDigits[b >> 4]; // High nibble
122+
result[i * 2 + 1] = HexDigits[b & 0xF]; // Low nibble
88123
}
89124

90-
return hexString.ToString();
125+
return result;
91126
}
92127

93-
private byte[] HexStringToByteArray(string hex)
128+
private byte[] HexToBytes(ReadOnlySpan<char> hex)
94129
{
95-
var has0x = hex.StartsWith("0x");
130+
hex = hex.Trim();
96131

97-
var length = has0x
98-
? (hex.Length - 1) / ByteLength
99-
: hex.Length + 1 / ByteLength;
100-
var byteArray = new byte[length];
101-
var has0xOffset = has0x ? 1 : 0;
132+
Span<char> prefix = ['0', 'x'];
133+
if (hex.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
134+
{
135+
hex = hex.Slice(2);
136+
}
102137

103-
for (var stringIndex = has0xOffset * 2; stringIndex < hex.Length; stringIndex += ByteLength)
138+
if (hex.IsEmpty)
104139
{
105-
byteArray[(stringIndex - has0xOffset) / ByteLength] = Convert.ToByte(hex.Substring(stringIndex, 2), 16);
140+
return Array.Empty<byte>();
106141
}
107142

108-
return byteArray;
143+
if (hex.Length % 2 != 0)
144+
{
145+
throw new ArgumentException("Hex string must have an even number of characters.", nameof(hex));
146+
}
147+
148+
var result = new byte[hex.Length / 2];
149+
150+
for (int i = 0; i < hex.Length; i += 2)
151+
{
152+
var hexPair = hex.Slice(i, 2);
153+
154+
try
155+
{
156+
result[i / 2] = byte.Parse(
157+
#if NET8_0_OR_GREATER
158+
hexPair
159+
#else
160+
hexPair.ToString()
161+
#endif
162+
, NumberStyles.HexNumber);
163+
}
164+
catch (FormatException)
165+
{
166+
throw new FormatException($"Invalid hex characters at position {i}: '{hexPair.ToString()}'.");
167+
}
168+
}
169+
170+
return result;
171+
}
172+
173+
private int GetBase64Length(byte[] bytes)
174+
{
175+
if (bytes == null || bytes.Length == 0)
176+
{
177+
return 0;
178+
}
179+
180+
return ((bytes.Length + 2) / 3) * 4;
109181
}
110182

111183
private void ValidateOptions()
@@ -121,4 +193,6 @@ private void ValidateOptions()
121193
}
122194
}
123195
}
196+
197+
private
124198
}

src/CsvHelper/TypeConversion/ByteConverter.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,21 @@ public class ByteConverter : DefaultTypeConverter
1919
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
2020
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
2121
/// <returns>The object created from the string.</returns>
22-
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
22+
public override object? ConvertFromString(ReadOnlySpan<char> text, IReaderRow row, MemberMapData memberMapData)
2323
{
2424
var numberStyle = memberMapData.TypeConverterOptions.NumberStyles ?? NumberStyles.Integer;
2525

26-
if (byte.TryParse(text, numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out var b))
26+
#if NET8_0_OR_GREATER
27+
if (byte.TryParse(text, numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out byte result))
28+
{
29+
return result;
30+
}
31+
#else
32+
if (byte.TryParse(text.ToString(), numberStyle, memberMapData.TypeConverterOptions.CultureInfo, out var b))
2733
{
2834
return b;
2935
}
36+
#endif
3037

3138
return base.ConvertFromString(text, row, memberMapData);
3239
}

src/CsvHelper/TypeConversion/CharConverter.cs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,24 @@ public class CharConverter : DefaultTypeConverter
1818
/// <param name="row">The <see cref="IReaderRow"/> for the current record.</param>
1919
/// <param name="memberMapData">The <see cref="MemberMapData"/> for the member being created.</param>
2020
/// <returns>The object created from the string.</returns>
21-
public override object? ConvertFromString(string? text, IReaderRow row, MemberMapData memberMapData)
21+
public override object? ConvertFromString(ReadOnlySpan<char> text, IReaderRow row, MemberMapData memberMapData)
2222
{
23-
if (text != null && text.Length > 1)
23+
if (text.Length > 1)
2424
{
2525
text = text.Trim();
2626
}
2727

28-
if (char.TryParse(text, out var c))
28+
#if NET8_0_OR_GREATER
29+
if (text.Length == 1)
30+
{
31+
return text[0];
32+
}
33+
#else
34+
if (char.TryParse(text.ToString(), out var c))
2935
{
3036
return c;
3137
}
38+
#endif
3239

3340
return base.ConvertFromString(text, row, memberMapData);
3441
}

0 commit comments

Comments
 (0)