internal static extern unsafe int IndexOf(IntPtr sortHandle, char* target, int cwTargetLength, char* pSource, int cwSourceLength, CompareOptions options, int* matchLengthPtr);
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_LastIndexOf")]
- internal static extern unsafe int LastIndexOf(IntPtr sortHandle, char* target, int cwTargetLength, char* pSource, int cwSourceLength, CompareOptions options);
+ internal static extern unsafe int LastIndexOf(IntPtr sortHandle, char* target, int cwTargetLength, char* pSource, int cwSourceLength, CompareOptions options, int* matchLengthPtr);
[DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_IndexOfOrdinalIgnoreCase")]
internal static extern unsafe int IndexOfOrdinalIgnoreCase(string target, int cwTargetLength, char* pSource, int cwSourceLength, bool findLast);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
internal static extern int LocaleNameToLCID(string lpName, uint dwFlags);
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern int LCMapStringEx(
string? lpLocaleName,
uint dwMapFlags,
Assert.Equal(s1.GetHashCode(), s1.GetHashCode());
}
- Assert.Equal(expected, s1.AsSpan().Equals(s2.AsSpan(), comparisonType));
+ if (string.IsNullOrEmpty(s1) && string.IsNullOrEmpty(s2))
+ {
+ // null strings are normalized to empty spans
+ Assert.True(s1.AsSpan().Equals(s2.AsSpan(), comparisonType));
+ }
+ else
+ {
+ Assert.Equal(expected, s1.AsSpan().Equals(s2.AsSpan(), comparisonType));
+ }
}
public static IEnumerable<object[]> Equals_EncyclopaediaData()
Assert.Equal(expected, source.EndsWith(end, ignoreCase, ci));
}
+ [Theory]
+ [InlineData("", StringComparison.InvariantCulture, true)]
+ [InlineData("", StringComparison.Ordinal, true)]
+ [InlineData(ZeroWidthJoiner, StringComparison.InvariantCulture, true)]
+ [InlineData(ZeroWidthJoiner, StringComparison.Ordinal, false)]
+ public static void StartEndWith_ZeroWeightValue(string value, StringComparison comparison, bool expectedStartsAndEndsWithResult)
+ {
+ Assert.Equal(expectedStartsAndEndsWithResult, string.Empty.StartsWith(value, comparison));
+ Assert.Equal(expectedStartsAndEndsWithResult, string.Empty.EndsWith(value, comparison));
+ Assert.Equal(expectedStartsAndEndsWithResult ? 0 : -1, string.Empty.IndexOf(value, comparison));
+ Assert.Equal(expectedStartsAndEndsWithResult ? 0 : -1, string.Empty.LastIndexOf(value, comparison));
+ }
+
[Fact]
public static void StartEndNegativeTest()
{
if (U_SUCCESS(err))
{
+ // Workaround for https://unicode-org.atlassian.net/projects/ICU/issues/ICU-9396
+ // The ucol_strcoll routine on some older versions of ICU doesn't correctly
+ // handle nullptr inputs. We'll play defensively and always flow a non-nullptr.
+
+ UChar dummyChar = 0;
+ if (lpStr1 == NULL)
+ {
+ lpStr1 = &dummyChar;
+ }
+ if (lpStr2 == NULL)
+ {
+ lpStr2 = &dummyChar;
+ }
+
result = ucol_strcoll(pColl, lpStr1, cwStr1Length, lpStr2, cwStr2Length);
}
int32_t options,
int32_t* pMatchedLength)
{
+ assert(cwTargetLength > 0);
+
int32_t result = USEARCH_DONE;
+
+ // It's possible somebody passed us (source = <empty>, target = <non-empty>).
+ // ICU's usearch_* APIs don't handle empty source inputs properly. However,
+ // if this occurs the user really just wanted us to perform an equality check.
+ // We can't short-circuit the operation because depending on the collation in
+ // use, certain code points may have zero weight, which means that empty
+ // strings may compare as equal to non-empty strings.
+
+ if (cwSourceLength == 0)
+ {
+ result = GlobalizationNative_CompareString(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options);
+ if (result == UCOL_EQUAL && pMatchedLength != NULL)
+ {
+ *pMatchedLength = cwTargetLength;
+ }
+
+ return (result == UCOL_EQUAL) ? 0 : -1;
+ }
+
UErrorCode err = U_ZERO_ERROR;
const UCollator* pColl = GetCollatorFromSortHandle(pSortHandle, options, &err);
int32_t cwTargetLength,
const UChar* lpSource,
int32_t cwSourceLength,
- int32_t options)
+ int32_t options,
+ int32_t* pMatchedLength)
{
+ assert(cwTargetLength > 0);
+
int32_t result = USEARCH_DONE;
+
+ // It's possible somebody passed us (source = <empty>, target = <non-empty>).
+ // ICU's usearch_* APIs don't handle empty source inputs properly. However,
+ // if this occurs the user really just wanted us to perform an equality check.
+ // We can't short-circuit the operation because depending on the collation in
+ // use, certain code points may have zero weight, which means that empty
+ // strings may compare as equal to non-empty strings.
+
+ if (cwSourceLength == 0)
+ {
+ result = GlobalizationNative_CompareString(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options);
+ if (result == UCOL_EQUAL && pMatchedLength != NULL)
+ {
+ *pMatchedLength = cwTargetLength;
+ }
+
+ return (result == UCOL_EQUAL) ? 0 : -1;
+ }
+
UErrorCode err = U_ZERO_ERROR;
const UCollator* pColl = GetCollatorFromSortHandle(pSortHandle, options, &err);
if (U_SUCCESS(err))
{
result = usearch_last(pSearch, &err);
+
+ // if the search was successful,
+ // we'll try to get the matched string length.
+ if (result != USEARCH_DONE && pMatchedLength != NULL)
+ {
+ *pMatchedLength = usearch_getMatchedLength(pSearch);
+ }
usearch_close(pSearch);
}
}
int32_t idx = usearch_last(pSearch, pErrorCode);
if (idx != USEARCH_DONE)
{
- if ((idx + usearch_getMatchedLength(pSearch)) == patternLength)
+ int32_t matchEnd = idx + usearch_getMatchedLength(pSearch);
+ assert(matchEnd <= textLength);
+
+ if (matchEnd == textLength)
{
result = TRUE;
}
else
{
- int32_t matchEnd = idx + usearch_getMatchedLength(pSearch);
- int32_t remainingStringLength = patternLength - matchEnd;
+ int32_t remainingStringLength = textLength - matchEnd;
result = CanIgnoreAllCollationElements(pCollator, pText + matchEnd, remainingStringLength);
}
int32_t cwTargetLength,
const UChar* lpSource,
int32_t cwSourceLength,
- int32_t options);
+ int32_t options,
+ int32_t* pMatchedLength);
PALEXPORT int32_t GlobalizationNative_IndexOfOrdinalIgnoreCase(const UChar* lpTarget,
int32_t cwTargetLength,
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
using Xunit;
yield return new object[] { s_invariantCompare, "Test's", null, CompareOptions.None, 1 };
yield return new object[] { s_invariantCompare, null, null, CompareOptions.None, 0 };
+ yield return new object[] { s_invariantCompare, "", "Tests", CompareOptions.None, -1 };
+ yield return new object[] { s_invariantCompare, "Tests", "", CompareOptions.None, 1 };
+
+ yield return new object[] { s_invariantCompare, null, "", CompareOptions.None, -1 };
+ yield return new object[] { s_invariantCompare, "", null, CompareOptions.None, 1 };
+ yield return new object[] { s_invariantCompare, "", "", CompareOptions.None, 0 };
+
yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5555), CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "foobar", "FooB\u00C0R", CompareOptions.IgnoreNonSpace | CompareOptions.IgnoreCase, 0 };
yield return new object[] { s_invariantCompare, "foobar", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, -1 };
// Use Compare(string, int, int, string, int, int, CompareOptions)
Assert.Equal(expected, Math.Sign(compareInfo.Compare(string1, offset1, length1, string2, offset2, length2, options)));
Assert.Equal(-expected, Math.Sign(compareInfo.Compare(string2, offset2, length2, string1, offset1, length1, options)));
+
+ // Now test the span-based versions - use BoundedMemory to detect buffer overruns
+ // We can't run this test for null inputs since they implicitly convert to empty span
+
+ if (string1 != null && string2 != null)
+ {
+ RunSpanCompareTest(compareInfo, string1.AsSpan(offset1, length1), string2.AsSpan(offset2, length2), options, expected);
+ }
+
+ static void RunSpanCompareTest(CompareInfo compareInfo, ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options, int expected)
+ {
+ using BoundedMemory<char> string1BoundedMemory = BoundedMemory.AllocateFromExistingData(string1);
+ string1BoundedMemory.MakeReadonly();
+
+ using BoundedMemory<char> string2BoundedMemory = BoundedMemory.AllocateFromExistingData(string2);
+ string2BoundedMemory.MakeReadonly();
+
+ Assert.Equal(expected, Math.Sign(compareInfo.Compare(string1, string2, options)));
+ Assert.Equal(-expected, Math.Sign(compareInfo.Compare(string2, string1, options)));
+ }
}
[Fact]
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
+using System.Text;
using Xunit;
namespace System.Globalization.Tests
yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 0, 11, CompareOptions.IgnoreNonSpace, 4 };
yield return new object[] { s_invariantCompare, "o\u0308", "o", 0, 2, CompareOptions.None, -1 };
+ // Weightless characters
+ yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0 };
+ yield return new object[] { s_invariantCompare, "hello", "\u200d", 1, 3, CompareOptions.IgnoreCase, 1 };
+
// Ignore symbols
yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5 };
yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.None, -1 };
// Use int MemoryExtensions.IndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
Assert.Equal((expected == -1) ? -1 : (expected - startIndex), source.AsSpan(startIndex, count).IndexOf(value.AsSpan(), stringComparison));
}
- }
+
+ // Now test the span-based versions - use BoundedMemory to detect buffer overruns
+
+ RunSpanIndexOfTest(compareInfo, source.AsSpan(startIndex, count), value, options, (expected < 0) ? expected : expected - startIndex);
+
+ static void RunSpanIndexOfTest(CompareInfo compareInfo, ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options, int expected)
+ {
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source);
+ sourceBoundedMemory.MakeReadonly();
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value);
+ valueBoundedMemory.MakeReadonly();
+
+ Assert.Equal(expected, compareInfo.IndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
+
+ if (TryCreateRuneFrom(value, out Rune rune))
+ {
+ Assert.Equal(expected, compareInfo.IndexOf(sourceBoundedMemory.Span, rune, options)); // try the Rune-based version
+ }
+ }
+ }
private static void IndexOf_Char(CompareInfo compareInfo, string source, char value, int startIndex, int count, CompareOptions options, int expected)
{
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.IndexOf("Test", 'a', 2, 4, CompareOptions.None));
}
- [Fact]
- public static void IndexOf_MinusOneCompatability()
+ // Attempts to create a Rune from the entirety of a given text buffer.
+ private static bool TryCreateRuneFrom(ReadOnlySpan<char> text, out Rune value)
{
- // This behavior was for .NET Framework 1.1 compatability.
- // Allowing empty source strings with invalid offsets was quickly outed.
- // with invalid offsets.
- Assert.Equal(0, s_invariantCompare.IndexOf("", "", -1, CompareOptions.None));
- Assert.Equal(-1, s_invariantCompare.IndexOf("", "a", -1, CompareOptions.None));
+ return Rune.DecodeFromUtf16(text, out value, out int charsConsumed) == OperationStatus.Done
+ && charsConsumed == text.Length;
}
}
}
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
using Xunit;
yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, true };
yield return new object[] { s_invariantCompare, "o\u0000\u0308", "o", CompareOptions.None, true };
+ // Weightless comparisons
+ yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true };
+
// Surrogates
yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.None, true };
yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.IgnoreCase, true };
Assert.Equal(expected, source.StartsWith(value, stringComparison));
Assert.Equal(expected, source.AsSpan().StartsWith(value.AsSpan(), stringComparison));
}
+
+ // Now test the span version - use BoundedMemory to detect buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+ valueBoundedMemory.MakeReadonly();
+
+ Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
}
[Fact]
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
using Xunit;
yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.None, true };
yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.Ordinal, true };
+ // Weightless comparisons
+ yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true };
+
// Surrogates
yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.None, true };
yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.IgnoreCase, true };
Assert.Equal(expected, source.EndsWith(value, stringComparison));
Assert.Equal(expected, source.AsSpan().EndsWith(value.AsSpan(), stringComparison));
}
+
+ // Now test the span version - use BoundedMemory to detect buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(value);
+ valueBoundedMemory.MakeReadonly();
+
+ Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
}
[Fact]
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
+using System.Text;
using Xunit;
namespace System.Globalization.Tests
public static IEnumerable<object[]> LastIndexOf_TestData()
{
+ bool useNls = PlatformDetection.IsNlsGlobalization;
+
// Empty strings
yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3 };
yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 };
yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 10, 11, CompareOptions.IgnoreNonSpace, 4 };
yield return new object[] { s_invariantCompare, "o\u0308", "o", 1, 2, CompareOptions.None, -1 };
+ // Weightless characters
+ // NLS matches weightless characters at the end of the string
+ // ICU matches weightless characters at 1 index prior to the end of the string
+ yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0 };
+ yield return new object[] { s_invariantCompare, "", "\u200d", -1, 0, CompareOptions.None, 0 };
+ yield return new object[] { s_invariantCompare, "hello", "\u200d", 4, 5, CompareOptions.IgnoreCase, useNls ? 5 : 4 };
+
// Ignore symbols
yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5 };
yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.None, -1 };
// Use int MemoryExtensions.LastIndexOf(this ReadOnlySpan<char>, ReadOnlySpan<char>, StringComparison)
Assert.Equal(expected - adjustmentFactor, sourceSpan.LastIndexOf(value.AsSpan(), stringComparison));
}
+
+ // Now test the span-based versions - use BoundedMemory to detect buffer overruns
+
+ RunSpanLastIndexOfTest(compareInfo, sourceSpan, value, options, expected - adjustmentFactor);
+
+ static void RunSpanLastIndexOfTest(CompareInfo compareInfo, ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options, int expected)
+ {
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source);
+ sourceBoundedMemory.MakeReadonly();
+
+ using BoundedMemory<char> valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value);
+ valueBoundedMemory.MakeReadonly();
+
+ Assert.Equal(expected, compareInfo.LastIndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options));
+
+ if (TryCreateRuneFrom(value, out Rune rune))
+ {
+ Assert.Equal(expected, compareInfo.LastIndexOf(sourceBoundedMemory.Span, rune, options)); // try the Rune-based version
+ }
+ }
}
private static void LastIndexOf_Char(CompareInfo compareInfo, string source, char value, int startIndex, int count, CompareOptions options, int expected)
// Options are invalid
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.StringSort));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.StringSort));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 2, CompareOptions.StringSort));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, CompareOptions.StringSort));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.StringSort));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.StringSort));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 2, CompareOptions.StringSort));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.StringSort));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 2, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 2, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.Ordinal | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 2, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 2, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)(-1)));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, (CompareOptions)(-1)));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 2, (CompareOptions)(-1)));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, (CompareOptions)(-1)));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)(-1)));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, (CompareOptions)(-1)));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 2, (CompareOptions)(-1)));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, (CompareOptions)(-1)));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)0x11111111));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, (CompareOptions)0x11111111));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 2, (CompareOptions)0x11111111));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, 1, (CompareOptions)0x11111111));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', (CompareOptions)0x11111111));
AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, (CompareOptions)0x11111111));
- AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 2, (CompareOptions)0x11111111));
+ AssertExtensions.Throws<ArgumentException>("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, (CompareOptions)0x11111111));
// StartIndex < 0
AssertExtensions.Throws<ArgumentOutOfRangeException>("startIndex", () => s_invariantCompare.LastIndexOf("Test", "Test", -1, CompareOptions.None));
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", "s", 4, 7, CompareOptions.None));
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", 's', 4, 6));
AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", 's', 4, 7, CompareOptions.None));
+
+ // Count > StartIndex + 1
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", "e", 1, 3));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", "e", 1, 3, CompareOptions.None));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", 'e', 1, 3));
+ AssertExtensions.Throws<ArgumentOutOfRangeException>("count", () => s_invariantCompare.LastIndexOf("Test", 'e', 1, 3, CompareOptions.None));
+ }
+
+ // Attempts to create a Rune from the entirety of a given text buffer.
+ private static bool TryCreateRuneFrom(ReadOnlySpan<char> text, out Rune value)
+ {
+ return Rune.DecodeFromUtf16(text, out value, out int charsConsumed) == OperationStatus.Done
+ && charsConsumed == text.Length;
}
}
}
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
+using System.Buffers;
using System.Collections.Generic;
using System.Reflection;
+using System.Text;
using Xunit;
namespace System.Globalization.Tests
// sort before the corresponding characters that are in the block U+FF00-U+FFEF
private static int s_expectedHalfToFullFormsComparison = PlatformDetection.IsNlsGlobalization ? -1 : 1;
+ private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo;
private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo;
private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo;
Assert.Equal(string1, sk1.OriginalString);
Assert.Equal(string2, sk2.OriginalString);
+
+ // Now try the span-based versions - use BoundedMemory to detect buffer overruns
+
+ RunSpanSortKeyTest(compareInfo, string1, options, sk1.KeyData);
+ RunSpanSortKeyTest(compareInfo, string2, options, sk2.KeyData);
+
+ unsafe static void RunSpanSortKeyTest(CompareInfo compareInfo, ReadOnlySpan<char> source, CompareOptions options, byte[] expectedSortKey)
+ {
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source);
+ sourceBoundedMemory.MakeReadonly();
+
+ Assert.Equal(expectedSortKey.Length, compareInfo.GetSortKeyLength(sourceBoundedMemory.Span, options));
+
+ using BoundedMemory<byte> sortKeyBoundedMemory = BoundedMemory.Allocate<byte>(expectedSortKey.Length);
+
+ // First try with a destination which is too small - should result in an error
+
+ Assert.Throws<ArgumentException>("destination", () => compareInfo.GetSortKey(sourceBoundedMemory.Span, sortKeyBoundedMemory.Span.Slice(1), options));
+
+ // Next, try with a destination which is perfectly sized - should succeed
+
+ Span<byte> sortKeyBoundedSpan = sortKeyBoundedMemory.Span;
+ sortKeyBoundedSpan.Clear();
+
+ Assert.Equal(expectedSortKey.Length, compareInfo.GetSortKey(sourceBoundedMemory.Span, sortKeyBoundedSpan, options));
+ Assert.Equal(expectedSortKey, sortKeyBoundedSpan[0..expectedSortKey.Length].ToArray());
+ }
}
[Fact]
string source = sourceObj as string ?? new string((char[])sourceObj);
Assert.Equal(expected, CompareInfo.IsSortable(source));
+ // Now test the span version - use BoundedMemory to detect buffer overruns
+
+ using BoundedMemory<char> sourceBoundedMemory = BoundedMemory.AllocateFromExistingData<char>(source);
+ sourceBoundedMemory.MakeReadonly();
+ Assert.Equal(expected, CompareInfo.IsSortable(sourceBoundedMemory.Span));
+
// If the string as a whole is sortable, then all chars which aren't standalone
// surrogate halves must also be sortable.
<PropertyGroup>
<TargetFrameworks>$(NetCoreAppCurrent)</TargetFrameworks>
<TestRuntime>true</TestRuntime>
+ <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Include="InvariantMode.cs" />
// See the LICENSE file in the project root for more information.
using System.Buffers;
+using System.Buffers.Binary;
using System.Collections.Generic;
-using System.Collections;
+using System.IO;
+using System.Runtime.InteropServices;
using System.Text;
using Xunit;
yield return new object[] { "FooBar", "Foo\u0400Bar", 0, 6, CompareOptions.Ordinal, -1 };
yield return new object[] { "TestFooBA\u0300R", "FooB\u00C0R", 0, 11, CompareOptions.IgnoreNonSpace, -1 };
+ // Weightless characters
+ yield return new object[] { "", "\u200d", 0, 0, CompareOptions.None, -1 };
+ yield return new object[] { "hello", "\u200d", 0, 5, CompareOptions.IgnoreCase, -1 };
+
// Ignore symbols
yield return new object[] { "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, -1 };
yield return new object[] { "More Test's", "Tests", 0, 11, CompareOptions.None, -1 };
yield return new object[] { "FooBar", "Foo\u0400Bar", 5, 6, CompareOptions.Ordinal, -1 };
yield return new object[] { "TestFooBA\u0300R", "FooB\u00C0R", 10, 11, CompareOptions.IgnoreNonSpace, -1 };
+ // Weightless characters
+ yield return new object[] { "", "\u200d", 0, 0, CompareOptions.None, -1 };
+ yield return new object[] { "", "\u200d", -1, 0, CompareOptions.None, -1 };
+ yield return new object[] { "hello", "\u200d", 4, 5, CompareOptions.IgnoreCase, -1 };
+
// Ignore symbols
yield return new object[] { "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, -1 };
yield return new object[] { "More Test's", "Tests", 10, 11, CompareOptions.None, -1 };
yield return new object[] { "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false };
yield return new object[] { "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, false };
+ // Weightless characters
+ yield return new object[] { "", "\u200d", CompareOptions.None, false };
+ yield return new object[] { "", "\u200d", CompareOptions.IgnoreCase, false };
+
// Ignore symbols
yield return new object[] { "More Test's", "Tests", CompareOptions.IgnoreSymbols, false };
yield return new object[] { "More Test's", "Tests", CompareOptions.None, false };
Assert.Equal(version, new CultureInfo(cultureName).CompareInfo.Version);
}
+ [Theory]
+ [InlineData(0, 0)]
+ [InlineData(1, 2)]
+ [InlineData(100_000, 200_000)]
+ [InlineData(0x3FFF_FFFF, 0x7FFF_FFFE)]
+ public void TestGetSortKeyLength_Valid(int inputLength, int expectedSortKeyLength)
+ {
+ using BoundedMemory<char> boundedMemory = BoundedMemory.Allocate<char>(0); // AV if dereferenced
+ boundedMemory.MakeReadonly();
+ ReadOnlySpan<char> dummySpan = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(boundedMemory.Span), inputLength);
+ Assert.Equal(expectedSortKeyLength, CultureInfo.InvariantCulture.CompareInfo.GetSortKeyLength(dummySpan));
+ }
+
+ [Theory]
+ [InlineData(0x4000_0000)]
+ [InlineData(int.MaxValue)]
+ public unsafe void TestGetSortKeyLength_OverlongArgument(int inputLength)
+ {
+ using BoundedMemory<char> boundedMemory = BoundedMemory.Allocate<char>(0); // AV if dereferenced
+ boundedMemory.MakeReadonly();
+
+ Assert.Throws<ArgumentException>("source", () =>
+ {
+ ReadOnlySpan<char> dummySpan = MemoryMarshal.CreateReadOnlySpan(ref MemoryMarshal.GetReference(boundedMemory.Span), inputLength);
+ CultureInfo.InvariantCulture.CompareInfo.GetSortKeyLength(dummySpan);
+ });
+ }
+
+ [Theory]
+ [InlineData("Hello", CompareOptions.None, "Hello")]
+ [InlineData("Hello", CompareOptions.IgnoreWidth, "Hello")]
+ [InlineData("Hello", CompareOptions.IgnoreCase, "HELLO")]
+ [InlineData("Hello", CompareOptions.IgnoreCase | CompareOptions.IgnoreWidth, "HELLO")]
+ [InlineData("Hell\u00F6", CompareOptions.None, "Hell\u00F6")] // U+00F6 = LATIN SMALL LETTER O WITH DIAERESIS
+ [InlineData("Hell\u00F6", CompareOptions.IgnoreCase, "HELL\u00F6")] // note the final "o with diaeresis" isn't capitalized
+ public unsafe void TestSortKey_FromSpan(string input, CompareOptions options, string expected)
+ {
+ byte[] expectedOutputBytes = GetExpectedInvariantOrdinalSortKey(expected);
+
+ CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;
+
+ // First, validate that too short a buffer throws
+
+ Assert.Throws<ArgumentException>("destination", () => compareInfo.GetSortKey(input, new byte[expectedOutputBytes.Length - 1], options));
+
+ // Next, validate that using a properly-sized buffer succeeds
+ // We'll use BoundedMemory to check for buffer overruns
+
+ using BoundedMemory<char> boundedInputMemory = BoundedMemory.AllocateFromExistingData<char>(input);
+ boundedInputMemory.MakeReadonly();
+ ReadOnlySpan<char> boundedInputSpan = boundedInputMemory.Span;
+
+ using BoundedMemory<byte> boundedOutputMemory = BoundedMemory.Allocate<byte>(expectedOutputBytes.Length);
+ Span<byte> boundedOutputSpan = boundedOutputMemory.Span;
+
+ Assert.Equal(expectedOutputBytes.Length, compareInfo.GetSortKey(boundedInputSpan, boundedOutputSpan, options));
+ Assert.Equal(expectedOutputBytes, boundedOutputSpan[0..expectedOutputBytes.Length].ToArray());
+
+ // Now try it once more, passing a larger span where the last byte points to unallocated memory.
+ // If GetSortKey attempts to write beyond the number of bytes we expect, the unit test will AV.
+
+ boundedOutputSpan.Clear();
+
+ fixed (byte* pBoundedOutputSpan = boundedOutputSpan)
+ {
+ boundedOutputSpan = new Span<byte>(pBoundedOutputSpan, boundedOutputSpan.Length + 1); // last byte is unallocated memory
+ Assert.Equal(expectedOutputBytes.Length, compareInfo.GetSortKey(boundedInputSpan, boundedOutputSpan, options));
+ Assert.Equal(expectedOutputBytes, boundedOutputSpan[0..expectedOutputBytes.Length].ToArray());
+ }
+ }
+
[Fact]
public void TestSortKey_ZeroWeightCodePoints()
{
valueBoundedMemory.MakeReadonly();
ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+ Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsSuffix(sourceBoundedSpan, valueBoundedSpan, options));
Assert.Equal(result, sourceBoundedSpan.EndsWith(valueBoundedSpan, GetStringComparison(options)));
}
}
valueBoundedMemory.MakeReadonly();
ReadOnlySpan<char> valueBoundedSpan = valueBoundedMemory.Span;
+ res = CultureInfo.GetCultureInfo(cul).CompareInfo.Compare(sourceBoundedSpan, valueBoundedSpan, options);
+ Assert.Equal(result, Math.Sign(res));
+
res = sourceBoundedSpan.CompareTo(valueBoundedSpan, GetStringComparison(options));
Assert.Equal(result, Math.Sign(res));
}
Assert.Equal(expectedToLower, Rune.ToLowerInvariant(originalRune).Value);
Assert.Equal(expectedToLower, Rune.ToLower(originalRune, CultureInfo.GetCultureInfo("tr-TR")).Value);
}
+
+ private static byte[] GetExpectedInvariantOrdinalSortKey(ReadOnlySpan<char> input)
+ {
+ MemoryStream memoryStream = new MemoryStream();
+ Span<byte> tempBuffer = stackalloc byte[sizeof(char)];
+
+ foreach (char ch in input)
+ {
+ BinaryPrimitives.WriteUInt16BigEndian(tempBuffer, (ushort)ch);
+ memoryStream.Write(tempBuffer);
+ }
+
+ return memoryStream.ToArray();
+ }
}
}
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;
+using System.Text;
namespace System.Globalization
{
}
}
- // TODO https://github.com/dotnet/runtime/issues/8890:
- // This method shouldn't be necessary, as we should be able to just use the overload
- // that takes two spans. But due to this issue, that's adding significant overhead.
- private unsafe int IcuCompareString(ReadOnlySpan<char> string1, string string2, CompareOptions options)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!GlobalizationMode.UseNls);
- Debug.Assert(string2 != null);
- Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
-
- // Unlike NLS, ICU (ucol_getSortKey) allows passing nullptr for either of the source arguments
- // as long as the corresponding length parameter is 0.
-
- fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
- fixed (char* pString2 = &string2.GetRawStringData())
- {
- return Interop.Globalization.CompareString(_sortHandle, pString1, string1.Length, pString2, string2.Length, options);
- }
- }
-
private unsafe int IcuCompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(!GlobalizationMode.UseNls);
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
- // Unlike NLS, ICU (ucol_getSortKey) allows passing nullptr for either of the source arguments
- // as long as the corresponding length parameter is 0.
+ // GetReference may return nullptr if the input span is defaulted. The native layer handles
+ // this appropriately; no workaround is needed on the managed side.
fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
fixed (char* pString2 = &MemoryMarshal.GetReference(string2))
}
}
- private unsafe int IcuIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!GlobalizationMode.UseNls);
-
- Debug.Assert(!string.IsNullOrEmpty(source));
- Debug.Assert(target != null);
- Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
- Debug.Assert((options & CompareOptions.Ordinal) == 0);
-
- int index;
-
- if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options))
- {
- if ((options & CompareOptions.IgnoreCase) != 0)
- index = IndexOfOrdinalIgnoreCaseHelper(source.AsSpan(startIndex, count), target.AsSpan(), options, matchLengthPtr, fromBeginning: true);
- else
- index = IndexOfOrdinalHelper(source.AsSpan(startIndex, count), target.AsSpan(), options, matchLengthPtr, fromBeginning: true);
- }
- else
- {
- fixed (char* pSource = source)
- fixed (char* pTarget = target)
- {
- index = Interop.Globalization.IndexOf(_sortHandle, pTarget, target.Length, pSource + startIndex, count, options, matchLengthPtr);
- }
- }
-
- return index != -1 ? index + startIndex : -1;
- }
-
private unsafe int IcuIndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning)
{
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(!GlobalizationMode.UseNls);
- Debug.Assert(source.Length != 0);
Debug.Assert(target.Length != 0);
if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options))
}
else
{
+ // GetReference may return nullptr if the input span is defaulted. The native layer handles
+ // this appropriately; no workaround is needed on the managed side.
+
fixed (char* pSource = &MemoryMarshal.GetReference(source))
fixed (char* pTarget = &MemoryMarshal.GetReference(target))
{
if (fromBeginning)
return Interop.Globalization.IndexOf(_sortHandle, pTarget, target.Length, pSource, source.Length, options, matchLengthPtr);
else
- return Interop.Globalization.LastIndexOf(_sortHandle, pTarget, target.Length, pSource, source.Length, options);
+ return Interop.Globalization.LastIndexOf(_sortHandle, pTarget, target.Length, pSource, source.Length, options, matchLengthPtr);
}
}
}
if (fromBeginning)
return Interop.Globalization.IndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr);
else
- return Interop.Globalization.LastIndexOf(_sortHandle, b, target.Length, a, source.Length, options);
+ return Interop.Globalization.LastIndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr);
}
}
if (fromBeginning)
return Interop.Globalization.IndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr);
else
- return Interop.Globalization.LastIndexOf(_sortHandle, b, target.Length, a, source.Length, options);
+ return Interop.Globalization.LastIndexOf(_sortHandle, b, target.Length, a, source.Length, options, matchLengthPtr);
}
}
- private unsafe int IcuLastIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!GlobalizationMode.UseNls);
-
- Debug.Assert(!string.IsNullOrEmpty(source));
- Debug.Assert(target != null);
- Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
-
- // startIndex points to the final char to include in the search space.
- // empty target strings trivially occur at the end of the search space.
-
- if (target.Length == 0)
- {
- return startIndex + 1;
- }
-
- if (options == CompareOptions.Ordinal)
- {
- return IcuLastIndexOfOrdinalCore(source, target, startIndex, count, ignoreCase: false);
- }
-
- // startIndex is the index into source where we start search backwards from. leftStartIndex is the index into source
- // of the start of the string that is count characters away from startIndex.
- int leftStartIndex = (startIndex - count + 1);
-
- int lastIndex;
-
- if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options))
- {
- if ((options & CompareOptions.IgnoreCase) != 0)
- lastIndex = IndexOfOrdinalIgnoreCaseHelper(source.AsSpan(leftStartIndex, count), target.AsSpan(), options, matchLengthPtr: null, fromBeginning: false);
- else
- lastIndex = IndexOfOrdinalHelper(source.AsSpan(leftStartIndex, count), target.AsSpan(), options, matchLengthPtr: null, fromBeginning: false);
- }
- else
- {
- fixed (char* pSource = source)
- fixed (char* pTarget = target)
- {
- lastIndex = Interop.Globalization.LastIndexOf(_sortHandle, pTarget, target.Length, pSource + (startIndex - count + 1), count, options);
- }
- }
-
- return lastIndex != -1 ? lastIndex + leftStartIndex : -1;
- }
-
private unsafe bool IcuStartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert(!GlobalizationMode.UseNls);
Debug.Assert(!prefix.IsEmpty);
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
continue;
}
+ // The match may be affected by special character. Verify that the preceding character is regular ASCII.
+ if (a > ap && *(a - 1) >= 0x80)
+ goto InteropCall;
+ if (b > bp && *(b - 1) >= 0x80)
+ goto InteropCall;
return false;
}
- return (source.Length >= suffix.Length);
+ // The match may be affected by special character. Verify that the preceding character is regular ASCII.
+
+ if (source.Length < suffix.Length)
+ {
+ if (*b >= 0x80)
+ goto InteropCall;
+ return false;
+ }
+
+ if (source.Length > suffix.Length)
+ {
+ if (*a >= 0x80)
+ goto InteropCall;
+ }
+ return true;
InteropCall:
return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options);
continue;
}
+ // The match may be affected by special character. Verify that the preceding character is regular ASCII.
+ if (a > ap && *(a - 1) >= 0x80)
+ goto InteropCall;
+ if (b > bp && *(b - 1) >= 0x80)
+ goto InteropCall;
+ return false;
+ }
+
+ // The match may be affected by special character. Verify that the preceding character is regular ASCII.
+
+ if (source.Length < suffix.Length)
+ {
+ if (*b >= 0x80)
+ goto InteropCall;
return false;
}
- return (source.Length >= suffix.Length);
+ if (source.Length > suffix.Length)
+ {
+ if (*a >= 0x80)
+ goto InteropCall;
+ }
+ return true;
InteropCall:
return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options);
return new SortKey(this, source, options, keyData);
}
- private static unsafe bool IcuIsSortable(char *text, int length)
+ private unsafe int IcuGetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(!GlobalizationMode.UseNls);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
- int index = 0;
- UnicodeCategory uc;
+ // It's ok to pass nullptr (for empty buffers) to ICU's sort key routines.
- while (index < length)
+ int actualSortKeyLength;
+
+ fixed (char* pSource = &MemoryMarshal.GetReference(source))
+ fixed (byte* pDest = &MemoryMarshal.GetReference(destination))
{
- if (char.IsHighSurrogate(text[index]))
- {
- if (index == length - 1 || !char.IsLowSurrogate(text[index+1]))
- return false; // unpaired surrogate
+ actualSortKeyLength = Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, pDest, destination.Length, options);
+ }
- uc = CharUnicodeInfo.GetUnicodeCategory(char.ConvertToUtf32(text[index], text[index+1]));
- if (uc == UnicodeCategory.PrivateUse || uc == UnicodeCategory.OtherNotAssigned)
- return false;
+ // The check below also handles errors due to negative values / overflow being returned.
- index += 2;
- continue;
+ if ((uint)actualSortKeyLength > (uint)destination.Length)
+ {
+ if (actualSortKeyLength > destination.Length)
+ {
+ ThrowHelper.ThrowArgumentException_DestinationTooShort();
}
+ else
+ {
+ throw new ArgumentException(SR.Arg_ExternalException);
+ }
+ }
- if (char.IsLowSurrogate(text[index]))
+ return actualSortKeyLength;
+ }
+
+ private unsafe int IcuGetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options)
+ {
+ Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert(!GlobalizationMode.UseNls);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
+
+ // It's ok to pass nullptr (for empty buffers) to ICU's sort key routines.
+
+ fixed (char* pSource = &MemoryMarshal.GetReference(source))
+ {
+ return Interop.Globalization.GetSortKey(_sortHandle, pSource, source.Length, null, 0, options);
+ }
+ }
+
+ private static bool IcuIsSortable(ReadOnlySpan<char> text)
+ {
+ Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert(!GlobalizationMode.UseNls);
+ Debug.Assert(!text.IsEmpty);
+
+ do
+ {
+ if (Rune.DecodeFromUtf16(text, out Rune result, out int charsConsumed) != OperationStatus.Done)
{
- return false; // unpaired surrogate
+ return false; // found an unpaired surrogate somewhere in the text
}
- uc = CharUnicodeInfo.GetUnicodeCategory(text[index]);
- if (uc == UnicodeCategory.PrivateUse || uc == UnicodeCategory.OtherNotAssigned)
+ UnicodeCategory category = Rune.GetUnicodeCategory(result);
+ if (category == UnicodeCategory.PrivateUse || category == UnicodeCategory.OtherNotAssigned)
{
- return false;
+ return false; // can't sort private use or unassigned code points
}
- index++;
- }
+ text = text.Slice(charsConsumed);
+ } while (!text.IsEmpty);
- return true;
+ return true; // saw no unsortable data in the buffer
}
// -----------------------------
sortKey = sortKey.Slice(sizeof(ushort));
}
}
+
+ private int InvariantGetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options)
+ {
+ Debug.Assert(GlobalizationMode.Invariant);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
+
+ // Make sure the destination buffer is large enough to hold the source projection.
+ // Using unsigned arithmetic below also checks for buffer overflow since the incoming
+ // length is always a non-negative signed integer.
+
+ if ((uint)destination.Length < (uint)source.Length * sizeof(char))
+ {
+ ThrowHelper.ThrowArgumentException_DestinationTooShort();
+ }
+
+ if ((options & CompareOptions.IgnoreCase) == 0)
+ {
+ InvariantCreateSortKeyOrdinal(source, destination);
+ }
+ else
+ {
+ InvariantCreateSortKeyOrdinalIgnoreCase(source, destination);
+ }
+
+ return source.Length * sizeof(char);
+ }
+
+ private int InvariantGetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options)
+ {
+ Debug.Assert(GlobalizationMode.Invariant);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
+
+ // In invariant mode, sort keys are simply a byte projection of the source input,
+ // optionally with casing modifications. We need to make sure we don't overflow
+ // while computing the length.
+
+ int byteLength = source.Length * sizeof(char);
+
+ if (byteLength < 0)
+ {
+ throw new ArgumentException(
+ paramName: nameof(source),
+ message: SR.ArgumentOutOfRange_GetByteCountOverflow);
+ }
+
+ return byteLength;
+ }
}
}
Debug.Assert(GlobalizationMode.UseNls);
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
+#if TARGET_WINDOWS
+ if (!Environment.IsWindows8OrAbove)
+ {
+ // On Windows 7 / Server 2008, LCMapStringEx exhibits strange behaviors if the destination
+ // buffer is both non-null and too small for the required output. To prevent this from
+ // causing issues for us, we need to make an immutable copy of the input buffer so that
+ // its contents can't change between when we calculate the required sort key length and
+ // when we populate the sort key buffer.
+
+ source = source.ToString();
+ }
+#endif
+
// LCMapStringEx doesn't support passing cchSrc = 0, so if given a null or empty input
// we'll normalize it to an empty null-terminated string and pass -1 to indicate that
// the underlying OS function should read until it encounters the null terminator.
}
}
- // TODO https://github.com/dotnet/runtime/issues/8890:
- // This method shouldn't be necessary, as we should be able to just use the overload
- // that takes two spans. But due to this issue, that's adding significant overhead.
- private unsafe int NlsCompareString(ReadOnlySpan<char> string1, string string2, CompareOptions options)
- {
- Debug.Assert(string2 != null);
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(GlobalizationMode.UseNls);
- Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
-
- string? localeName = _sortHandle != IntPtr.Zero ? null : _sortName;
-
- // CompareStringEx may try to dereference the first character of its input, even if an explicit
- // length of 0 is specified. To work around potential AVs we'll always ensure zero-length inputs
- // are normalized to a null-terminated empty string.
-
- if (string1.IsEmpty)
- {
- string1 = string.Empty;
- }
-
- fixed (char* pLocaleName = localeName)
- fixed (char* pString1 = &MemoryMarshal.GetReference(string1))
- fixed (char* pString2 = &string2.GetPinnableReference())
- {
- Debug.Assert(*pString1 >= 0); // assert that we can always dereference this
- Debug.Assert(*pString2 >= 0); // assert that we can always dereference this
-
- int result = Interop.Kernel32.CompareStringEx(
- pLocaleName,
- (uint)GetNativeCompareFlags(options),
- pString1,
- string1.Length,
- pString2,
- string2.Length,
- null,
- null,
- _sortHandle);
-
- if (result == 0)
- {
- throw new ArgumentException(SR.Arg_ExternalException);
- }
-
- // Map CompareStringEx return value to -1, 0, 1.
- return result - 2;
- }
- }
-
private unsafe int NlsCompareString(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
}
}
- private unsafe int NlsIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options, int* matchLengthPtr)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
-
- Debug.Assert(target != null);
- Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
- Debug.Assert((options & CompareOptions.Ordinal) == 0);
-
- int retValue = FindString(FIND_FROMSTART | (uint)GetNativeCompareFlags(options), source.AsSpan(startIndex, count), target, matchLengthPtr);
- if (retValue >= 0)
- {
- return retValue + startIndex;
- }
-
- return -1;
- }
-
private unsafe int NlsIndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning)
{
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(GlobalizationMode.UseNls);
Debug.Assert(target.Length != 0);
- Debug.Assert(options == CompareOptions.None || options == CompareOptions.IgnoreCase);
uint positionFlag = fromBeginning ? (uint)FIND_FROMSTART : FIND_FROMEND;
return FindString(positionFlag | (uint)GetNativeCompareFlags(options), source, target, matchLengthPtr);
}
- private unsafe int NlsLastIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(GlobalizationMode.UseNls);
-
- Debug.Assert(!string.IsNullOrEmpty(source));
- Debug.Assert(target != null);
- Debug.Assert((options & CompareOptions.OrdinalIgnoreCase) == 0);
-
- // startIndex points to the final char to include in the search space.
- // empty target strings trivially occur at the end of the search space.
-
- if (target.Length == 0)
- return startIndex + 1;
-
- if ((options & CompareOptions.Ordinal) != 0)
- {
- return FastLastIndexOfString(source, target, startIndex, count, target.Length);
- }
- else
- {
- int retValue = FindString(FIND_FROMEND | (uint)GetNativeCompareFlags(options), source.AsSpan(startIndex - count + 1, count), target, null);
-
- if (retValue >= 0)
- {
- return retValue + startIndex - (count - 1);
- }
- }
-
- return -1;
- }
-
private unsafe bool NlsStartsWith(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert(GlobalizationMode.UseNls);
- Debug.Assert(!source.IsEmpty);
Debug.Assert(!prefix.IsEmpty);
Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
private const int FIND_FROMSTART = 0x00400000;
private const int FIND_FROMEND = 0x00800000;
- // TODO: Instead of this method could we just have upstack code call LastIndexOfOrdinal with ignoreCase = false?
- private static unsafe int FastLastIndexOfString(string source, string target, int startIndex, int sourceCount, int targetCount)
- {
- int retValue = -1;
-
- int sourceStartIndex = startIndex - sourceCount + 1;
-
- fixed (char* pSource = source, spTarget = target)
- {
- char* spSubSource = pSource + sourceStartIndex;
-
- int endPattern = sourceCount - targetCount;
- if (endPattern < 0)
- return -1;
-
- Debug.Assert(target.Length >= 1);
- char patternChar0 = spTarget[0];
- for (int ctrSrc = endPattern; ctrSrc >= 0; ctrSrc--)
- {
- if (spSubSource[ctrSrc] != patternChar0)
- continue;
-
- int ctrPat;
- for (ctrPat = 1; ctrPat < targetCount; ctrPat++)
- {
- if (spSubSource[ctrSrc + ctrPat] != spTarget[ctrPat])
- break;
- }
- if (ctrPat == targetCount)
- {
- retValue = ctrSrc;
- break;
- }
- }
-
- if (retValue >= 0)
- {
- retValue += startIndex - sourceCount + 1;
- }
- }
-
- return retValue;
- }
-
private unsafe SortKey NlsCreateSortKey(string source, CompareOptions options)
{
Debug.Assert(!GlobalizationMode.Invariant);
return new SortKey(this, source, options, keyData);
}
- private static unsafe bool NlsIsSortable(char* text, int length)
+ private unsafe int NlsGetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options)
+ {
+ Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
+
+ // LCMapStringEx doesn't allow cchDest = 0 unless we're trying to query
+ // the total number of bytes necessary.
+
+ if (destination.IsEmpty)
+ {
+ ThrowHelper.ThrowArgumentException_DestinationTooShort();
+ }
+
+#if TARGET_WINDOWS
+ if (!Environment.IsWindows8OrAbove)
+ {
+ // On Windows 7 / Server 2008, LCMapStringEx exhibits strange behaviors if the destination
+ // buffer is both non-null and too small for the required output. To prevent this from
+ // causing issues for us, we need to make an immutable copy of the input buffer so that
+ // its contents can't change between when we calculate the required sort key length and
+ // when we populate the sort key buffer.
+
+ source = source.ToString();
+ }
+#endif
+
+ uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
+
+ // LCMapStringEx doesn't support passing cchSrc = 0, so if given an empty span
+ // we'll instead normalize to a null-terminated empty string and pass -1 as
+ // the length to indicate that the implicit null terminator should be used.
+
+ int sourceLength = source.Length;
+ if (sourceLength == 0)
+ {
+ source = string.Empty;
+ sourceLength = -1;
+ }
+
+ int actualSortKeyLength;
+
+ fixed (char* pSource = &MemoryMarshal.GetReference(source))
+ fixed (byte* pSortKey = &MemoryMarshal.GetReference(destination))
+ {
+ Debug.Assert(pSource != null);
+ Debug.Assert(pSortKey != null);
+
+#if TARGET_WINDOWS
+ if (!Environment.IsWindows8OrAbove)
+ {
+ // Manually check that the destination buffer is large enough to hold the full output.
+ // See earlier comment for reasoning.
+
+ int requiredSortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
+ flags,
+ pSource, sourceLength,
+ null, 0,
+ null, null, _sortHandle);
+
+ if (requiredSortKeyLength > destination.Length)
+ {
+ ThrowHelper.ThrowArgumentException_DestinationTooShort();
+ }
+
+ if (requiredSortKeyLength <= 0)
+ {
+ throw new ArgumentException(SR.Arg_ExternalException);
+ }
+ }
+#endif
+
+ actualSortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
+ flags,
+ pSource, sourceLength,
+ pSortKey, destination.Length,
+ null, null, _sortHandle);
+ }
+
+ if (actualSortKeyLength <= 0)
+ {
+ Debug.Assert(actualSortKeyLength == 0, "LCMapStringEx should never return a negative value.");
+
+ // This could fail for a variety of reasons, including NLS being unable
+ // to allocate a temporary buffer large enough to hold intermediate state,
+ // or the destination buffer being too small.
+
+ if (Marshal.GetLastWin32Error() == Interop.Errors.ERROR_INSUFFICIENT_BUFFER)
+ {
+ ThrowHelper.ThrowArgumentException_DestinationTooShort();
+ }
+ else
+ {
+ throw new ArgumentException(SR.Arg_ExternalException);
+ }
+ }
+
+ Debug.Assert(actualSortKeyLength <= destination.Length);
+ return actualSortKeyLength;
+ }
+
+ private unsafe int NlsGetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options)
+ {
+ Debug.Assert(!GlobalizationMode.Invariant);
+ Debug.Assert((options & ValidCompareMaskOffFlags) == 0);
+
+ uint flags = LCMAP_SORTKEY | (uint)GetNativeCompareFlags(options);
+
+ // LCMapStringEx doesn't support passing cchSrc = 0, so if given an empty span
+ // we'll instead normalize to a null-terminated empty string and pass -1 as
+ // the length to indicate that the implicit null terminator should be used.
+
+ int sourceLength = source.Length;
+ if (sourceLength == 0)
+ {
+ source = string.Empty;
+ sourceLength = -1;
+ }
+
+ int sortKeyLength;
+
+ fixed (char* pSource = &MemoryMarshal.GetReference(source))
+ {
+ Debug.Assert(pSource != null);
+ sortKeyLength = Interop.Kernel32.LCMapStringEx(_sortHandle != IntPtr.Zero ? null : _sortName,
+ flags,
+ pSource, sourceLength,
+ null, 0,
+ null, null, _sortHandle);
+ }
+
+ if (sortKeyLength <= 0)
+ {
+ Debug.Assert(sortKeyLength == 0, "LCMapStringEx should never return a negative value.");
+
+ // This could fail for a variety of reasons, including NLS being unable
+ // to allocate a temporary buffer large enough to hold intermediate state.
+
+ throw new ArgumentException(SR.Arg_ExternalException);
+ }
+
+ return sortKeyLength;
+ }
+
+ private static unsafe bool NlsIsSortable(ReadOnlySpan<char> text)
{
Debug.Assert(!GlobalizationMode.Invariant);
Debug.Assert(GlobalizationMode.UseNls);
- Debug.Assert(text != null);
+ Debug.Assert(!text.IsEmpty);
- return Interop.Kernel32.IsNLSDefinedString(Interop.Kernel32.COMPARE_STRING, 0, IntPtr.Zero, text, length);
+ fixed (char* pText = &MemoryMarshal.GetReference(text))
+ {
+ return Interop.Kernel32.IsNLSDefinedString(Interop.Kernel32.COMPARE_STRING, 0, IntPtr.Zero, pText, text.Length);
+ }
}
private const int COMPARE_OPTIONS_ORDINAL = 0x40000000; // Ordinal
// See the LICENSE file in the project root for more information.
using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
+using System.Text;
using System.Text.Unicode;
using Internal.Runtime.CompilerServices;
return CultureInfo.GetCultureInfo(name).CompareInfo;
}
- public static unsafe bool IsSortable(char ch)
+ public static bool IsSortable(char ch)
{
- if (GlobalizationMode.Invariant)
- {
- return true;
- }
-
- char* pChar = &ch;
- return IsSortableCore(pChar, 1);
+ return IsSortable(MemoryMarshal.CreateReadOnlySpan(ref ch, 1));
}
- public static unsafe bool IsSortable(string text)
+ public static bool IsSortable(string text)
{
if (text == null)
{
throw new ArgumentNullException(nameof(text));
}
+ return IsSortable(text.AsSpan());
+ }
+
+ /// <summary>
+ /// Indicates whether a specified Unicode string is sortable.
+ /// </summary>
+ /// <param name="text">A string of zero or more Unicode characters.</param>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="text"/> is non-empty and contains
+ /// only sortable Unicode characters; otherwise, <see langword="false"/>.
+ /// </returns>
+ public static bool IsSortable(ReadOnlySpan<char> text)
+ {
if (text.Length == 0)
{
return false;
if (GlobalizationMode.Invariant)
{
- return true;
+ return true; // all chars are sortable in invariant mode
}
- fixed (char* pChar = text)
- {
- return IsSortableCore(pChar, text.Length);
- }
+ return (GlobalizationMode.UseNls) ? NlsIsSortable(text) : IcuIsSortable(text);
}
- private static unsafe bool IsSortableCore(char* pChar, int length) =>
- GlobalizationMode.UseNls ?
- NlsIsSortable(pChar, length) :
- IcuIsSortable(pChar, length);
+ /// <summary>
+ /// Indicates whether a specified <see cref="Rune"/> is sortable.
+ /// </summary>
+ /// <param name="value">A Unicode scalar value.</param>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="value"/> is a sortable Unicode scalar
+ /// value; otherwise, <see langword="false"/>.
+ /// </returns>
+ public static bool IsSortable(Rune value)
+ {
+ Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
+ int charCount = value.EncodeToUtf16(valueAsUtf16);
+ return IsSortable(valueAsUtf16.Slice(0, charCount));
+ }
private void InitSort(CultureInfo culture)
{
public int Compare(string? string1, string? string2, CompareOptions options)
{
- if (options == CompareOptions.OrdinalIgnoreCase)
- {
- return string.Compare(string1, string2, StringComparison.OrdinalIgnoreCase);
- }
-
- // Verify the options before we do any real comparison.
- if ((options & CompareOptions.Ordinal) != 0)
- {
- if (options != CompareOptions.Ordinal)
- {
- throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
- }
-
- return string.CompareOrdinal(string1, string2);
- }
-
- if ((options & ValidCompareMaskOffFlags) != 0)
- {
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ int retVal;
// Our paradigm is that null sorts less than any other string and
// that two nulls sort as equal.
- if (string1 == null)
- {
- if (string2 == null)
- {
- return 0;
- }
- return -1; // null < non-null
- }
- if (string2 == null)
- {
- return 1; // non-null > null
- }
-
- if (GlobalizationMode.Invariant)
- {
- if ((options & CompareOptions.IgnoreCase) != 0)
- {
- return CompareOrdinalIgnoreCase(string1, string2);
- }
-
- return string.CompareOrdinal(string1, string2);
- }
-
- return CompareStringCore(string1.AsSpan(), string2.AsSpan(), options);
- }
-
- // TODO https://github.com/dotnet/runtime/issues/8890:
- // This method shouldn't be necessary, as we should be able to just use the overload
- // that takes two spans. But due to this issue, that's adding significant overhead.
- internal int Compare(ReadOnlySpan<char> string1, string? string2, CompareOptions options)
- {
- if (options == CompareOptions.OrdinalIgnoreCase)
- {
- return CompareOrdinalIgnoreCase(string1, string2.AsSpan());
- }
- // Verify the options before we do any real comparison.
- if ((options & CompareOptions.Ordinal) != 0)
- {
- if (options != CompareOptions.Ordinal)
- {
- throw new ArgumentException(SR.Argument_CompareOptionOrdinal, nameof(options));
- }
-
- return string.CompareOrdinal(string1, string2.AsSpan());
- }
-
- if ((options & ValidCompareMaskOffFlags) != 0)
+ if (string1 == null)
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
+ retVal = (string2 == null) ? 0 : -1;
+ goto CheckOptionsAndReturn;
}
-
- // null sorts less than any other string.
if (string2 == null)
{
- return 1;
+ retVal = 1;
+ goto CheckOptionsAndReturn;
}
- if (GlobalizationMode.Invariant)
- {
- return (options & CompareOptions.IgnoreCase) != 0 ?
- CompareOrdinalIgnoreCase(string1, string2.AsSpan()) :
- string.CompareOrdinal(string1, string2.AsSpan());
- }
+ return Compare(string1.AsSpan(), string2.AsSpan(), options);
- return CompareStringCore(string1, string2, options);
- }
+ CheckOptionsAndReturn:
- internal int CompareOptionNone(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
- {
- // Check for empty span or span from a null string
- if (string1.Length == 0 || string2.Length == 0)
- {
- return string1.Length - string2.Length;
- }
+ // If we're short-circuiting the globalization logic, we still need to check that
+ // the provided options were valid.
- return GlobalizationMode.Invariant ?
- string.CompareOrdinal(string1, string2) :
- CompareStringCore(string1, string2, CompareOptions.None);
+ CheckCompareOptionsForCompare(options);
+ return retVal;
}
internal int CompareOptionIgnoreCase(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2)
{
- // Check for empty span or span from a null string
- if (string1.Length == 0 || string2.Length == 0)
- {
- return string1.Length - string2.Length;
- }
-
return GlobalizationMode.Invariant ?
CompareOrdinalIgnoreCase(string1, string2) :
CompareStringCore(string1, string2, CompareOptions.IgnoreCase);
/// </summary>
public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2)
{
- return Compare(string1, offset1, length1, string2, offset2, length2, 0);
+ return Compare(string1, offset1, length1, string2, offset2, length2, CompareOptions.None);
}
public int Compare(string? string1, int offset1, string? string2, int offset2, CompareOptions options)
public int Compare(string? string1, int offset1, string? string2, int offset2)
{
- return Compare(string1, offset1, string2, offset2, 0);
+ return Compare(string1, offset1, string2, offset2, CompareOptions.None);
}
public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2, CompareOptions options)
{
- if (options == CompareOptions.OrdinalIgnoreCase)
+ ReadOnlySpan<char> span1 = default;
+ ReadOnlySpan<char> span2 = default;
+
+ if (string1 == null)
{
- int result = string.Compare(string1, offset1, string2, offset2, length1 < length2 ? length1 : length2, StringComparison.OrdinalIgnoreCase);
- if ((length1 != length2) && result == 0)
+ if (offset1 != 0 || length1 != 0)
{
- return length1 > length2 ? 1 : -1;
+ goto BoundsCheckError;
}
+ }
+ else if (!string1.TryGetSpan(offset1, length1, out span1))
+ {
+ goto BoundsCheckError;
+ }
- return result;
+ if (string2 == null)
+ {
+ if (offset2 != 0 || length2 != 0)
+ {
+ goto BoundsCheckError;
+ }
+ }
+ else if (!string2.TryGetSpan(offset2, length2, out span2))
+ {
+ goto BoundsCheckError;
}
+ // At this point both string1 and string2 have been bounds-checked.
+
+ int retVal;
+
+ // Our paradigm is that null sorts less than any other string and
+ // that two nulls sort as equal.
+
+ if (string1 == null)
+ {
+ retVal = (string2 == null) ? 0 : -1;
+ goto CheckOptionsAndReturn;
+ }
+ if (string2 == null)
+ {
+ retVal = 1;
+ goto CheckOptionsAndReturn;
+ }
+
+ // At this point we know both string1 and string2 weren't null,
+ // though they may have been empty.
+
+ Debug.Assert(!Unsafe.IsNullRef(ref MemoryMarshal.GetReference(span1)));
+ Debug.Assert(!Unsafe.IsNullRef(ref MemoryMarshal.GetReference(span2)));
+
+ return Compare(span1, span2, options);
+
+ CheckOptionsAndReturn:
+
+ // If we're short-circuiting the globalization logic, we still need to check that
+ // the provided options were valid.
+
+ CheckCompareOptionsForCompare(options);
+ return retVal;
+
+ BoundsCheckError:
+
+ // We know a bounds check error occurred. Now we just need to figure
+ // out the correct error message to surface.
+
if (length1 < 0 || length2 < 0)
{
throw new ArgumentOutOfRangeException((length1 < 0) ? nameof(length1) : nameof(length2), SR.ArgumentOutOfRange_NeedPosNum);
}
+
if (offset1 < 0 || offset2 < 0)
{
throw new ArgumentOutOfRangeException((offset1 < 0) ? nameof(offset1) : nameof(offset2), SR.ArgumentOutOfRange_NeedPosNum);
}
+
if (offset1 > (string1 == null ? 0 : string1.Length) - length1)
{
throw new ArgumentOutOfRangeException(nameof(string1), SR.ArgumentOutOfRange_OffsetLength);
}
- if (offset2 > (string2 == null ? 0 : string2.Length) - length2)
+
+ Debug.Assert(offset2 > (string2 == null ? 0 : string2.Length) - length2);
+ throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
+ }
+
+ /// <summary>
+ /// Compares two strings.
+ /// </summary>
+ /// <param name="string1">The first string to compare.</param>
+ /// <param name="string2">The second string to compare.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the comparison.</param>
+ /// <returns>
+ /// Zero if <paramref name="string1"/> and <paramref name="string2"/> are equal;
+ /// or a negative value if <paramref name="string1"/> sorts before <paramref name="string2"/>;
+ /// or a positive value if <paramref name="string1"/> sorts after <paramref name="string2"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public int Compare(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options = CompareOptions.None)
+ {
+ if (string1 == string2) // referential equality + length
{
- throw new ArgumentOutOfRangeException(nameof(string2), SR.ArgumentOutOfRange_OffsetLength);
+ CheckCompareOptionsForCompare(options);
+ return 0;
}
- if ((options & CompareOptions.Ordinal) != 0)
+
+ if ((options & ValidCompareMaskOffFlags) == 0)
{
- if (options != CompareOptions.Ordinal)
+ // Common case: caller is attempting to perform linguistic comparison.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
+
+ if (!GlobalizationMode.Invariant)
+ {
+ return CompareStringCore(string1, string2, options);
+ }
+ else if ((options & CompareOptions.IgnoreCase) == 0)
{
- throw new ArgumentException(SR.Argument_CompareOptionOrdinal,
- nameof(options));
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
}
}
- else if ((options & ValidCompareMaskOffFlags) != 0)
+ else
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- if (string1 == null)
- {
- if (string2 == null)
+ if (options == CompareOptions.Ordinal)
{
- return 0;
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ ThrowCompareOptionsCheckFailed(options);
}
- return -1;
- }
- if (string2 == null)
- {
- return 1;
}
- ReadOnlySpan<char> span1 = string1.AsSpan(offset1, length1);
- ReadOnlySpan<char> span2 = string2.AsSpan(offset2, length2);
+ ReturnOrdinal:
+ return string1.SequenceCompareTo(string2);
- if (options == CompareOptions.Ordinal)
- {
- return string.CompareOrdinal(span1, span2);
- }
+ ReturnOrdinalIgnoreCase:
+ return CompareOrdinalIgnoreCase(string1, string2);
+ }
- if (GlobalizationMode.Invariant)
+ // Checks that 'CompareOptions' is valid for a call to Compare, throwing the appropriate
+ // exception if the check fails.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [StackTraceHidden]
+ private static void CheckCompareOptionsForCompare(CompareOptions options)
+ {
+ // Any combination of defined CompareOptions flags is valid, except for
+ // Ordinal and OrdinalIgnoreCase, which may only be used in isolation.
+
+ if ((options & ValidCompareMaskOffFlags) != 0)
{
- if ((options & CompareOptions.IgnoreCase) != 0)
+ if (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase)
{
- return CompareOrdinalIgnoreCase(span1, span2);
+ ThrowCompareOptionsCheckFailed(options);
}
-
- return string.CompareOrdinal(span1, span2);
}
-
- return CompareStringCore(span1, span2, options);
}
- // TODO https://github.com/dotnet/runtime/issues/8890:
- // This method shouldn't be necessary, as we should be able to just use the overload
- // that takes two spans. But due to this issue, that's adding significant overhead.
- private unsafe int CompareStringCore(ReadOnlySpan<char> string1, string string2, CompareOptions options) =>
- GlobalizationMode.UseNls ?
- NlsCompareString(string1, string2, options) :
- IcuCompareString(string1, string2, options);
+ [DoesNotReturn]
+ [StackTraceHidden]
+ private static void ThrowCompareOptionsCheckFailed(CompareOptions options)
+ {
+ throw new ArgumentException(
+ paramName: nameof(options),
+ message: ((options & CompareOptions.Ordinal) != 0) ? SR.Argument_CompareOptionOrdinal : SR.Argument_InvalidFlag);
+ }
private unsafe int CompareStringCore(ReadOnlySpan<char> string1, ReadOnlySpan<char> string2, CompareOptions options) =>
GlobalizationMode.UseNls ?
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (prefix == null)
{
- throw new ArgumentNullException(nameof(prefix));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.prefix);
}
- if (prefix.Length == 0)
+ return IsPrefix(source.AsSpan(), prefix.AsSpan(), options);
+ }
+
+ /// <summary>
+ /// Determines whether a string starts with a specific prefix.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="prefix">The prefix to attempt to match at the start of <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="prefix"/> occurs at the start of <paramref name="source"/>;
+ /// otherwise, <see langword="false"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options = CompareOptions.None)
+ {
+ // The empty string is trivially a prefix of every other string. For compat with
+ // earlier versions of the Framework we'll early-exit here before validating the
+ // 'options' argument.
+
+ if (prefix.IsEmpty)
{
return true;
}
- if (source.Length == 0)
- {
- return false;
- }
- if (options == CompareOptions.OrdinalIgnoreCase)
+ if ((options & ValidIndexMaskOffFlags) == 0)
{
- return source.StartsWith(prefix, StringComparison.OrdinalIgnoreCase);
- }
+ // Common case: caller is attempting to perform a linguistic search.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
- if (options == CompareOptions.Ordinal)
- {
- return source.StartsWith(prefix, StringComparison.Ordinal);
+ if (!GlobalizationMode.Invariant)
+ {
+ return StartsWithCore(source, prefix, options);
+ }
+ else if ((options & CompareOptions.IgnoreCase) == 0)
+ {
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
}
-
- if ((options & ValidIndexMaskOffFlags) != 0)
+ else
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- if (GlobalizationMode.Invariant)
- {
- return source.StartsWith(prefix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ if (options == CompareOptions.Ordinal)
+ {
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ ThrowCompareOptionsCheckFailed(options);
+ }
}
- return StartsWithCore(source, prefix, options);
- }
+ ReturnOrdinal:
+ return source.StartsWith(prefix);
- internal bool IsPrefix(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options)
- {
- Debug.Assert(prefix.Length != 0);
- Debug.Assert(source.Length != 0);
- Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
-
- return StartsWithCore(source, prefix, options);
+ ReturnOrdinalIgnoreCase:
+ return source.StartsWithOrdinalIgnoreCase(prefix);
}
private unsafe bool StartsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> prefix, CompareOptions options) =>
public bool IsPrefix(string source, string prefix)
{
- return IsPrefix(source, prefix, 0);
+ return IsPrefix(source, prefix, CompareOptions.None);
}
/// <summary>
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (suffix == null)
{
- throw new ArgumentNullException(nameof(suffix));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.suffix);
}
- if (suffix.Length == 0)
+ return IsSuffix(source.AsSpan(), suffix.AsSpan(), options);
+ }
+
+ /// <summary>
+ /// Determines whether a string ends with a specific suffix.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="suffix">The suffix to attempt to match at the end of <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the match.</param>
+ /// <returns>
+ /// <see langword="true"/> if <paramref name="suffix"/> occurs at the end of <paramref name="source"/>;
+ /// otherwise, <see langword="false"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options = CompareOptions.None)
+ {
+ // The empty string is trivially a suffix of every other string. For compat with
+ // earlier versions of the Framework we'll early-exit here before validating the
+ // 'options' argument.
+
+ if (suffix.IsEmpty)
{
return true;
}
- if (source.Length == 0)
- {
- return false;
- }
- if (options == CompareOptions.OrdinalIgnoreCase)
+ if ((options & ValidIndexMaskOffFlags) == 0)
{
- return source.EndsWith(suffix, StringComparison.OrdinalIgnoreCase);
- }
+ // Common case: caller is attempting to perform a linguistic search.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
- if (options == CompareOptions.Ordinal)
- {
- return source.EndsWith(suffix, StringComparison.Ordinal);
+ if (!GlobalizationMode.Invariant)
+ {
+ return EndsWithCore(source, suffix, options);
+ }
+ else if ((options & CompareOptions.IgnoreCase) == 0)
+ {
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
}
-
- if ((options & ValidIndexMaskOffFlags) != 0)
+ else
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- if (GlobalizationMode.Invariant)
- {
- return source.EndsWith(suffix, (options & CompareOptions.IgnoreCase) != 0 ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
+ if (options == CompareOptions.Ordinal)
+ {
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ ThrowCompareOptionsCheckFailed(options);
+ }
}
- return EndsWithCore(source, suffix, options);
- }
+ ReturnOrdinal:
+ return source.EndsWith(suffix);
- internal bool IsSuffix(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options)
- {
- Debug.Assert(suffix.Length != 0);
- Debug.Assert(source.Length != 0);
- Debug.Assert((options & ValidIndexMaskOffFlags) == 0);
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0);
-
- return EndsWithCore(source, suffix, options);
+ ReturnOrdinalIgnoreCase:
+ return source.EndsWithOrdinalIgnoreCase(suffix);
}
public bool IsSuffix(string source, string suffix)
{
- return IsSuffix(source, suffix, 0);
+ return IsSuffix(source, suffix, CompareOptions.None);
}
private unsafe bool EndsWithCore(ReadOnlySpan<char> source, ReadOnlySpan<char> suffix, CompareOptions options) =>
/// the specified value is not found. If value equals string.Empty,
/// startIndex is returned. Throws IndexOutOfRange if startIndex or
/// endIndex is less than zero or greater than the length of string.
- /// Throws ArgumentException if value is null.
+ /// Throws ArgumentException if value (as a string) is null.
/// </summary>
public int IndexOf(string source, char value)
{
- if (source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- return IndexOf(source, value, 0, source.Length, CompareOptions.None);
+ return IndexOf(source, value, CompareOptions.None);
}
public int IndexOf(string source, string value)
{
- if (source == null)
- throw new ArgumentNullException(nameof(source));
-
- return IndexOf(source, value, 0, source.Length, CompareOptions.None);
+ return IndexOf(source, value, CompareOptions.None);
}
public int IndexOf(string source, char value, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
- return IndexOf(source, value, 0, source.Length, options);
+ return IndexOf(source, MemoryMarshal.CreateReadOnlySpan(ref value, 1), options);
}
public int IndexOf(string source, string value, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
+ }
+ if (value == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
}
- return IndexOf(source, value, 0, source.Length, options);
+ return IndexOf(source.AsSpan(), value.AsSpan(), options);
}
public int IndexOf(string source, char value, int startIndex)
{
- if (source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
+ return IndexOf(source, value, startIndex, CompareOptions.None);
}
public int IndexOf(string source, string value, int startIndex)
{
- if (source == null)
- throw new ArgumentNullException(nameof(source));
-
- return IndexOf(source, value, startIndex, source.Length - startIndex, CompareOptions.None);
+ return IndexOf(source, value, startIndex, CompareOptions.None);
}
public int IndexOf(string source, char value, int startIndex, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
return IndexOf(source, value, startIndex, source.Length - startIndex, options);
+
}
public int IndexOf(string source, string value, int startIndex, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
return IndexOf(source, value, startIndex, source.Length - startIndex, options);
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
- }
- if (startIndex < 0 || startIndex > source.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
- }
- if (count < 0 || startIndex > source.Length - count)
- {
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
- if (source.Length == 0)
+ if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
{
- return -1;
+ // Bounds check failed - figure out exactly what went wrong so that we can
+ // surface the correct argument exception.
+
+ if ((uint)startIndex > (uint)source.Length)
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index);
+ }
+ else
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
+ }
}
- // Validate CompareOptions
- // Ordinal can't be selected with other flags
- if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase))
+ int result = IndexOf(sourceSpan, MemoryMarshal.CreateReadOnlySpan(ref value, 1), options);
+ if (result >= 0)
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
+ result += startIndex;
}
-
- return IndexOf(source, char.ToString(value), startIndex, count, options, null);
+ return result;
}
public unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (value == null)
{
- throw new ArgumentNullException(nameof(value));
- }
- if (startIndex > source.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
}
- // In Everett we used to return -1 for empty string even if startIndex is negative number so we keeping same behavior here.
- // We return 0 if both source and value are empty strings for Everett compatibility too.
- if (source.Length == 0)
+ if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
{
- if (value.Length == 0)
+ // Bounds check failed - figure out exactly what went wrong so that we can
+ // surface the correct argument exception.
+
+ if ((uint)startIndex > (uint)source.Length)
{
- return 0;
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index);
+ }
+ else
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.count, ExceptionResource.ArgumentOutOfRange_Count);
}
- return -1;
}
- if (startIndex < 0)
+ int result = IndexOf(sourceSpan, value, options);
+ if (result >= 0)
{
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
+ result += startIndex;
}
+ return result;
+ }
- if (count < 0 || startIndex > source.Length - count)
- {
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
+ /// <summary>
+ /// Searches for the first occurrence of a substring within a source string.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
+ /// <returns>
+ /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
+ /// first appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options = CompareOptions.None)
+ {
+ if ((options & ValidIndexMaskOffFlags) == 0)
+ {
+ // Common case: caller is attempting to perform a linguistic search.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
+
+ if (!GlobalizationMode.Invariant)
+ {
+ if (value.IsEmpty)
+ {
+ return 0; // Empty target string trivially occurs at index 0 of every search space.
+ }
+ else
+ {
+ return IndexOfCore(source, value, options, null /* matchLengthPtr */, fromBeginning: true);
+ }
+ }
+ else if ((options & CompareOptions.IgnoreCase) == 0)
+ {
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
}
-
- // Validate CompareOptions
- // Ordinal can't be selected with other flags
- if ((options & ValidIndexMaskOffFlags) != 0 && (options != CompareOptions.Ordinal && options != CompareOptions.OrdinalIgnoreCase))
+ else
{
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- return IndexOf(source, value, startIndex, count, options, null);
- }
+ if (options == CompareOptions.Ordinal)
+ {
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
+ }
+ }
- internal int IndexOfOrdinalIgnoreCase(ReadOnlySpan<char> source, ReadOnlySpan<char> value)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!source.IsEmpty);
- Debug.Assert(!value.IsEmpty);
+ ReturnOrdinal:
+ return source.IndexOf(value);
- return IndexOfOrdinalCore(source, value, ignoreCase: true, fromBeginning: true);
+ ReturnOrdinalIgnoreCase:
+ return IndexOfOrdinalIgnoreCase(source, value, fromBeginning: true);
}
- internal int LastIndexOfOrdinal(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!source.IsEmpty);
- Debug.Assert(!value.IsEmpty);
- return IndexOfOrdinalCore(source, value, ignoreCase, fromBeginning: false);
+ /// <summary>
+ /// Searches for the first occurrence of a <see cref="Rune"/> within a source string.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="value">The <see cref="Rune"/> to locate within <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
+ /// <returns>
+ /// The zero-based index into <paramref name="source"/> where <paramref name="value"/>
+ /// first appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public int IndexOf(ReadOnlySpan<char> source, Rune value, CompareOptions options = CompareOptions.None)
+ {
+ Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
+ int charCount = value.EncodeToUtf16(valueAsUtf16);
+ return IndexOf(source, valueAsUtf16.Slice(0, charCount), options);
}
private static int IndexOfOrdinalCore(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool ignoreCase, bool fromBeginning) =>
NlsIndexOfOrdinalCore(source, value, ignoreCase, fromBeginning) :
IcuIndexOfOrdinalCore(source, value, ignoreCase, fromBeginning);
- internal unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
+ internal static int IndexOfOrdinalIgnoreCase(ReadOnlySpan<char> source, ReadOnlySpan<char> value, bool fromBeginning)
{
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!source.IsEmpty);
- Debug.Assert(!value.IsEmpty);
- return IndexOfCore(source, value, options, null, fromBeginning: true);
- }
-
- internal unsafe int LastIndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options)
- {
- Debug.Assert(!GlobalizationMode.Invariant);
- Debug.Assert(!source.IsEmpty);
- Debug.Assert(!value.IsEmpty);
- return IndexOfCore(source, value, options, null, fromBeginning: false);
- }
+ if (value.IsEmpty)
+ {
+ // Empty target string trivially appears at all indexes of all search spaces.
- /// <summary>
- /// The following IndexOf overload is mainly used by String.Replace. This overload assumes the parameters are already validated
- /// and the caller is passing a valid matchLengthPtr pointer.
- /// </summary>
- internal unsafe int IndexOf(string source, string value, int startIndex, int count, CompareOptions options, int* matchLengthPtr, bool fromBeginning = true)
- {
- Debug.Assert(source != null);
- Debug.Assert(value != null);
- Debug.Assert(startIndex >= 0);
+ return (fromBeginning) ? 0 : source.Length;
+ }
- if (matchLengthPtr != null)
+ if (value.Length > source.Length)
{
- *matchLengthPtr = 0;
+ // A non-linguistic search compares chars directly against one another, so large
+ // target strings can never be found inside small search spaces. This check also
+ // handles empty 'source' spans.
+
+ return -1;
}
- if (value.Length == 0)
+ if (GlobalizationMode.Invariant)
{
- return startIndex;
+ return InvariantIndexOf(source, value, ignoreCase: true, fromBeginning);
}
-
- if (startIndex >= source.Length)
+ else
{
- return -1;
+ return IndexOfOrdinalCore(source, value, ignoreCase: true, fromBeginning);
}
+ }
+
+ /// <summary>
+ /// The following IndexOf overload is mainly used by String.Replace. This overload assumes the parameters are already validated
+ /// and the caller is passing a valid matchLengthPtr pointer.
+ /// </summary>
+ internal unsafe int IndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, int* matchLengthPtr, CompareOptions options, bool fromBeginning)
+ {
+ Debug.Assert(matchLengthPtr != null);
+ *matchLengthPtr = 0;
- if (options == CompareOptions.OrdinalIgnoreCase)
+ if ((options & ValidIndexMaskOffFlags) == 0)
{
- int res;
- if (fromBeginning)
+ // Common case: caller is attempting to perform a linguistic search.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
+
+ if (!GlobalizationMode.Invariant)
{
- res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
+ if (value.IsEmpty)
+ {
+ // empty target substring trivially occurs at beginning / end of search space
+ return (fromBeginning) ? 0 : source.Length;
+ }
+ else
+ {
+ return IndexOfCore(source, value, options, matchLengthPtr, fromBeginning);
+ }
}
- else
+ else if ((options & CompareOptions.IgnoreCase) == 0)
{
- res = LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
+ goto ReturnOrdinal;
}
-
- if (res >= 0 && matchLengthPtr != null)
+ else
{
- *matchLengthPtr = value.Length;
+ goto ReturnOrdinalIgnoreCase;
}
- return res;
}
-
- if (GlobalizationMode.Invariant)
+ else
{
- bool ignoreCase = (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0;
- int res;
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- if (fromBeginning)
+ if (options == CompareOptions.Ordinal)
{
- res = IndexOfOrdinal(source, value, startIndex, count, ignoreCase);
+ goto ReturnOrdinal;
}
- else
+ else if (options == CompareOptions.OrdinalIgnoreCase)
{
- res = LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase);
+ goto ReturnOrdinalIgnoreCase;
}
-
- if (res >= 0 && matchLengthPtr != null)
+ else
{
- *matchLengthPtr = value.Length;
+ ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
}
- return res;
}
- if (options == CompareOptions.Ordinal)
- {
- int retValue;
+ ReturnOrdinal:
+ int retVal = (fromBeginning) ? source.IndexOf(value) : source.LastIndexOf(value);
+ goto OrdinalReturn;
- if (fromBeginning)
- {
- retValue = SpanHelpers.IndexOf(
- ref Unsafe.Add(ref source.GetRawStringData(), startIndex),
- count,
- ref value.GetRawStringData(),
- value.Length);
- }
- else
- {
- retValue = SpanHelpers.LastIndexOf(
- ref Unsafe.Add(ref source.GetRawStringData(), startIndex),
- count,
- ref value.GetRawStringData(),
- value.Length);
- }
+ ReturnOrdinalIgnoreCase:
+ retVal = IndexOfOrdinalIgnoreCase(source, value, fromBeginning);
+ goto OrdinalReturn;
- if (retValue >= 0)
- {
- retValue += startIndex;
- if (matchLengthPtr != null)
- {
- *matchLengthPtr = value.Length;
- }
- }
+ OrdinalReturn:
+ // Both Ordinal and OrdinalIgnoreCase match by individual code points in a non-linguistic manner.
+ // Non-BMP code points will never match BMP code points, so given UTF-16 inputs the match length
+ // will always be equivalent to the target string length.
- return retValue;
- }
- else
+ if (retVal >= 0)
{
- if (fromBeginning)
- {
- // Call the string-based overload, as it special-cases IsFastSort as a perf optimization.
- return IndexOfCore(source, value, startIndex, count, options, matchLengthPtr);
- }
- else
- {
- return IndexOfCore(source.AsSpan(startIndex, count), value, options, matchLengthPtr, fromBeginning: false);
- }
+ *matchLengthPtr = value.Length;
}
+ return retVal;
}
- private unsafe int IndexOfCore(string source, string target, int startIndex, int count, CompareOptions options, int* matchLengthPtr) =>
- GlobalizationMode.UseNls ?
- NlsIndexOfCore(source, target, startIndex, count, options, matchLengthPtr) :
- IcuIndexOfCore(source, target, startIndex, count, options, matchLengthPtr);
-
private unsafe int IndexOfCore(ReadOnlySpan<char> source, ReadOnlySpan<char> target, CompareOptions options, int* matchLengthPtr, bool fromBeginning) =>
GlobalizationMode.UseNls ?
NlsIndexOfCore(source, target, options, matchLengthPtr, fromBeginning) :
/// the specified value is not found. If value equals string.Empty,
/// endIndex is returned. Throws IndexOutOfRange if startIndex or
/// endIndex is less than zero or greater than the length of string.
- /// Throws ArgumentException if value is null.
+ /// Throws ArgumentException if value (as a string) is null.
/// </summary>
public int LastIndexOf(string source, char value)
{
- if (source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- // Can't start at negative index, so make sure we check for the length == 0 case.
- return LastIndexOf(source, value, source.Length - 1, source.Length, CompareOptions.None);
+ return LastIndexOf(source, value, CompareOptions.None);
}
public int LastIndexOf(string source, string value)
{
- if (source == null)
- {
- throw new ArgumentNullException(nameof(source));
- }
-
- // Can't start at negative index, so make sure we check for the length == 0 case.
- return LastIndexOf(source, value, source.Length - 1,
- source.Length, CompareOptions.None);
+ return LastIndexOf(source, value, CompareOptions.None);
}
public int LastIndexOf(string source, char value, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
- // Can't start at negative index, so make sure we check for the length == 0 case.
- return LastIndexOf(source, value, source.Length - 1, source.Length, options);
+ return LastIndexOf(source, MemoryMarshal.CreateReadOnlySpan(ref value, 1), options);
}
public int LastIndexOf(string source, string value, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
+ }
+ if (value == null)
+ {
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
}
- // Can't start at negative index, so make sure we check for the length == 0 case.
- return LastIndexOf(source, value, source.Length - 1, source.Length, options);
+ return LastIndexOf(source.AsSpan(), value.AsSpan(), options);
}
public int LastIndexOf(string source, char value, int startIndex)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
- }
- // Validate CompareOptions
- // Ordinal can't be selected with other flags
- if ((options & ValidIndexMaskOffFlags) != 0 &&
- (options != CompareOptions.Ordinal) &&
- (options != CompareOptions.OrdinalIgnoreCase))
- {
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
- // Special case for 0 length input strings
- if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
- {
- return -1;
- }
+ TryAgain:
- // Make sure we're not out of range
- if (startIndex < 0 || startIndex > source.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
- }
+ // Previous versions of the Framework special-cased empty 'source' to allow startIndex = -1 or startIndex = 0,
+ // ignoring 'count' and short-circuiting the entire operation. We'll silently fix up the 'count' parameter
+ // if this occurs.
+ //
+ // See the comments just before string.IndexOf(string) for more information on how these computations are
+ // performed.
- // Make sure that we allow startIndex == source.Length
- if (startIndex == source.Length)
+ if ((uint)startIndex >= (uint)source.Length)
{
- startIndex--;
- if (count > 0)
+ if (startIndex == -1 && source.Length == 0)
{
- count--;
+ count = 0; // normalize
}
- }
+ else if (startIndex == source.Length)
+ {
+ // The caller likely had an off-by-one error when invoking the API. The Framework has historically
+ // allowed for this and tried to fix up the parameters, so we'll continue to do so for compat.
- // 2nd have of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
- if (count < 0 || startIndex - count + 1 < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
+ startIndex--;
+ if (count > 0)
+ {
+ count--;
+ }
+
+ goto TryAgain; // guaranteed never to loop more than once
+ }
+ else
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index);
+ }
}
- if (options == CompareOptions.OrdinalIgnoreCase)
+ startIndex = startIndex - count + 1; // this will be the actual index where we begin our search
+
+ if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
{
- return source.LastIndexOf(value.ToString(), startIndex, count, StringComparison.OrdinalIgnoreCase);
+ ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
}
- if (GlobalizationMode.Invariant)
+ int retVal = LastIndexOf(sourceSpan, MemoryMarshal.CreateReadOnlySpan(ref value, 1), options);
+ if (retVal >= 0)
{
- return InvariantLastIndexOf(source, char.ToString(value), startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
+ retVal += startIndex;
}
-
- return LastIndexOfCore(source, value.ToString(), startIndex, count, options);
+ return retVal;
}
public int LastIndexOf(string source, string value, int startIndex, int count, CompareOptions options)
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
if (value == null)
{
- throw new ArgumentNullException(nameof(value));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.value);
}
- // Validate CompareOptions
- // Ordinal can't be selected with other flags
- if ((options & ValidIndexMaskOffFlags) != 0 &&
- (options != CompareOptions.Ordinal) &&
- (options != CompareOptions.OrdinalIgnoreCase))
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
+ TryAgain:
+
+ // Previous versions of the Framework special-cased empty 'source' to allow startIndex = -1 or startIndex = 0,
+ // ignoring 'count' and short-circuiting the entire operation. We'll silently fix up the 'count' parameter
+ // if this occurs.
+ //
+ // See the comments just before string.IndexOf(string) for more information on how these computations are
+ // performed.
- // Special case for 0 length input strings
- if (source.Length == 0 && (startIndex == -1 || startIndex == 0))
+ if ((uint)startIndex >= (uint)source.Length)
{
- return (value.Length == 0) ? 0 : -1;
+ if (startIndex == -1 && source.Length == 0)
+ {
+ count = 0; // normalize
+ }
+ else if (startIndex == source.Length)
+ {
+ // The caller likely had an off-by-one error when invoking the API. The Framework has historically
+ // allowed for this and tried to fix up the parameters, so we'll continue to do so for compat.
+
+ startIndex--;
+ if (count > 0)
+ {
+ count--;
+ }
+
+ goto TryAgain; // guaranteed never to loop more than once
+ }
+ else
+ {
+ ThrowHelper.ThrowArgumentOutOfRangeException(ExceptionArgument.startIndex, ExceptionResource.ArgumentOutOfRange_Index);
+ }
}
- // Make sure we're not out of range
- if (startIndex < 0 || startIndex > source.Length)
+ startIndex = startIndex - count + 1; // this will be the actual index where we begin our search
+
+ if (!source.TryGetSpan(startIndex, count, out ReadOnlySpan<char> sourceSpan))
{
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
+ ThrowHelper.ThrowCountArgumentOutOfRange_ArgumentOutOfRange_Count();
}
- // Make sure that we allow startIndex == source.Length
- if (startIndex == source.Length)
+ int retVal = LastIndexOf(sourceSpan, value, options);
+ if (retVal >= 0)
{
- startIndex--;
- if (count > 0)
+ retVal += startIndex;
+ }
+ return retVal;
+ }
+
+ /// <summary>
+ /// Searches for the last occurrence of a substring within a source string.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="value">The substring to locate within <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
+ /// <returns>
+ /// The zero-based index into <paramref name="source"/> where the substring <paramref name="value"/>
+ /// last appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public unsafe int LastIndexOf(ReadOnlySpan<char> source, ReadOnlySpan<char> value, CompareOptions options = CompareOptions.None)
+ {
+ if ((options & ValidIndexMaskOffFlags) == 0)
+ {
+ // Common case: caller is attempting to perform a linguistic search.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
+
+ if (!GlobalizationMode.Invariant)
{
- count--;
+ if (value.IsEmpty)
+ {
+ return source.Length; // Empty target string trivially occurs at the last index of every search space.
+ }
+ else
+ {
+ return IndexOfCore(source, value, options, null /* matchLengthPtr */, fromBeginning: false);
+ }
}
-
- // empty substrings trivially occur at the end of the search space
- if (value.Length == 0 && count >= 0 && startIndex - count + 1 >= 0)
+ else if ((options & CompareOptions.IgnoreCase) == 0)
{
- return startIndex + 1;
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
}
}
-
- // 2nd half of this also catches when startIndex == MAXINT, so MAXINT - 0 + 1 == -1, which is < 0.
- if (count < 0 || startIndex - count + 1 < 0)
+ else
{
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
- }
+ // Less common case: caller is attempting to perform non-linguistic comparison,
+ // or an invalid combination of flags was supplied.
- if (options == CompareOptions.OrdinalIgnoreCase)
- {
- return LastIndexOfOrdinal(source, value, startIndex, count, ignoreCase: true);
+ if (options == CompareOptions.Ordinal)
+ {
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ throw new ArgumentException(
+ paramName: nameof(options),
+ message: SR.Argument_InvalidFlag);
+ }
}
- if (GlobalizationMode.Invariant)
- return InvariantLastIndexOf(source, value, startIndex, count, (options & (CompareOptions.IgnoreCase | CompareOptions.OrdinalIgnoreCase)) != 0);
+ ReturnOrdinal:
+ return source.LastIndexOf(value);
- return LastIndexOfCore(source, value, startIndex, count, options);
+ ReturnOrdinalIgnoreCase:
+ return IndexOfOrdinalIgnoreCase(source, value, fromBeginning: false);
}
- private int LastIndexOfCore(string source, string target, int startIndex, int count, CompareOptions options) =>
- GlobalizationMode.UseNls ?
- NlsLastIndexOfCore(source, target, startIndex, count, options) :
- IcuLastIndexOfCore(source, target, startIndex, count, options);
+ /// <summary>
+ /// Searches for the last occurrence of a <see cref="Rune"/> within a source string.
+ /// </summary>
+ /// <param name="source">The string to search within.</param>
+ /// <param name="value">The <see cref="Rune"/> to locate within <paramref name="source"/>.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> to use during the search.</param>
+ /// <returns>
+ /// The zero-based index into <paramref name="source"/> where <paramref name="value"/>
+ /// last appears; or -1 if <paramref name="value"/> cannot be found within <paramref name="source"/>.
+ /// </returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported combination of flags.
+ /// </exception>
+ public unsafe int LastIndexOf(ReadOnlySpan<char> source, Rune value, CompareOptions options = CompareOptions.None)
+ {
+ Span<char> valueAsUtf16 = stackalloc char[Rune.MaxUtf16CharsPerRune];
+ int charCount = value.EncodeToUtf16(valueAsUtf16);
+ return LastIndexOf(source, valueAsUtf16.Slice(0, charCount), options);
+ }
internal static int LastIndexOfOrdinal(string source, string value, int startIndex, int count, bool ignoreCase)
{
NlsCreateSortKey(source, options) :
IcuCreateSortKey(source, options);
+ /// <summary>
+ /// Computes a sort key over the specified input.
+ /// </summary>
+ /// <param name="source">The text over which to compute the sort key.</param>
+ /// <param name="destination">The buffer into which to write the resulting sort key bytes.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> used for computing the sort key.</param>
+ /// <returns>The number of bytes written to <paramref name="destination"/>.</returns>
+ /// <remarks>
+ /// Use <see cref="GetSortKeyLength"/> to query the required size of <paramref name="destination"/>.
+ /// It is acceptable to provide a larger-than-necessary output buffer to this method.
+ /// </remarks>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="destination"/> is too small to contain the resulting sort key;
+ /// or <paramref name="options"/> contains an unsupported flag;
+ /// or <paramref name="source"/> cannot be processed using the desired <see cref="CompareOptions"/>
+ /// under the current <see cref="CompareInfo"/>.
+ /// </exception>
+ public int GetSortKey(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options = CompareOptions.None)
+ {
+ if ((options & ValidCompareMaskOffFlags) != 0)
+ {
+ ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
+ }
+
+ if (GlobalizationMode.Invariant)
+ {
+ return InvariantGetSortKey(source, destination, options);
+ }
+ else
+ {
+ return GetSortKeyCore(source, destination, options);
+ }
+ }
+
+ private int GetSortKeyCore(ReadOnlySpan<char> source, Span<byte> destination, CompareOptions options) =>
+ GlobalizationMode.UseNls ?
+ NlsGetSortKey(source, destination, options) :
+ IcuGetSortKey(source, destination, options);
+
+ /// <summary>
+ /// Returns the length (in bytes) of the sort key that would be produced from the specified input.
+ /// </summary>
+ /// <param name="source">The text over which to compute the sort key.</param>
+ /// <param name="options">The <see cref="CompareOptions"/> used for computing the sort key.</param>
+ /// <returns>The length (in bytes) of the sort key.</returns>
+ /// <exception cref="ArgumentException">
+ /// <paramref name="options"/> contains an unsupported flag;
+ /// or <paramref name="source"/> cannot be processed using the desired <see cref="CompareOptions"/>
+ /// under the current <see cref="CompareInfo"/>.
+ /// </exception>
+ public int GetSortKeyLength(ReadOnlySpan<char> source, CompareOptions options = CompareOptions.None)
+ {
+ if ((options & ValidCompareMaskOffFlags) != 0)
+ {
+ ThrowHelper.ThrowArgumentException(ExceptionResource.Argument_InvalidFlag, ExceptionArgument.options);
+ }
+
+ if (GlobalizationMode.Invariant)
+ {
+ return InvariantGetSortKeyLength(source, options);
+ }
+ else
+ {
+ return GetSortKeyLengthCore(source, options);
+ }
+ }
+
+ private int GetSortKeyLengthCore(ReadOnlySpan<char> source, CompareOptions options) =>
+ GlobalizationMode.UseNls ?
+ NlsGetSortKeyLength(source, options) :
+ IcuGetSortKeyLength(source, options);
+
public override bool Equals(object? value)
{
return value is CompareInfo otherCompareInfo
{
if (source == null)
{
- throw new ArgumentNullException(nameof(source));
+ ThrowHelper.ThrowArgumentNullException(ExceptionArgument.source);
}
- if ((options & ValidCompareMaskOffFlags) == 0)
- {
- // No unsupported flags are set - continue on with the regular logic
- if (GlobalizationMode.Invariant)
- {
- return ((options & CompareOptions.IgnoreCase) != 0) ? source.GetHashCodeOrdinalIgnoreCase() : source.GetHashCode();
- }
- return GetHashCodeOfStringCore(source, options);
- }
- else if (options == CompareOptions.Ordinal)
- {
- // We allow Ordinal in isolation
- return source.GetHashCode();
- }
- else if (options == CompareOptions.OrdinalIgnoreCase)
- {
- // We allow OrdinalIgnoreCase in isolation
- return source.GetHashCodeOrdinalIgnoreCase();
- }
- else
- {
- // Unsupported combination of flags specified
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
- }
+ return GetHashCode(source.AsSpan(), options);
}
public int GetHashCode(ReadOnlySpan<char> source, CompareOptions options)
{
if ((options & ValidCompareMaskOffFlags) == 0)
{
- // No unsupported flags are set - continue on with the regular logic
- if (GlobalizationMode.Invariant)
+ // Common case: caller is attempting to get a linguistic sort key.
+ // Pass the flags down to NLS or ICU unless we're running in invariant
+ // mode, at which point we normalize the flags to Orginal[IgnoreCase].
+
+ if (!GlobalizationMode.Invariant)
{
- return ((options & CompareOptions.IgnoreCase) != 0) ? string.GetHashCodeOrdinalIgnoreCase(source) : string.GetHashCode(source);
+ return GetHashCodeOfStringCore(source, options);
+ }
+ else if ((options & CompareOptions.IgnoreCase) == 0)
+ {
+ goto ReturnOrdinal;
+ }
+ else
+ {
+ goto ReturnOrdinalIgnoreCase;
}
-
- return GetHashCodeOfStringCore(source, options);
- }
- else if (options == CompareOptions.Ordinal)
- {
- // We allow Ordinal in isolation
- return string.GetHashCode(source);
- }
- else if (options == CompareOptions.OrdinalIgnoreCase)
- {
- // We allow OrdinalIgnoreCase in isolation
- return string.GetHashCodeOrdinalIgnoreCase(source);
}
else
{
- // Unsupported combination of flags specified
- throw new ArgumentException(SR.Argument_InvalidFlag, nameof(options));
+ // Less common case: caller is attempting to get a non-linguistic sort key,
+ // or an invalid combination of flags was supplied.
+
+ if (options == CompareOptions.Ordinal)
+ {
+ goto ReturnOrdinal;
+ }
+ else if (options == CompareOptions.OrdinalIgnoreCase)
+ {
+ goto ReturnOrdinalIgnoreCase;
+ }
+ else
+ {
+ ThrowCompareOptionsCheckFailed(options);
+ }
}
+
+ ReturnOrdinal:
+ return string.GetHashCode(source);
+
+ ReturnOrdinalIgnoreCase:
+ return string.GetHashCodeOrdinalIgnoreCase(source);
}
private unsafe int GetHashCodeOfStringCore(ReadOnlySpan<char> source, CompareOptions options) =>
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
+using Internal.Runtime.CompilerServices;
namespace System
{
switch (comparisonType)
{
case StringComparison.CurrentCulture:
- return CultureInfo.CurrentCulture.CompareInfo.CompareOptionNone(span, other) == 0;
-
case StringComparison.CurrentCultureIgnoreCase:
- return CultureInfo.CurrentCulture.CompareInfo.CompareOptionIgnoreCase(span, other) == 0;
+ return CultureInfo.CurrentCulture.CompareInfo.Compare(span, other, string.GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
case StringComparison.InvariantCulture:
- return CompareInfo.Invariant.CompareOptionNone(span, other) == 0;
-
case StringComparison.InvariantCultureIgnoreCase:
- return CompareInfo.Invariant.CompareOptionIgnoreCase(span, other) == 0;
+ return CompareInfo.Invariant.Compare(span, other, string.GetCaseCompareOfComparisonCulture(comparisonType)) == 0;
case StringComparison.Ordinal:
return EqualsOrdinal(span, other);
- case StringComparison.OrdinalIgnoreCase:
+ default:
+ Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
return EqualsOrdinalIgnoreCase(span, other);
}
-
- Debug.Fail("StringComparison outside range");
- return false;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
switch (comparisonType)
{
case StringComparison.CurrentCulture:
- return CultureInfo.CurrentCulture.CompareInfo.CompareOptionNone(span, other);
-
case StringComparison.CurrentCultureIgnoreCase:
- return CultureInfo.CurrentCulture.CompareInfo.CompareOptionIgnoreCase(span, other);
+ return CultureInfo.CurrentCulture.CompareInfo.Compare(span, other, string.GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.InvariantCulture:
- return CompareInfo.Invariant.CompareOptionNone(span, other);
-
case StringComparison.InvariantCultureIgnoreCase:
- return CompareInfo.Invariant.CompareOptionIgnoreCase(span, other);
+ return CompareInfo.Invariant.Compare(span, other, string.GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.Ordinal:
if (span.Length == 0 || other.Length == 0)
return span.Length - other.Length;
return string.CompareOrdinal(span, other);
- case StringComparison.OrdinalIgnoreCase:
+ default:
+ Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
return CompareInfo.CompareOrdinalIgnoreCase(span, other);
}
-
- Debug.Fail("StringComparison outside range");
- return 0;
}
/// <summary>
{
string.CheckStringComparison(comparisonType);
- if (value.Length == 0)
- {
- return 0; // empty substring trivially occurs at every index (including start) of search space
- }
-
- if (span.Length == 0)
- {
- return -1;
- }
-
if (comparisonType == StringComparison.Ordinal)
{
return SpanHelpers.IndexOf(
value.Length);
}
- if (GlobalizationMode.Invariant)
- {
- return CompareInfo.InvariantIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
- }
-
switch (comparisonType)
{
case StringComparison.CurrentCulture:
default:
Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
- return CompareInfo.Invariant.IndexOfOrdinalIgnoreCase(span, value);
+ return CompareInfo.IndexOfOrdinalIgnoreCase(span, value, fromBeginning: true);
}
}
{
string.CheckStringComparison(comparisonType);
- if (value.Length == 0)
- {
- return span.Length; // empty substring trivially occurs at every index (including end) of search space
- }
-
- if (span.Length == 0)
- {
- return -1;
- }
-
- if (GlobalizationMode.Invariant)
+ if (comparisonType == StringComparison.Ordinal)
{
- return CompareInfo.InvariantIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None, fromBeginning: false);
+ return SpanHelpers.LastIndexOf(
+ ref MemoryMarshal.GetReference(span),
+ span.Length,
+ ref MemoryMarshal.GetReference(value),
+ value.Length);
}
switch (comparisonType)
return CompareInfo.Invariant.LastIndexOf(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
default:
- Debug.Assert(comparisonType == StringComparison.Ordinal || comparisonType == StringComparison.OrdinalIgnoreCase);
- return CompareInfo.Invariant.LastIndexOfOrdinal(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
+ Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
+ return CompareInfo.IndexOfOrdinalIgnoreCase(span, value, fromBeginning: false);
}
}
{
string.CheckStringComparison(comparisonType);
- if (value.Length == 0)
+ switch (comparisonType)
{
- return true; // the empty string is trivially a suffix of every other string
- }
+ case StringComparison.CurrentCulture:
+ case StringComparison.CurrentCultureIgnoreCase:
+ return CultureInfo.CurrentCulture.CompareInfo.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
- if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
- {
- if (string.GetCaseCompareOfComparisonCulture(comparisonType) == CompareOptions.None)
- return span.EndsWith(value);
+ case StringComparison.InvariantCulture:
+ case StringComparison.InvariantCultureIgnoreCase:
+ return CompareInfo.Invariant.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
- return (span.Length >= value.Length) ? (CompareInfo.CompareOrdinalIgnoreCase(span.Slice(span.Length - value.Length), value) == 0) : false;
- }
+ case StringComparison.Ordinal:
+ return span.EndsWith(value);
- if (span.Length == 0)
- {
- return false;
+ default:
+ Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
+ return span.EndsWithOrdinalIgnoreCase(value);
}
-
- return (comparisonType >= StringComparison.InvariantCulture) ?
- CompareInfo.Invariant.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType)) :
- CultureInfo.CurrentCulture.CompareInfo.IsSuffix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool EndsWithOrdinalIgnoreCase(this ReadOnlySpan<char> span, ReadOnlySpan<char> value)
+ => value.Length <= span.Length
+ && CompareInfo.EqualsOrdinalIgnoreCase(
+ ref Unsafe.Add(ref MemoryMarshal.GetReference(span), span.Length - value.Length),
+ ref MemoryMarshal.GetReference(value),
+ value.Length);
+
/// <summary>
/// Determines whether the beginning of the <paramref name="span"/> matches the specified <paramref name="value"/> when compared using the specified <paramref name="comparisonType"/> option.
/// </summary>
{
string.CheckStringComparison(comparisonType);
- if (value.Length == 0)
+ switch (comparisonType)
{
- return true; // the empty string is trivially a prefix of every other string
- }
+ case StringComparison.CurrentCulture:
+ case StringComparison.CurrentCultureIgnoreCase:
+ return CultureInfo.CurrentCulture.CompareInfo.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
- if (comparisonType >= StringComparison.Ordinal || GlobalizationMode.Invariant)
- {
- if (string.GetCaseCompareOfComparisonCulture(comparisonType) == CompareOptions.None)
- return span.StartsWith(value);
+ case StringComparison.InvariantCulture:
+ case StringComparison.InvariantCultureIgnoreCase:
+ return CompareInfo.Invariant.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
- return (span.Length >= value.Length) ? (CompareInfo.CompareOrdinalIgnoreCase(span.Slice(0, value.Length), value) == 0) : false;
- }
+ case StringComparison.Ordinal:
+ return span.StartsWith(value);
- if (span.Length == 0)
- {
- return false;
+ default:
+ Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
+ return span.StartsWithOrdinalIgnoreCase(value);
}
-
- return (comparisonType >= StringComparison.InvariantCulture) ?
- CompareInfo.Invariant.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType)) :
- CultureInfo.CurrentCulture.CompareInfo.IsPrefix(span, value, string.GetCaseCompareOfComparisonCulture(comparisonType));
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal static bool StartsWithOrdinalIgnoreCase(this ReadOnlySpan<char> span, ReadOnlySpan<char> value)
+ => value.Length <= span.Length
+ && CompareInfo.EqualsOrdinalIgnoreCase(ref MemoryMarshal.GetReference(span), ref MemoryMarshal.GetReference(value), value.Length);
+
/// <summary>
/// Returns an enumeration of <see cref="Rune"/> from the provided span.
/// </summary>
public string Replace(string oldValue, string? newValue, bool ignoreCase, CultureInfo? culture)
{
- return ReplaceCore(oldValue, newValue, culture, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
+ return ReplaceCore(oldValue, newValue, culture?.CompareInfo, ignoreCase ? CompareOptions.IgnoreCase : CompareOptions.None);
}
public string Replace(string oldValue, string? newValue, StringComparison comparisonType)
{
case StringComparison.CurrentCulture:
case StringComparison.CurrentCultureIgnoreCase:
- return ReplaceCore(oldValue, newValue, CultureInfo.CurrentCulture, GetCaseCompareOfComparisonCulture(comparisonType));
+ return ReplaceCore(oldValue, newValue, CultureInfo.CurrentCulture.CompareInfo, GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.InvariantCulture:
case StringComparison.InvariantCultureIgnoreCase:
- return ReplaceCore(oldValue, newValue, CultureInfo.InvariantCulture, GetCaseCompareOfComparisonCulture(comparisonType));
+ return ReplaceCore(oldValue, newValue, CompareInfo.Invariant, GetCaseCompareOfComparisonCulture(comparisonType));
case StringComparison.Ordinal:
return Replace(oldValue, newValue);
case StringComparison.OrdinalIgnoreCase:
- return ReplaceCore(oldValue, newValue, CultureInfo.InvariantCulture, CompareOptions.OrdinalIgnoreCase);
+ return ReplaceCore(oldValue, newValue, CompareInfo.Invariant, CompareOptions.OrdinalIgnoreCase);
default:
throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
}
}
- private unsafe string ReplaceCore(string oldValue, string? newValue, CultureInfo? culture, CompareOptions options)
+ private string ReplaceCore(string oldValue, string? newValue, CompareInfo? ci, CompareOptions options)
{
- if (oldValue == null)
+ if (oldValue is null)
+ {
throw new ArgumentNullException(nameof(oldValue));
+ }
+
if (oldValue.Length == 0)
+ {
throw new ArgumentException(SR.Argument_StringZeroLength, nameof(oldValue));
+ }
// If they asked to replace oldValue with a null, replace all occurrences
- // with the empty string.
- newValue ??= string.Empty;
+ // with the empty string. AsSpan() will normalize appropriately.
+ //
+ // If inner ReplaceCore method returns null, it means no substitutions were
+ // performed, so as an optimization we'll return the original string.
- CultureInfo referenceCulture = culture ?? CultureInfo.CurrentCulture;
- var result = new ValueStringBuilder(stackalloc char[256]);
- result.EnsureCapacity(this.Length);
+ return ReplaceCore(this, oldValue.AsSpan(), newValue.AsSpan(), ci ?? CultureInfo.CurrentCulture.CompareInfo, options)
+ ?? this;
+ }
- int startIndex = 0;
- int index = 0;
+ private static unsafe string? ReplaceCore(ReadOnlySpan<char> searchSpace, ReadOnlySpan<char> oldValue, ReadOnlySpan<char> newValue, CompareInfo compareInfo, CompareOptions options)
+ {
+ Debug.Assert(!oldValue.IsEmpty);
+ Debug.Assert(compareInfo != null);
- int matchLength = 0;
+ var result = new ValueStringBuilder(stackalloc char[256]);
+ result.EnsureCapacity(searchSpace.Length);
+ int matchLength = 0;
bool hasDoneAnyReplacements = false;
- CompareInfo ci = referenceCulture.CompareInfo;
- do
+ while (true)
{
- index = ci.IndexOf(this, oldValue, startIndex, this.Length - startIndex, options, &matchLength);
+ int index = compareInfo.IndexOf(searchSpace, oldValue, &matchLength, options, fromBeginning: true);
// There's the possibility that 'oldValue' has zero collation weight (empty string equivalent).
// If this is the case, we behave as if there are no more substitutions to be made.
- if (index >= 0 && matchLength > 0)
+ if (index < 0 || matchLength == 0)
{
- // append the unmodified portion of string
- result.Append(this.AsSpan(startIndex, index - startIndex));
+ break;
+ }
- // append the replacement
- result.Append(newValue);
+ // append the unmodified portion of search space
+ result.Append(searchSpace.Slice(0, index));
- startIndex = index + matchLength;
- hasDoneAnyReplacements = true;
- }
- else if (!hasDoneAnyReplacements)
- {
- // small optimization,
- // if we have not done any replacements,
- // we will return the original string
- result.Dispose();
- return this;
- }
- else
- {
- result.Append(this.AsSpan(startIndex, this.Length - startIndex));
- }
- } while (index >= 0);
+ // append the replacement
+ result.Append(newValue);
+
+ searchSpace = searchSpace.Slice(index + matchLength);
+ hasDoneAnyReplacements = true;
+ }
+
+ // Didn't find 'oldValue' in the remaining search space, or the match
+ // consisted only of zero collation weight characters. As an optimization,
+ // if we have not yet performed any replacements, we'll save the
+ // allocation.
+
+ if (!hasDoneAnyReplacements)
+ {
+ result.Dispose();
+ return null;
+ }
+
+ // Append what remains of the search space, then allocate the new string.
+ result.Append(searchSpace);
return result.ToString();
}
public int IndexOf(string value, int startIndex, int count)
{
- if (startIndex < 0 || startIndex > this.Length)
- {
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
- }
-
- if (count < 0 || count > this.Length - startIndex)
- {
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
- }
-
return IndexOf(value, startIndex, count, StringComparison.CurrentCulture);
}
public int IndexOf(string value, int startIndex, int count, StringComparison comparisonType)
{
- // Validate inputs
- if (value == null)
- throw new ArgumentNullException(nameof(value));
-
- if (startIndex < 0 || startIndex > this.Length)
- throw new ArgumentOutOfRangeException(nameof(startIndex), SR.ArgumentOutOfRange_Index);
-
- if (count < 0 || startIndex > this.Length - count)
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
-
- if (comparisonType == StringComparison.Ordinal)
- {
- int result = SpanHelpers.IndexOf(
- ref Unsafe.Add(ref this._firstChar, startIndex),
- count,
- ref value._firstChar,
- value.Length);
-
- return (result >= 0 ? startIndex : 0) + result;
- }
+ // Parameter checking will be done by CompareInfo.IndexOf.
switch (comparisonType)
{
case StringComparison.InvariantCultureIgnoreCase:
return CompareInfo.Invariant.IndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType));
+ case StringComparison.Ordinal:
case StringComparison.OrdinalIgnoreCase:
- return CompareInfo.IndexOfOrdinal(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType) != CompareOptions.None);
+ return CompareInfo.Invariant.IndexOf(this, value, startIndex, count, GetCompareOptionsFromOrdinalStringComparison(comparisonType));
default:
- throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
+ throw (value is null)
+ ? new ArgumentNullException(nameof(value))
+ : new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType));
}
}
public int LastIndexOf(string value, int startIndex, int count)
{
- if (count < 0)
- {
- throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_Count);
- }
-
return LastIndexOf(value, startIndex, count, StringComparison.CurrentCulture);
}
public static implicit operator ReadOnlySpan<char>(string? value) =>
value != null ? new ReadOnlySpan<char>(ref value.GetRawStringData(), value.Length) : default;
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal bool TryGetSpan(int startIndex, int count, out ReadOnlySpan<char> slice)
+ {
+#if TARGET_64BIT
+ // See comment in Span<T>.Slice for how this works.
+ if ((ulong)(uint)startIndex + (ulong)(uint)count > (ulong)(uint)Length)
+ {
+ slice = default;
+ return false;
+ }
+#else
+ if ((uint)startIndex > (uint)Length || (uint)count > (uint)(Length - startIndex))
+ {
+ slice = default;
+ return false;
+ }
+#endif
+
+ slice = new ReadOnlySpan<char>(ref Unsafe.Add(ref _firstChar, startIndex), count);
+ return true;
+ }
+
public object Clone()
{
return this;
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public readonly struct Rune : IComparable<Rune>, IEquatable<Rune>
{
+ internal const int MaxUtf16CharsPerRune = 2; // supplementary plane code points are encoded as 2 UTF-16 code units
+ internal const int MaxUtf8BytesPerRune = 4; // supplementary plane code points are encoded as 4 UTF-8 code units
+
private const char HighSurrogateStart = '\ud800';
private const char LowSurrogateStart = '\udc00';
private const int HighSurrogateRange = 0x3FF;
/// <remarks>
/// The return value will be 1 or 2.
/// </remarks>
- public int Utf16SequenceLength => UnicodeUtility.GetUtf16SequenceLength(_value);
+ public int Utf16SequenceLength
+ {
+ get
+ {
+ int codeUnitCount = UnicodeUtility.GetUtf16SequenceLength(_value);
+ Debug.Assert(codeUnitCount > 0 && codeUnitCount <= MaxUtf16CharsPerRune);
+ return codeUnitCount;
+ }
+ }
/// <summary>
/// Returns the length in code units of the
/// <remarks>
/// The return value will be 1 through 4, inclusive.
/// </remarks>
- public int Utf8SequenceLength => UnicodeUtility.GetUtf8SequenceLength(_value);
+ public int Utf8SequenceLength
+ {
+ get
+ {
+ int codeUnitCount = UnicodeUtility.GetUtf8SequenceLength(_value);
+ Debug.Assert(codeUnitCount > 0 && codeUnitCount <= MaxUtf8BytesPerRune);
+ return codeUnitCount;
+ }
+ }
/// <summary>
/// Returns the Unicode scalar value as an integer.
Debug.Assert(!GlobalizationMode.Invariant, "This should've been checked by the caller.");
Debug.Assert(textInfo != null, "This should've been checked by the caller.");
- Span<char> original = stackalloc char[2]; // worst case scenario = 2 code units (for a surrogate pair)
- Span<char> modified = stackalloc char[2]; // case change should preserve UTF-16 code unit count
+ Span<char> original = stackalloc char[MaxUtf16CharsPerRune];
+ Span<char> modified = stackalloc char[MaxUtf16CharsPerRune];
int charCount = rune.EncodeToUtf16(original);
original = original.Slice(0, charCount);
Debug.Assert(!GlobalizationMode.Invariant, "This should've been checked by the caller.");
Debug.Assert(culture != null, "This should've been checked by the caller.");
- Span<char> original = stackalloc char[2]; // worst case scenario = 2 code units (for a surrogate pair)
- Span<char> modified = stackalloc char[2]; // case change should preserve UTF-16 code unit count
+ Span<char> original = stackalloc char[MaxUtf16CharsPerRune]; // worst case scenario = 2 code units (for a surrogate pair)
+ Span<char> modified = stackalloc char[MaxUtf16CharsPerRune]; // case change should preserve UTF-16 code unit count
int charCount = rune.EncodeToUtf16(original);
original = original.Slice(0, charCount);
}
else
{
- Span<char> buffer = stackalloc char[2];
+ Span<char> buffer = stackalloc char[MaxUtf16CharsPerRune];
UnicodeUtility.GetUtf16SurrogatesFromSupplementaryPlaneScalar(_value, out buffer[0], out buffer[1]);
return buffer.ToString();
}
}
else
{
- idx = compareInfo.IndexOf(thisTranscodedToUtf16, otherTranscodedToUtf16, 0, thisTranscodedToUtf16.Length, compareOptions, &matchLength, fromBeginning);
+ idx = compareInfo.IndexOf(thisTranscodedToUtf16, otherTranscodedToUtf16, &matchLength, compareOptions, fromBeginning);
}
#else
Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase);
return "codePoint";
case ExceptionArgument.str:
return "str";
+ case ExceptionArgument.options:
+ return "options";
+ case ExceptionArgument.prefix:
+ return "prefix";
+ case ExceptionArgument.suffix:
+ return "suffix";
default:
Debug.Fail("The enum value is not defined, please check the ExceptionArgument Enum.");
return "";
return SR.Arg_TypeNotSupported;
case ExceptionResource.Argument_SpansMustHaveSameLength:
return SR.Argument_SpansMustHaveSameLength;
+ case ExceptionResource.Argument_InvalidFlag:
+ return SR.Argument_InvalidFlag;
default:
Debug.Fail("The enum value is not defined, please check the ExceptionResource Enum.");
return "";
year,
codePoint,
str,
+ options,
+ prefix,
+ suffix,
}
//
Rank_MultiDimNotSupported,
Arg_TypeNotSupported,
Argument_SpansMustHaveSameLength,
+ Argument_InvalidFlag,
}
}
public int LCID { get { throw null; } }
public string Name { get { throw null; } }
public System.Globalization.SortVersion Version { get { throw null; } }
+ public int Compare(System.ReadOnlySpan<char> string1, System.ReadOnlySpan<char> string2, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2) { throw null; }
public int Compare(string? string1, int offset1, int length1, string? string2, int offset2, int length2, System.Globalization.CompareOptions options) { throw null; }
public int Compare(string? string1, int offset1, string? string2, int offset2) { throw null; }
public int GetHashCode(string source, System.Globalization.CompareOptions options) { throw null; }
public System.Globalization.SortKey GetSortKey(string source) { throw null; }
public System.Globalization.SortKey GetSortKey(string source, System.Globalization.CompareOptions options) { throw null; }
+ public int GetSortKey(System.ReadOnlySpan<char> source, System.Span<byte> destination, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
+ public int GetSortKeyLength(System.ReadOnlySpan<char> source, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
public int IndexOf(string source, char value) { throw null; }
public int IndexOf(string source, char value, System.Globalization.CompareOptions options) { throw null; }
public int IndexOf(string source, char value, int startIndex) { throw null; }
public int IndexOf(string source, string value, int startIndex, System.Globalization.CompareOptions options) { throw null; }
public int IndexOf(string source, string value, int startIndex, int count) { throw null; }
public int IndexOf(string source, string value, int startIndex, int count, System.Globalization.CompareOptions options) { throw null; }
+ public int IndexOf(System.ReadOnlySpan<char> source, System.ReadOnlySpan<char> value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
+ public int IndexOf(System.ReadOnlySpan<char> source, System.Text.Rune value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
public bool IsPrefix(string source, string prefix) { throw null; }
public bool IsPrefix(string source, string prefix, System.Globalization.CompareOptions options) { throw null; }
+ public bool IsPrefix(System.ReadOnlySpan<char> source, System.ReadOnlySpan<char> prefix, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
public static bool IsSortable(char ch) { throw null; }
+ public static bool IsSortable(System.ReadOnlySpan<char> text) { throw null; }
public static bool IsSortable(string text) { throw null; }
+ public static bool IsSortable(System.Text.Rune value) { throw null; }
public bool IsSuffix(string source, string suffix) { throw null; }
public bool IsSuffix(string source, string suffix, System.Globalization.CompareOptions options) { throw null; }
+ public bool IsSuffix(System.ReadOnlySpan<char> source, System.ReadOnlySpan<char> suffix, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
public int LastIndexOf(string source, char value) { throw null; }
public int LastIndexOf(string source, char value, System.Globalization.CompareOptions options) { throw null; }
public int LastIndexOf(string source, char value, int startIndex) { throw null; }
public int LastIndexOf(string source, string value, int startIndex, System.Globalization.CompareOptions options) { throw null; }
public int LastIndexOf(string source, string value, int startIndex, int count) { throw null; }
public int LastIndexOf(string source, string value, int startIndex, int count, System.Globalization.CompareOptions options) { throw null; }
+ public int LastIndexOf(System.ReadOnlySpan<char> source, System.ReadOnlySpan<char> value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
+ public int LastIndexOf(System.ReadOnlySpan<char> source, System.Text.Rune value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; }
void System.Runtime.Serialization.IDeserializationCallback.OnDeserialization(object sender) { }
public override string ToString() { throw null; }
}