From eebe2832d1f48f1dc9ce630020b1e4eaefe00e9e Mon Sep 17 00:00:00 2001 From: Tanner Gooding Date: Wed, 1 Mar 2023 06:30:17 -0800 Subject: [PATCH] Have bool and string implement ISpanParsable --- .../src/System/Boolean.cs | 23 +++- .../src/System/String.cs | 53 +++++++- .../System.Runtime/ref/System.Runtime.cs | 12 +- .../tests/System.Runtime.Tests.csproj | 7 +- .../tests/System/BooleanTests.GenericMath.cs | 89 ++++++++++++ .../tests/System/BooleanTests.cs | 2 +- .../tests/System/StringTests.GenericMath.cs | 127 ++++++++++++++++++ 7 files changed, 304 insertions(+), 9 deletions(-) create mode 100644 src/libraries/System.Runtime/tests/System/BooleanTests.GenericMath.cs create mode 100644 src/libraries/System.Runtime/tests/System/StringTests.GenericMath.cs diff --git a/src/libraries/System.Private.CoreLib/src/System/Boolean.cs b/src/libraries/System.Private.CoreLib/src/System/Boolean.cs index cbfaebf66dfa7..144157c259402 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Boolean.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Boolean.cs @@ -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, IEquatable + public readonly struct Boolean + : IComparable, + IConvertible, + IComparable, + IEquatable, + ISpanParsable { // // Member Variables @@ -397,5 +402,21 @@ object IConvertible.ToType(Type type, IFormatProvider? provider) { return Convert.DefaultToType((IConvertible)this, type, provider); } + + // + // IParsable + // + + static bool IParsable.Parse(string s, IFormatProvider? provider) => Parse(s); + + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out bool result) => TryParse(s, out result); + + // + // ISpanParsable + // + + static bool ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out bool result) => TryParse(s, out result); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/String.cs b/src/libraries/System.Private.CoreLib/src/System/String.cs index 3a3c973b66325..0f2ba393e1faa 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.cs @@ -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, IComparable, IEquatable, ICloneable + public sealed partial class String + : IComparable, + IEnumerable, + IConvertible, + IEnumerable, + IComparable, + IEquatable, + ICloneable, + ISpanParsable { /// Maximum length allowed for a string. /// Keep in sync with AllocateString in gchelpers.cpp. @@ -735,5 +743,48 @@ public int Length [Intrinsic] get => _stringLength; } + + // + // IParsable + // + + static string IParsable.Parse(string s, IFormatProvider? provider) + { + ArgumentNullException.ThrowIfNull(s); + return s; + } + + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out string result) + { + result = s; + return s is not null; + } + + // + // ISpanParsable + // + + static string ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) + { + if (s.Length > MaxLength) + { + ThrowHelper.ThrowFormatInvalidString(); + } + return s.ToString(); + } + + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, [MaybeNullWhen(returnValue: false)] out string result) + { + if (s.Length <= MaxLength) + { + result = s.ToString(); + return true; + } + else + { + result = null; + return false; + } + } } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 2de21393bbc2b..1ebd788c7e834 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -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, System.IConvertible, System.IEquatable + public readonly partial struct Boolean : System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IParsable, System.ISpanParsable { private readonly bool _dummyPrimitive; public static readonly string FalseString; @@ -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.Parse(string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out bool result) { throw null; } + static bool System.ISpanParsable.Parse(ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(ReadOnlySpan 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 destination, out int charsWritten) { throw null; } @@ -4970,7 +4974,7 @@ public sealed partial class STAThreadAttribute : System.Attribute { public STAThreadAttribute() { } } - public sealed partial class String : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable, System.ICloneable, System.IComparable, System.IComparable, System.IConvertible, System.IEquatable + public sealed partial class String : System.Collections.Generic.IEnumerable, System.Collections.IEnumerable, System.ICloneable, System.IComparable, System.IComparable, System.IConvertible, System.IEquatable, System.IParsable, System.ISpanParsable { public static readonly string Empty; [System.CLSCompliantAttribute(false)] @@ -5152,6 +5156,10 @@ public void CopyTo(System.Span 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.Parse(string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.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.Parse(ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(ReadOnlySpan 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; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index e6c2e676370fc..d1182a207c8d6 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -63,8 +63,10 @@ + + @@ -333,10 +335,7 @@ - + diff --git a/src/libraries/System.Runtime/tests/System/BooleanTests.GenericMath.cs b/src/libraries/System.Runtime/tests/System/BooleanTests.GenericMath.cs new file mode 100644 index 0000000000000..283ea7d70d0ca --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/BooleanTests.GenericMath.cs @@ -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.TryParse(value, provider: null, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, ParsableHelper.Parse(value, provider: null)); + + // Current Culture + Assert.True(ParsableHelper.TryParse(value, provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, ParsableHelper.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.TryParse(value, provider: null, out result)); + Assert.Equal(default(bool), result); + Assert.Throws(exceptionType, () => ParsableHelper.Parse(value, provider: null)); + + // Current Culture + Assert.False(ParsableHelper.TryParse(value, provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(default(bool), result); + Assert.Throws(exceptionType, () => ParsableHelper.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.TryParse(value.AsSpan(offset, count), provider: null, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, SpanParsableHelper.Parse(value.AsSpan(offset, count), provider: null)); + + // Current Culture + Assert.True(SpanParsableHelper.TryParse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, SpanParsableHelper.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.TryParse(value.AsSpan(), provider: null, out result)); + Assert.Equal(default(bool), result); + Assert.Throws(exceptionType, () => SpanParsableHelper.Parse(value.AsSpan(), provider: null)); + + // Current Culture + Assert.False(SpanParsableHelper.TryParse(value.AsSpan(), provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(default(bool), result); + Assert.Throws(exceptionType, () => SpanParsableHelper.Parse(value.AsSpan(), provider: CultureInfo.CurrentCulture)); + } + } +} diff --git a/src/libraries/System.Runtime/tests/System/BooleanTests.cs b/src/libraries/System.Runtime/tests/System/BooleanTests.cs index c0594ffcc0fa7..18ef70e5e22f0 100644 --- a/src/libraries/System.Runtime/tests/System/BooleanTests.cs +++ b/src/libraries/System.Runtime/tests/System/BooleanTests.cs @@ -63,7 +63,7 @@ public static IEnumerable 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))] diff --git a/src/libraries/System.Runtime/tests/System/StringTests.GenericMath.cs b/src/libraries/System.Runtime/tests/System/StringTests.GenericMath.cs new file mode 100644 index 0000000000000..b5e162a20851e --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/StringTests.GenericMath.cs @@ -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 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 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 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.TryParse(value, provider: null, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, ParsableHelper.Parse(value, provider: null)); + + // Current Culture + Assert.True(ParsableHelper.TryParse(value, provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, ParsableHelper.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.TryParse(value, provider: null, out result)); + Assert.Equal(default(string), result); + Assert.Throws(exceptionType, () => ParsableHelper.Parse(value, provider: null)); + + // Current Culture + Assert.False(ParsableHelper.TryParse(value, provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(default(string), result); + Assert.Throws(exceptionType, () => ParsableHelper.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.TryParse(value.AsSpan(offset, count), provider: null, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, SpanParsableHelper.Parse(value.AsSpan(offset, count), provider: null)); + + // Current Culture + Assert.True(SpanParsableHelper.TryParse(value.AsSpan(offset, count), provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(expected, result); + Assert.Equal(expected, SpanParsableHelper.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.TryParse(value.AsSpan(), provider: null, out result)); + Assert.Equal(default(string), result); + Assert.Throws(exceptionType, () => SpanParsableHelper.Parse(value.AsSpan(), provider: null)); + + // Current Culture + Assert.False(SpanParsableHelper.TryParse(value.AsSpan(), provider: CultureInfo.CurrentCulture, out result)); + Assert.Equal(default(string), result); + Assert.Throws(exceptionType, () => SpanParsableHelper.Parse(value.AsSpan(), provider: CultureInfo.CurrentCulture)); + } + } +}