Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Streamline bool.TryParse/Format #64782

Merged
merged 2 commits into from
Feb 7, 2022
Merged
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
95 changes: 44 additions & 51 deletions src/libraries/System.Private.CoreLib/src/System/Boolean.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
**
===========================================================*/

using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;

namespace System
Expand Down Expand Up @@ -99,10 +101,7 @@ public bool TryFormat(Span<char> destination, out int charsWritten)
{
if ((uint)destination.Length > 3) // uint cast, per https://github.com/dotnet/runtime/issues/10596
{
destination[0] = 'T';
destination[1] = 'r';
destination[2] = 'u';
destination[3] = 'e';
BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x65007500720054); // "True"
charsWritten = 4;
return true;
}
Expand All @@ -111,10 +110,7 @@ public bool TryFormat(Span<char> destination, out int charsWritten)
{
if ((uint)destination.Length > 4)
{
destination[0] = 'F';
destination[1] = 'a';
destination[2] = 'l';
destination[3] = 's';
BinaryPrimitives.WriteUInt64LittleEndian(MemoryMarshal.AsBytes(destination), 0x73006C00610046); // "Fals"
destination[4] = 'e';
charsWritten = 5;
return true;
Expand Down Expand Up @@ -191,24 +187,14 @@ public int CompareTo(bool value)

// Custom string compares for early application use by config switches, etc
//
internal static bool IsTrueStringIgnoreCase(ReadOnlySpan<char> value)
{
return value.Length == 4 &&
(value[0] == 't' || value[0] == 'T') &&
(value[1] == 'r' || value[1] == 'R') &&
(value[2] == 'u' || value[2] == 'U') &&
(value[3] == 'e' || value[3] == 'E');
}
internal static bool IsTrueStringIgnoreCase(ReadOnlySpan<char> value) =>
value.Length == 4 &&
(BinaryPrimitives.ReadUInt64LittleEndian(MemoryMarshal.AsBytes(value)) | 0x0020002000200020) == 0x65007500720074; // "true" as a ulong, each char |'d with 0x0020 for case-insensitivity

internal static bool IsFalseStringIgnoreCase(ReadOnlySpan<char> value)
{
return value.Length == 5 &&
(value[0] == 'f' || value[0] == 'F') &&
(value[1] == 'a' || value[1] == 'A') &&
(value[2] == 'l' || value[2] == 'L') &&
(value[3] == 's' || value[3] == 'S') &&
(value[4] == 'e' || value[4] == 'E');
}
internal static bool IsFalseStringIgnoreCase(ReadOnlySpan<char> value) =>
value.Length == 5 &&
(((BinaryPrimitives.ReadUInt64LittleEndian(MemoryMarshal.AsBytes(value)) | 0x0020002000200020) == 0x73006C00610066) & // "fals" as a ulong, each char |'d with 0x0020 for case-insensitivity
((value[4] | 0x20) == 'e'));

// Determines whether a String represents true or false.
//
Expand All @@ -223,19 +209,18 @@ public static bool Parse(ReadOnlySpan<char> value) =>

// Determines whether a String represents true or false.
//
public static bool TryParse([NotNullWhen(true)] string? value, out bool result)
{
if (value == null)
{
result = false;
return false;
}

return TryParse(value.AsSpan(), out result);
}
public static bool TryParse([NotNullWhen(true)] string? value, out bool result) =>
TryParse(value.AsSpan(), out result);

public static bool TryParse(ReadOnlySpan<char> value, out bool result)
{
// Boolean.{Try}Parse allows for optional whitespace/null values before and
// after the case-insensitive "true"/"false", but we don't expect those to
// be the common case. We check for "true"/"false" case-insensitive in the
// fast, inlined call path, and then only if neither match do we fall back
// to trimming and making a second post-trimming attempt at matching those
// same strings.

if (IsTrueStringIgnoreCase(value))
{
result = true;
Expand All @@ -248,33 +233,41 @@ public static bool TryParse(ReadOnlySpan<char> value, out bool result)
return true;
}

// Special case: Trim whitespace as well as null characters.
value = TrimWhiteSpaceAndNull(value);
return TryParseUncommon(value, out result);

if (IsTrueStringIgnoreCase(value))
static bool TryParseUncommon(ReadOnlySpan<char> value, out bool result)
{
result = true;
return true;
}
// With "true" being 4 characters, even if we trim something from <= 4 chars,
// it can't possibly match "true" or "false".
int originalLength = value.Length;
if (originalLength >= 5)
{
value = TrimWhiteSpaceAndNull(value);
if (value.Length != originalLength)
{
// Something was trimmed. Try matching again.
if (IsTrueStringIgnoreCase(value))
{
result = true;
return true;
}

result = false;
return IsFalseStringIgnoreCase(value);
}
}

if (IsFalseStringIgnoreCase(value))
{
result = false;
return true;
return false;
}

result = false;
return false;
}

private static ReadOnlySpan<char> TrimWhiteSpaceAndNull(ReadOnlySpan<char> value)
{
const char nullChar = (char)0x0000;

int start = 0;
while (start < value.Length)
{
if (!char.IsWhiteSpace(value[start]) && value[start] != nullChar)
if (!char.IsWhiteSpace(value[start]) && value[start] != '\0')
{
break;
}
Expand All @@ -284,7 +277,7 @@ private static ReadOnlySpan<char> TrimWhiteSpaceAndNull(ReadOnlySpan<char> value
int end = value.Length - 1;
while (end >= start)
{
if (!char.IsWhiteSpace(value[end]) && value[end] != nullChar)
if (!char.IsWhiteSpace(value[end]) && value[end] != '\0')
{
break;
}
Expand Down