Skip to content

Commit

Permalink
Have bool and string implement ISpanParsable<T> (#82836)
Browse files Browse the repository at this point in the history
  • Loading branch information
tannergooding authored Mar 2, 2023
1 parent e0e2345 commit 6e422d6
Show file tree
Hide file tree
Showing 7 changed files with 304 additions and 9 deletions.
23 changes: 22 additions & 1 deletion src/libraries/System.Private.CoreLib/src/System/Boolean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ namespace System
{
[Serializable]
[TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public readonly struct Boolean : IComparable, IConvertible, IComparable<bool>, IEquatable<bool>
public readonly struct Boolean
: IComparable,
IConvertible,
IComparable<bool>,
IEquatable<bool>,
ISpanParsable<bool>
{
//
// Member Variables
Expand Down Expand Up @@ -397,5 +402,21 @@ object IConvertible.ToType(Type type, IFormatProvider? provider)
{
return Convert.DefaultToType((IConvertible)this, type, provider);
}

//
// IParsable
//

static bool IParsable<bool>.Parse(string s, IFormatProvider? provider) => Parse(s);

static bool IParsable<bool>.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out bool result) => TryParse(s, out result);

//
// ISpanParsable
//

static bool ISpanParsable<bool>.Parse(ReadOnlySpan<char> s, IFormatProvider? provider) => Parse(s);

static bool ISpanParsable<bool>.TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, out bool result) => TryParse(s, out result);
}
}
53 changes: 52 additions & 1 deletion src/libraries/System.Private.CoreLib/src/System/String.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ namespace System
[Serializable]
[NonVersionable] // This only applies to field layout
[System.Runtime.CompilerServices.TypeForwardedFrom("mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
public sealed partial class String : IComparable, IEnumerable, IConvertible, IEnumerable<char>, IComparable<string?>, IEquatable<string?>, ICloneable
public sealed partial class String
: IComparable,
IEnumerable,
IConvertible,
IEnumerable<char>,
IComparable<string?>,
IEquatable<string?>,
ICloneable,
ISpanParsable<string>
{
/// <summary>Maximum length allowed for a string.</summary>
/// <remarks>Keep in sync with AllocateString in gchelpers.cpp.</remarks>
Expand Down Expand Up @@ -735,5 +743,48 @@ public int Length
[Intrinsic]
get => _stringLength;
}

//
// IParsable
//

static string IParsable<string>.Parse(string s, IFormatProvider? provider)
{
ArgumentNullException.ThrowIfNull(s);
return s;
}

static bool IParsable<string>.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out string result)
{
result = s;
return s is not null;
}

//
// ISpanParsable
//

static string ISpanParsable<string>.Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
{
if (s.Length > MaxLength)
{
ThrowHelper.ThrowFormatInvalidString();
}
return s.ToString();
}

static bool ISpanParsable<string>.TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out string result)
{
if (s.Length <= MaxLength)
{
result = s.ToString();
return true;
}
else
{
result = null;
return false;
}
}
}
}
12 changes: 10 additions & 2 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ public static partial class BitConverter
[System.CLSCompliantAttribute(false)]
public static double UInt64BitsToDouble(ulong value) { throw null; }
}
public readonly partial struct Boolean : System.IComparable, System.IComparable<bool>, System.IConvertible, System.IEquatable<bool>
public readonly partial struct Boolean : System.IComparable, System.IComparable<bool>, System.IConvertible, System.IEquatable<bool>, System.IParsable<bool>, System.ISpanParsable<bool>
{
private readonly bool _dummyPrimitive;
public static readonly string FalseString;
Expand Down Expand Up @@ -703,6 +703,10 @@ public static partial class BitConverter
ushort System.IConvertible.ToUInt16(System.IFormatProvider? provider) { throw null; }
uint System.IConvertible.ToUInt32(System.IFormatProvider? provider) { throw null; }
ulong System.IConvertible.ToUInt64(System.IFormatProvider? provider) { throw null; }
static bool System.IParsable<bool>.Parse(string s, System.IFormatProvider? provider) { throw null; }
static bool System.IParsable<bool>.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out bool result) { throw null; }
static bool System.ISpanParsable<bool>.Parse(ReadOnlySpan<char> s, System.IFormatProvider? provider) { throw null; }
static bool System.ISpanParsable<bool>.TryParse(ReadOnlySpan<char> s, System.IFormatProvider? provider, out bool result) { throw null; }
public override string ToString() { throw null; }
public string ToString(System.IFormatProvider? provider) { throw null; }
public bool TryFormat(System.Span<char> destination, out int charsWritten) { throw null; }
Expand Down Expand Up @@ -4971,7 +4975,7 @@ public sealed partial class STAThreadAttribute : System.Attribute
{
public STAThreadAttribute() { }
}
public sealed partial class String : System.Collections.Generic.IEnumerable<char>, System.Collections.IEnumerable, System.ICloneable, System.IComparable, System.IComparable<string?>, System.IConvertible, System.IEquatable<string?>
public sealed partial class String : System.Collections.Generic.IEnumerable<char>, System.Collections.IEnumerable, System.ICloneable, System.IComparable, System.IComparable<string?>, System.IConvertible, System.IEquatable<string?>, System.IParsable<string>, System.ISpanParsable<string>
{
public static readonly string Empty;
[System.CLSCompliantAttribute(false)]
Expand Down Expand Up @@ -5153,6 +5157,10 @@ public void CopyTo(System.Span<char> destination) { }
ushort System.IConvertible.ToUInt16(System.IFormatProvider? provider) { throw null; }
uint System.IConvertible.ToUInt32(System.IFormatProvider? provider) { throw null; }
ulong System.IConvertible.ToUInt64(System.IFormatProvider? provider) { throw null; }
static string System.IParsable<string>.Parse(string s, System.IFormatProvider? provider) { throw null; }
static bool System.IParsable<string>.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out string result) { throw null; }
static string System.ISpanParsable<string>.Parse(ReadOnlySpan<char> s, System.IFormatProvider? provider) { throw null; }
static bool System.ISpanParsable<string>.TryParse(ReadOnlySpan<char> s, System.IFormatProvider? provider, [System.Diagnostics.CodeAnalysis.MaybeNullWhenAttribute(false)] out string result) { throw null; }
public char[] ToCharArray() { throw null; }
public char[] ToCharArray(int startIndex, int length) { throw null; }
public System.String ToLower() { throw null; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,10 @@
<Compile Include="System\Attributes.cs" />
<Compile Include="System\AttributeUsageAttributeTests.cs" />
<Compile Include="System\BadImageFormatExceptionTests.cs" />
<Compile Include="System\StringTests.GenericMath.cs" />
<Compile Include="System\BooleanTests.cs" />
<Compile Include="System\BufferTests.cs" />
<Compile Include="System\BooleanTests.GenericMath.cs" />
<Compile Include="System\ByteTests.cs" />
<Compile Include="System\CharTests.cs" />
<Compile Include="System\CLSCompliantAttributeTests.cs" />
Expand Down Expand Up @@ -336,10 +338,7 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj"
ReferenceOutputAssembly="false"
SetTargetFramework="TargetFramework=netstandard2.0"
OutputItemType="Analyzer" />
<ProjectReference Include="$(LibrariesProjectRoot)System.Text.RegularExpressions\gen\System.Text.RegularExpressions.Generator.csproj" ReferenceOutputAssembly="false" SetTargetFramework="TargetFramework=netstandard2.0" OutputItemType="Analyzer" />

<PackageReference Include="Moq" Version="$(MoqVersion)" />
<PackageReference Include="System.Runtime.Numerics.TestData" Version="$(SystemRuntimeNumericsTestDataVersion)" GeneratePathProperty="true" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using Xunit;

namespace System.Tests
{
public class BooleanTests_GenericMath
{
//
// IParsable and ISpanParsable
//

[Theory]
[MemberData(nameof(BooleanTests.Parse_Valid_TestData), MemberType = typeof(BooleanTests))]
public static void ParseValidStringTest(string value, bool expected)
{
bool result;

// Default
Assert.True(ParsableHelper<bool>.TryParse(value, provider: null, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, ParsableHelper<bool>.Parse(value, provider: null));

// Current Culture
Assert.True(ParsableHelper<bool>.TryParse(value, provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, ParsableHelper<bool>.Parse(value, provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(BooleanTests.Parse_Invalid_TestData), MemberType = typeof(BooleanTests))]
public static void ParseInvalidStringTest(string value, Type exceptionType)
{
bool result;

// Default
Assert.False(ParsableHelper<bool>.TryParse(value, provider: null, out result));
Assert.Equal(default(bool), result);
Assert.Throws(exceptionType, () => ParsableHelper<bool>.Parse(value, provider: null));

// Current Culture
Assert.False(ParsableHelper<bool>.TryParse(value, provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(default(bool), result);
Assert.Throws(exceptionType, () => ParsableHelper<bool>.Parse(value, provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(BooleanTests.Parse_ValidWithOffsetCount_TestData), MemberType = typeof(BooleanTests))]
public static void ParseValidSpanTest(string value, int offset, int count, bool expected)
{
bool result;

// Default
Assert.True(SpanParsableHelper<bool>.TryParse(value.AsSpan(offset, count), provider: null, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, SpanParsableHelper<bool>.Parse(value.AsSpan(offset, count), provider: null));

// Current Culture
Assert.True(SpanParsableHelper<bool>.TryParse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, SpanParsableHelper<bool>.Parse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(BooleanTests.Parse_Invalid_TestData), MemberType = typeof(BooleanTests))]
public static void ParseInvalidSpanTest(string value, Type exceptionType)
{
if (value is null)
{
// null and empty span are treated the same
return;
}

bool result;

// Default
Assert.False(SpanParsableHelper<bool>.TryParse(value.AsSpan(), provider: null, out result));
Assert.Equal(default(bool), result);
Assert.Throws(exceptionType, () => SpanParsableHelper<bool>.Parse(value.AsSpan(), provider: null));

// Current Culture
Assert.False(SpanParsableHelper<bool>.TryParse(value.AsSpan(), provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(default(bool), result);
Assert.Throws(exceptionType, () => SpanParsableHelper<bool>.Parse(value.AsSpan(), provider: CultureInfo.CurrentCulture));
}
}
}
2 changes: 1 addition & 1 deletion src/libraries/System.Runtime/tests/System/BooleanTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public static IEnumerable<object[]> Parse_Invalid_TestData()
yield return new object[] { "T", typeof(FormatException) };
yield return new object[] { "0", typeof(FormatException) };
yield return new object[] { "1", typeof(FormatException) };
}
}

[Theory]
[MemberData(nameof(Parse_Invalid_TestData))]
Expand Down
127 changes: 127 additions & 0 deletions src/libraries/System.Runtime/tests/System/StringTests.GenericMath.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Globalization;
using Xunit;

namespace System.Tests
{
public class StringTests_GenericMath
{
public static IEnumerable<object[]> Parse_Valid_TestData()
{
yield return new object[] { "Hello" };
yield return new object[] { "hello" };
yield return new object[] { "HELLO" };
yield return new object[] { "hElLo" };
yield return new object[] { " Hello " };
yield return new object[] { "Hello\0" };
yield return new object[] { " \0 \0 Hello \0 " };
yield return new object[] { "World" };
yield return new object[] { "world" };
yield return new object[] { "WORLD" };
yield return new object[] { "wOrLd" };
yield return new object[] { "World " };
yield return new object[] { "World\0" };
yield return new object[] { " World \0\0\0 " };
}

public static IEnumerable<object[]> Parse_Invalid_TestData()
{
yield return new object[] { null, typeof(ArgumentNullException) };
// We cannot easily test inputs that exceed `string.MaxLength` without risk of OOM
}

public static IEnumerable<object[]> Parse_ValidWithOffsetCount_TestData()
{
foreach (object[] inputs in Parse_Valid_TestData())
{
yield return new object[] { inputs[0], 0, ((string)inputs[0]).Length, inputs[0] };
}

yield return new object[] { " \0 \0 Hello, World! \0 ", 6, 5, "Hello" };
yield return new object[] { " \0 \0 Hello, World! \0 ", 13, 5, "World" };
yield return new object[] { " \0 \0 Hello, World! \0 ", 6, 13, "Hello, World!" };
}

//
// IParsable and ISpanParsable
//

[Theory]
[MemberData(nameof(StringTests_GenericMath.Parse_Valid_TestData), MemberType = typeof(StringTests_GenericMath))]
public static void ParseValidStringTest(string value)
{
string result;
string expected = value;

// Default
Assert.True(ParsableHelper<string>.TryParse(value, provider: null, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, ParsableHelper<string>.Parse(value, provider: null));

// Current Culture
Assert.True(ParsableHelper<string>.TryParse(value, provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, ParsableHelper<string>.Parse(value, provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(StringTests_GenericMath.Parse_Invalid_TestData), MemberType = typeof(StringTests_GenericMath))]
public static void ParseInvalidStringTest(string value, Type exceptionType)
{
string result;

// Default
Assert.False(ParsableHelper<string>.TryParse(value, provider: null, out result));
Assert.Equal(default(string), result);
Assert.Throws(exceptionType, () => ParsableHelper<string>.Parse(value, provider: null));

// Current Culture
Assert.False(ParsableHelper<string>.TryParse(value, provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(default(string), result);
Assert.Throws(exceptionType, () => ParsableHelper<string>.Parse(value, provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(StringTests_GenericMath.Parse_ValidWithOffsetCount_TestData), MemberType = typeof(StringTests_GenericMath))]
public static void ParseValidSpanTest(string value, int offset, int count, string expected)
{
string result;

// Default
Assert.True(SpanParsableHelper<string>.TryParse(value.AsSpan(offset, count), provider: null, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, SpanParsableHelper<string>.Parse(value.AsSpan(offset, count), provider: null));

// Current Culture
Assert.True(SpanParsableHelper<string>.TryParse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(expected, result);
Assert.Equal(expected, SpanParsableHelper<string>.Parse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture));
}

[Theory]
[MemberData(nameof(StringTests_GenericMath.Parse_Invalid_TestData), MemberType = typeof(StringTests_GenericMath))]
public static void ParseInvalidSpanTest(string value, Type exceptionType)
{
if (value is null)
{
// null and empty span are treated the same
return;
}

string result;

// Default
Assert.False(SpanParsableHelper<string>.TryParse(value.AsSpan(), provider: null, out result));
Assert.Equal(default(string), result);
Assert.Throws(exceptionType, () => SpanParsableHelper<string>.Parse(value.AsSpan(), provider: null));

// Current Culture
Assert.False(SpanParsableHelper<string>.TryParse(value.AsSpan(), provider: CultureInfo.CurrentCulture, out result));
Assert.Equal(default(string), result);
Assert.Throws(exceptionType, () => SpanParsableHelper<string>.Parse(value.AsSpan(), provider: CultureInfo.CurrentCulture));
}
}
}

0 comments on commit 6e422d6

Please sign in to comment.