From 9a781f13e08d944343397565a959a17772d7b4d5 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Wed, 12 Aug 2020 16:31:12 -0700 Subject: [PATCH 1/3] Add matchLength-returning APIs to CompareInfo --- .../Common/src/Interop/Interop.Collation.cs | 4 +- .../pal_collation.c | 68 +++-- .../pal_collation.h | 6 +- .../pal_icushim_internal.h | 2 + .../pal_icushim_internal_android.h | 1 + .../CompareInfo/CompareInfoTests.IndexOf.cs | 217 ++++++++------ .../CompareInfo/CompareInfoTests.IsPrefix.cs | 114 ++++--- .../CompareInfo/CompareInfoTests.IsSuffix.cs | 120 ++++---- .../CompareInfoTests.LastIndexOf.cs | 179 ++++++----- .../tests/Invariant/InvariantMode.cs | 108 ++++++- .../System/Globalization/CompareInfo.Icu.cs | 64 +++- .../System/Globalization/CompareInfo.Nls.cs | 28 +- .../src/System/Globalization/CompareInfo.cs | 282 +++++++++++++++++- .../src/System/Text/Utf8Span.Searching.cs | 4 +- .../System.Runtime/ref/System.Runtime.cs | 4 + 15 files changed, 860 insertions(+), 341 deletions(-) diff --git a/src/libraries/Common/src/Interop/Interop.Collation.cs b/src/libraries/Common/src/Interop/Interop.Collation.cs index e352149d27e03d..ce7f411b473b8c 100644 --- a/src/libraries/Common/src/Interop/Interop.Collation.cs +++ b/src/libraries/Common/src/Interop/Interop.Collation.cs @@ -32,11 +32,11 @@ internal static partial class Globalization [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_StartsWith")] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern unsafe bool StartsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options); + internal static extern unsafe bool StartsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* pMatchedLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_EndsWith")] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern unsafe bool EndsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options); + internal static extern unsafe bool EndsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* pMatchedLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_StartsWith")] [return: MarshalAs(UnmanagedType.Bool)] diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c index 6317f1da8d0030..f9de0314e05b71 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c @@ -492,7 +492,7 @@ int32_t GlobalizationNative_IndexOf( result = GlobalizationNative_CompareString(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options); if (result == UCOL_EQUAL && pMatchedLength != NULL) { - *pMatchedLength = cwTargetLength; + *pMatchedLength = cwSourceLength; } return (result == UCOL_EQUAL) ? 0 : -1; @@ -551,7 +551,7 @@ int32_t GlobalizationNative_LastIndexOf( result = GlobalizationNative_CompareString(pSortHandle, lpTarget, cwTargetLength, lpSource, cwSourceLength, options); if (result == UCOL_EQUAL && pMatchedLength != NULL) { - *pMatchedLength = cwTargetLength; + *pMatchedLength = cwSourceLength; } return (result == UCOL_EQUAL) ? 0 : -1; @@ -689,13 +689,14 @@ static int32_t GetCollationElementMask(UColAttributeValue strength) } } -static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator, UCollationElements* pSourceIterator, UColAttributeValue strength, int32_t forwardSearch) +static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator, UCollationElements* pSourceIterator, UColAttributeValue strength, int32_t forwardSearch, int32_t captureOffset, int32_t* pCapturedOffset) { assert(strength >= UCOL_SECONDARY); UErrorCode errorCode = U_ZERO_ERROR; int32_t movePattern = TRUE, moveSource = TRUE; int32_t patternElement = UCOL_IGNORABLE, sourceElement = UCOL_IGNORABLE; + int32_t capturedOffset = 0; int32_t collationElementMask = GetCollationElementMask(strength); @@ -707,6 +708,10 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator } if (moveSource) { + if (captureOffset) + { + capturedOffset = ucol_getOffset(pSourceIterator); // need to capture offset before advancing iterator + } sourceElement = forwardSearch ? ucol_next(pSourceIterator, &errorCode) : ucol_previous(pSourceIterator, &errorCode); } movePattern = TRUE; moveSource = TRUE; @@ -715,11 +720,11 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator { if (sourceElement == UCOL_NULLORDER) { - return TRUE; // source is equal to pattern, we have reached both ends|beginnings at the same time + goto ReturnTrue; // source is equal to pattern, we have reached both ends|beginnings at the same time } else if (sourceElement == UCOL_IGNORABLE) { - return TRUE; // the next|previous character in source is an ignorable character, an example: "o\u0000".StartsWith("o") + goto ReturnTrue; // the next|previous character in source is an ignorable character, an example: "o\u0000".StartsWith("o") } else if (forwardSearch && ((sourceElement & UCOL_PRIMARYORDERMASK) == 0) && (sourceElement & UCOL_SECONDARYORDERMASK) != 0) { @@ -727,7 +732,7 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator } else { - return TRUE; + goto ReturnTrue; } } else if (patternElement == UCOL_IGNORABLE) @@ -743,9 +748,17 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator return FALSE; } } + +ReturnTrue: + if (captureOffset) + { + assert(pCapturedOffset != NULL); + *pCapturedOffset = capturedOffset; + } + return TRUE; } -static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t forwardSearch) +static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t forwardSearch, int32_t* pMatchedLength) { int32_t result = FALSE; @@ -757,7 +770,15 @@ static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, c { UColAttributeValue strength = ucol_getStrength(pCollator); - result = SimpleAffix_Iterators(pPatternIterator, pSourceIterator, strength, forwardSearch); + int32_t capturedOffset = 0; + result = SimpleAffix_Iterators(pPatternIterator, pSourceIterator, strength, forwardSearch, (pMatchedLength != NULL) /* captureOffset */, &capturedOffset); + + if (result && pMatchedLength != NULL) + { + // depending on whether we're searching forward or backward, the matching substring + // is [start of source string .. curIdx] or [curIdx .. end of source string] + *pMatchedLength = (forwardSearch) ? capturedOffset : (textLength - capturedOffset); + } ucol_closeElements(pSourceIterator); } @@ -768,7 +789,7 @@ static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, c return result; } -static int32_t ComplexStartsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength) +static int32_t ComplexStartsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t* pMatchedLength) { int32_t result = FALSE; @@ -786,6 +807,12 @@ static int32_t ComplexStartsWith(const UCollator* pCollator, UErrorCode* pErrorC { result = CanIgnoreAllCollationElements(pCollator, pText, idx); } + + if (result && pMatchedLength != NULL) + { + // adjust matched length to account for all the elements we implicitly consumed at beginning of string + *pMatchedLength = idx + usearch_getMatchedLength(pSearch); + } } usearch_close(pSearch); @@ -803,9 +830,9 @@ int32_t GlobalizationNative_StartsWith( int32_t cwTargetLength, const UChar* lpSource, int32_t cwSourceLength, - int32_t options) + int32_t options, + int32_t* pMatchedLength) { - UErrorCode err = U_ZERO_ERROR; const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); @@ -815,15 +842,15 @@ int32_t GlobalizationNative_StartsWith( } else if (options > CompareOptionsIgnoreCase) { - return ComplexStartsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength); + return ComplexStartsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, pMatchedLength); } else { - return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, TRUE); + return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, TRUE, pMatchedLength); } } -static int32_t ComplexEndsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength) +static int32_t ComplexEndsWith(const UCollator* pCollator, UErrorCode* pErrorCode, const UChar* pPattern, int32_t patternLength, const UChar* pText, int32_t textLength, int32_t* pMatchedLength) { int32_t result = FALSE; @@ -846,6 +873,12 @@ static int32_t ComplexEndsWith(const UCollator* pCollator, UErrorCode* pErrorCod result = CanIgnoreAllCollationElements(pCollator, pText + matchEnd, remainingStringLength); } + + if (result && pMatchedLength != NULL) + { + // adjust matched length to account for all the elements we implicitly consumed at end of string + *pMatchedLength = textLength - idx; + } } usearch_close(pSearch); @@ -863,7 +896,8 @@ int32_t GlobalizationNative_EndsWith( int32_t cwTargetLength, const UChar* lpSource, int32_t cwSourceLength, - int32_t options) + int32_t options, + int32_t* pMatchedLength) { UErrorCode err = U_ZERO_ERROR; const UCollator* pCollator = GetCollatorFromSortHandle(pSortHandle, options, &err); @@ -874,11 +908,11 @@ int32_t GlobalizationNative_EndsWith( } else if (options > CompareOptionsIgnoreCase) { - return ComplexEndsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength); + return ComplexEndsWith(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, pMatchedLength); } else { - return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, FALSE); + return SimpleAffix(pCollator, &err, lpTarget, cwTargetLength, lpSource, cwSourceLength, FALSE, pMatchedLength); } } diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.h index e31a779175cd5a..64aa8f195342a2 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.h @@ -50,14 +50,16 @@ PALEXPORT int32_t GlobalizationNative_StartsWith(SortHandle* pSortHandle, int32_t cwTargetLength, const UChar* lpSource, int32_t cwSourceLength, - int32_t options); + int32_t options, + int32_t* pMatchedLength); PALEXPORT int32_t GlobalizationNative_EndsWith(SortHandle* pSortHandle, const UChar* lpTarget, int32_t cwTargetLength, const UChar* lpSource, int32_t cwSourceLength, - int32_t options); + int32_t options, + int32_t* pMatchedLength); PALEXPORT int32_t GlobalizationNative_GetSortKey(SortHandle* pSortHandle, const UChar* lpStr, diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h index 46d8064b2d1a73..abe0f38ec0e4b8 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal.h @@ -73,6 +73,7 @@ PER_FUNCTION_BLOCK(ucal_set, libicui18n) \ PER_FUNCTION_BLOCK(ucol_close, libicui18n) \ PER_FUNCTION_BLOCK(ucol_closeElements, libicui18n) \ + PER_FUNCTION_BLOCK(ucol_getOffset, libicui18n) \ PER_FUNCTION_BLOCK(ucol_getRules, libicui18n) \ PER_FUNCTION_BLOCK(ucol_getSortKey, libicui18n) \ PER_FUNCTION_BLOCK(ucol_getStrength, libicui18n) \ @@ -199,6 +200,7 @@ FOR_ALL_ICU_FUNCTIONS #define ucal_set(...) ucal_set_ptr(__VA_ARGS__) #define ucol_close(...) ucol_close_ptr(__VA_ARGS__) #define ucol_closeElements(...) ucol_closeElements_ptr(__VA_ARGS__) +#define ucol_getOffset(...) ucol_getOffset_ptr(__VA_ARGS__) #define ucol_getRules(...) ucol_getRules_ptr(__VA_ARGS__) #define ucol_getSortKey(...) ucol_getSortKey_ptr(__VA_ARGS__) #define ucol_getStrength(...) ucol_getStrength_ptr(__VA_ARGS__) diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h index b07088537e6f5a..15ce08db2af6fb 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_icushim_internal_android.h @@ -438,6 +438,7 @@ UCalendar * ucal_open(const UChar * zoneID, int32_t len, const char * locale, UC void ucal_set(UCalendar * cal, UCalendarDateFields field, int32_t value); void ucol_close(UCollator * coll); void ucol_closeElements(UCollationElements * elems); +int32_t ucol_getOffset(const UCollationElements *elems); const UChar * ucol_getRules(const UCollator * coll, int32_t * length); int32_t ucol_getSortKey(const UCollator * coll, const UChar * source, int32_t sourceLength, uint8_t * result, int32_t resultLength); UCollationStrength ucol_getStrength(const UCollator * coll); diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs index 9a83e22a255191..a5c4e420c65d0c 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs @@ -19,102 +19,108 @@ public class CompareInfoIndexOfTests public static IEnumerable IndexOf_TestData() { // Empty string - yield return new object[] { s_invariantCompare, "foo", "", 0, 3, CompareOptions.None, 0 }; - yield return new object[] { s_invariantCompare, "foo", "", 2, 1, CompareOptions.None, 2 }; - yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0 }; + yield return new object[] { s_invariantCompare, "foo", "", 0, 3, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "foo", "", 2, 1, CompareOptions.None, 2, 0 }; + yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0, 0 }; // OrdinalIgnoreCase - yield return new object[] { s_invariantCompare, "Hello", "l", 0, 5, CompareOptions.OrdinalIgnoreCase, 2 }; - yield return new object[] { s_invariantCompare, "Hello", "L", 0, 5, CompareOptions.OrdinalIgnoreCase, 2 }; - yield return new object[] { s_invariantCompare, "Hello", "h", 0, 5, CompareOptions.OrdinalIgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "Hello", "l", 0, 5, CompareOptions.OrdinalIgnoreCase, 2, 1 }; + yield return new object[] { s_invariantCompare, "Hello", "L", 0, 5, CompareOptions.OrdinalIgnoreCase, 2, 1 }; + yield return new object[] { s_invariantCompare, "Hello", "h", 0, 5, CompareOptions.OrdinalIgnoreCase, 0, 1 }; // Long strings - yield return new object[] { s_invariantCompare, new string('b', 100) + new string('a', 5555), "aaaaaaaaaaaaaaa", 0, 5655, CompareOptions.None, 100 }; - yield return new object[] { s_invariantCompare, new string('b', 101) + new string('a', 5555), new string('a', 5000), 0, 5656, CompareOptions.None, 101 }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", 0, 5555, CompareOptions.None, -1 }; + yield return new object[] { s_invariantCompare, new string('b', 100) + new string('a', 5555), "aaaaaaaaaaaaaaa", 0, 5655, CompareOptions.None, 100, 15 }; + yield return new object[] { s_invariantCompare, new string('b', 101) + new string('a', 5555), new string('a', 5000), 0, 5656, CompareOptions.None, 101, 5000 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", 0, 5555, CompareOptions.None, -1, 0 }; // Hungarian - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.Ordinal, -1 }; + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.Ordinal, -1, 0 }; // Slovak - yield return new object[] { s_slovakCompare, "ch", "h", 0, 2, CompareOptions.None, -1 }; - yield return new object[] { s_slovakCompare, "chodit hore", "HO", 0, 11, CompareOptions.IgnoreCase, 7 }; - yield return new object[] { s_slovakCompare, "chh", "h", 0, 3, CompareOptions.None, 2 }; + yield return new object[] { s_slovakCompare, "ch", "h", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_slovakCompare, "chodit hore", "HO", 0, 11, CompareOptions.IgnoreCase, 7, 2 }; + yield return new object[] { s_slovakCompare, "chh", "h", 0, 3, CompareOptions.None, 2, 1 }; // Turkish - yield return new object[] { s_turkishCompare, "Hi", "I", 0, 2, CompareOptions.None, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "I", 0, 2, CompareOptions.IgnoreCase, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", 0, 2, CompareOptions.None, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", 0, 2, CompareOptions.IgnoreCase, 1 }; - yield return new object[] { s_invariantCompare, "Hi", "I", 0, 2, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Hi", "I", 0, 2, CompareOptions.IgnoreCase, 1 }; - yield return new object[] { s_invariantCompare, "Hi", "\u0130", 0, 2, CompareOptions.IgnoreCase, -1 }; + yield return new object[] { s_turkishCompare, "Hi", "I", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "I", 0, 2, CompareOptions.IgnoreCase, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", 0, 2, CompareOptions.IgnoreCase, 1, 1 }; + yield return new object[] { s_invariantCompare, "Hi", "I", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Hi", "I", 0, 2, CompareOptions.IgnoreCase, 1, 1 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", 0, 2, CompareOptions.IgnoreCase, -1, 0 }; // Unicode - yield return new object[] { s_invariantCompare, "Hi", "\u0130", 0, 2, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 0, 9, CompareOptions.None, 8 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 0, 9, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.IgnoreCase, 8 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.OrdinalIgnoreCase, -1 }; - yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 0, 6, CompareOptions.Ordinal, -1 }; - 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 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", 0, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 0, 9, CompareOptions.None, 8, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 0, 9, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.IgnoreCase, 8, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 0, 9, CompareOptions.OrdinalIgnoreCase, -1, 0 }; + yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 0, 6, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 0, 11, CompareOptions.IgnoreNonSpace, 4, 7 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", 0, 2, CompareOptions.None, -1, 0 }; // 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 }; + yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "hello", "\u200d", 1, 3, CompareOptions.IgnoreCase, 1, 0 }; // 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 }; - yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 0, 13, CompareOptions.None, 2 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 0, 11, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 0, 13, CompareOptions.None, 2, 2 }; // Ordinal should be case-sensitive - yield return new object[] { s_currentCompare, "a", "a", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "a", "A", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "abc", "aBc", 0, 3, CompareOptions.Ordinal, -1 }; + yield return new object[] { s_currentCompare, "a", "a", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "a", "A", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "abc", "aBc", 0, 3, CompareOptions.Ordinal, -1, 0 }; // Ordinal with numbers and symbols - yield return new object[] { s_currentCompare, "a", "1", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "1", "1", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "1", "!", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "a", "-", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "-", "-", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "-", "!", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "!", "!", 0, 1, CompareOptions.Ordinal, 0 }; + yield return new object[] { s_currentCompare, "a", "1", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "1", "1", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "1", "!", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "a", "-", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "-", "-", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "-", "!", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "!", "!", 0, 1, CompareOptions.Ordinal, 0, 1 }; // Ordinal with unicode - yield return new object[] { s_currentCompare, "\uFF21", "\uFE57", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\uFE57", "\uFF21", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\uFF21", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\uFE57", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "a", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "a\u0400Bc", "a", 0, 4, CompareOptions.Ordinal, 0 }; + yield return new object[] { s_currentCompare, "\uFF21", "\uFE57", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\uFE57", "\uFF21", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\uFF21", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\uFE57", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "a", "a\u0400Bc", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "a\u0400Bc", "a", 0, 4, CompareOptions.Ordinal, 0, 1 }; // Ordinal with I or i (American and Turkish) - yield return new object[] { s_currentCompare, "I", "i", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "I", "I", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "i", "I", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "i", "i", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "I", "\u0130", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\u0130", "I", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "i", "\u0130", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\u0130", "i", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "I", "\u0131", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\0131", "I", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "i", "\u0131", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\u0131", "i", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\u0130", "\u0130", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "\u0131", "\u0131", 0, 1, CompareOptions.Ordinal, 0 }; - yield return new object[] { s_currentCompare, "\u0130", "\u0131", 0, 1, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_currentCompare, "\u0131", "\u0130", 0, 1, CompareOptions.Ordinal, -1 }; + yield return new object[] { s_currentCompare, "I", "i", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "I", "I", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "i", "I", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "i", "i", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "I", "\u0130", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\u0130", "I", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "i", "\u0130", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\u0130", "i", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "I", "\u0131", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\0131", "I", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "i", "\u0131", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\u0131", "i", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\u0130", "\u0130", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "\u0131", "\u0131", 0, 1, CompareOptions.Ordinal, 0, 1 }; + yield return new object[] { s_currentCompare, "\u0130", "\u0131", 0, 1, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_currentCompare, "\u0131", "\u0130", 0, 1, CompareOptions.Ordinal, -1, 0 }; // Platform differences - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, PlatformDetection.IsNlsGlobalization ? 5 : -1}; + if (PlatformDetection.IsNlsGlobalization) + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, 5, 7 }; + } else + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, -1, 0 }; + } } public static IEnumerable IndexOf_Aesc_Ligature_TestData() @@ -122,43 +128,43 @@ public static IEnumerable IndexOf_Aesc_Ligature_TestData() bool useNls = PlatformDetection.IsNlsGlobalization; // Searches for the ligature \u00C6 string source1 = "Is AE or ae the same as \u00C6 or \u00E6?"; - yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.None, useNls ? 24 : -1}; - yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.None, useNls ? 9 : -1}; - yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.Ordinal, 9 }; - yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.Ordinal, 24 }; - yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.IgnoreCase, 9 }; - yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.IgnoreCase, 9 }; - yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.IgnoreCase, useNls ? 9 : 24 }; - yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.IgnoreCase, useNls ? 9 : 24 }; + yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.None, useNls ? 24 : -1, useNls ? 1 : 0}; + yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.None, 9 , 2}; + yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.None, useNls ? 9 : -1, useNls ? 2 : 0}; + yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.Ordinal, 9, 2 }; + yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.Ordinal, 24, 1 }; + yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source1, "AE", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; + yield return new object[] { s_invariantCompare, source1, "ae", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; + yield return new object[] { s_invariantCompare, source1, "\u00C6", 8, 18, CompareOptions.IgnoreCase, useNls ? 9 : 24, useNls ? 2 : 1 }; + yield return new object[] { s_invariantCompare, source1, "\u00E6", 8, 18, CompareOptions.IgnoreCase, useNls ? 9 : 24, useNls ? 2 : 1 }; } public static IEnumerable IndexOf_U_WithDiaeresis_TestData() { // Searches for the combining character sequence Latin capital letter U with diaeresis or Latin small letter u with diaeresis. string source = "Is \u0055\u0308 or \u0075\u0308 the same as \u00DC or \u00FC?"; - yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.Ordinal, 9 }; - yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.Ordinal, 24 }; - yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.IgnoreCase, 9 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.IgnoreCase, 9 }; - yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.IgnoreCase, 9 }; - yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.IgnoreCase, 9 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.None, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.None, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.Ordinal, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.Ordinal, 24, 1 }; + yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "\u00DC", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "\u00FC", 8, 18, CompareOptions.IgnoreCase, 9, 2 }; } [Theory] [MemberData(nameof(IndexOf_TestData))] [MemberData(nameof(IndexOf_Aesc_Ligature_TestData))] [MemberData(nameof(IndexOf_U_WithDiaeresis_TestData))] - public void IndexOf_String(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected) + public void IndexOf_String(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected, int expectedMatchLength) { if (value.Length == 1) { @@ -200,9 +206,9 @@ public void IndexOf_String(CompareInfo compareInfo, string source, string value, // 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); + RunSpanIndexOfTest(compareInfo, source.AsSpan(startIndex, count), value, options, (expected < 0) ? expected : expected - startIndex, expectedMatchLength); - static void RunSpanIndexOfTest(CompareInfo compareInfo, ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, int expected) + static void RunSpanIndexOfTest(CompareInfo compareInfo, ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, int expected, int expectedMatchLength) { using BoundedMemory sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source); sourceBoundedMemory.MakeReadonly(); @@ -211,6 +217,8 @@ static void RunSpanIndexOfTest(CompareInfo compareInfo, ReadOnlySpan sourc valueBoundedMemory.MakeReadonly(); Assert.Equal(expected, compareInfo.IndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options)); + Assert.Equal(expected, compareInfo.IndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength)); + Assert.Equal(expectedMatchLength, actualMatchLength); if (TryCreateRuneFrom(value, out Rune rune)) { @@ -251,8 +259,9 @@ private static void IndexOf_Char(CompareInfo compareInfo, string source, char va public void IndexOf_UnassignedUnicode() { bool useNls = PlatformDetection.IsNlsGlobalization; - IndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 0, 6, CompareOptions.None, useNls ? 0 : -1); - IndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 0, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1); + int expectedMatchLength = (useNls) ? 6 : 0; + IndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 0, 6, CompareOptions.None, useNls ? 0 : -1, expectedMatchLength); + IndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 0, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1, expectedMatchLength); } [Fact] @@ -292,6 +301,8 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'b', 0, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'c', 0, 2, CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.StringSort, out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); @@ -299,6 +310,8 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'b', 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'c', 0, 2, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); @@ -306,6 +319,8 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'b', 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'c', 0, 2, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth, out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, (CompareOptions)(-1))); @@ -313,6 +328,8 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', 0, (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', 0, 2, (CompareOptions)(-1))); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)(-1))); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)(-1), out _)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", "Tests", 0, (CompareOptions)0x11111111)); @@ -320,6 +337,8 @@ public void IndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', 0, (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's", 'a', 0, 2, (CompareOptions)0x11111111)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)0x11111111)); + AssertExtensions.Throws("options", () => s_invariantCompare.IndexOf("Test's".AsSpan(), "b".AsSpan(), (CompareOptions)0x11111111, out _)); // StartIndex < 0 AssertExtensions.Throws("startIndex", () => s_invariantCompare.IndexOf("Test", "Test", -1, CompareOptions.None)); diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs index 4ed1684070596a..17c17ab1a8784a 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs @@ -17,81 +17,96 @@ public class CompareInfoIsPrefixTests public static IEnumerable IsPrefix_TestData() { // Empty strings - yield return new object[] { s_invariantCompare, "foo", "", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "", "", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "foo", "", CompareOptions.None, true, 0 }; + yield return new object[] { s_invariantCompare, "", "", CompareOptions.None, true, 0 }; // Long strings - yield return new object[] { s_invariantCompare, new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000), CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", CompareOptions.None, false }; + yield return new object[] { s_invariantCompare, new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true, 15 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000), CompareOptions.None, true, 5000 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", CompareOptions.None, false, 0 }; // Hungarian - yield return new object[] { s_invariantCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false }; - yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "dz", "d", CompareOptions.None, true }; - yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.None, false }; - yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.Ordinal, true }; + yield return new object[] { s_invariantCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "dz", "d", CompareOptions.None, true, 1 }; + yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.None, false, 0 }; + yield return new object[] { s_hungarianCompare, "dz", "d", CompareOptions.Ordinal, true, 1 }; // Turkish - yield return new object[] { s_turkishCompare, "interesting", "I", CompareOptions.None, false }; - yield return new object[] { s_turkishCompare, "interesting", "I", CompareOptions.IgnoreCase, false }; - yield return new object[] { s_turkishCompare, "interesting", "\u0130", CompareOptions.None, false }; - yield return new object[] { s_turkishCompare, "interesting", "\u0130", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "interesting", "I", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "interesting", "I", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "interesting", "\u0130", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "interesting", "\u0130", CompareOptions.IgnoreCase, false }; + yield return new object[] { s_turkishCompare, "interesting", "I", CompareOptions.None, false, 0 }; + yield return new object[] { s_turkishCompare, "interesting", "I", CompareOptions.IgnoreCase, false, 0 }; + yield return new object[] { s_turkishCompare, "interesting", "\u0130", CompareOptions.None, false, 0 }; + yield return new object[] { s_turkishCompare, "interesting", "\u0130", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "interesting", "I", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "interesting", "I", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "interesting", "\u0130", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "interesting", "\u0130", CompareOptions.IgnoreCase, false, 0 }; // Unicode - yield return new object[] { s_invariantCompare, "\u00C0nimal", "A\u0300", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "\u00C0nimal", "A\u0300", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.OrdinalIgnoreCase, false }; - yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true }; - yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false }; - 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 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "A\u0300", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "A\u0300", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "\u00C0nimal", "a\u0300", CompareOptions.OrdinalIgnoreCase, false, 0 }; + yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true, 7 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, true, 1 }; + yield return new object[] { s_invariantCompare, "o\u0000\u0308", "o", CompareOptions.None, true, 1 }; // Weightless comparisons - yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true, 0 }; + yield return new object[] { s_invariantCompare, "\u200dxy", "x", CompareOptions.None, true, 2 }; // 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 }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.Ordinal, true }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.OrdinalIgnoreCase, true }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.None, true, 2 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.IgnoreCase, true, 2 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.Ordinal, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.OrdinalIgnoreCase, true, 1 }; // Malformed Unicode - Invalid Surrogates (there is nothing special about them, they don't have a special treatment) - yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 }; // Ignore symbols - yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true }; - yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false }; + yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; + yield return new object[] { s_invariantCompare, "Test's can be interesting", "Tests", CompareOptions.None, false, 0 }; // Platform differences bool useNls = PlatformDetection.IsNlsGlobalization; - yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, useNls ? true : false }; - yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, useNls ? true : false }; - yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, useNls ? true : false }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, useNls ? true : false }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, useNls ? true : false }; + if (useNls) + { + yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, true, 7 }; + yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, true, 7 }; + yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, true, 1 }; + } + else + { + yield return new object[] { s_hungarianCompare, "dzsdzsfoobar", "ddzsf", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "''Tests", "Tests", CompareOptions.IgnoreSymbols, false, 0 }; + yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800", CompareOptions.IgnoreCase, false, 0 }; + } // ICU bugs // UInt16 overflow: https://unicode-org.atlassian.net/browse/ICU-20832 fixed in https://github.com/unicode-org/icu/pull/840 (ICU 65) if (useNls || PlatformDetection.ICUVersion.Major >= 65) { - yield return new object[] { s_frenchCompare, "b", new string('a', UInt16.MaxValue + 1), CompareOptions.None, false }; + yield return new object[] { s_frenchCompare, "b", new string('a', UInt16.MaxValue + 1), CompareOptions.None, false, 0 }; } + + // We don't validate 'options' for empty prefix + yield return new object[] { s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0 }; } [Theory] [MemberData(nameof(IsPrefix_TestData))] - public void IsPrefix(CompareInfo compareInfo, string source, string value, CompareOptions options, bool expected) + public void IsPrefix(CompareInfo compareInfo, string source, string value, CompareOptions options, bool expected, int expectedMatchLength) { if (options == CompareOptions.None) { @@ -115,14 +130,17 @@ public void IsPrefix(CompareInfo compareInfo, string source, string value, Compa valueBoundedMemory.MakeReadonly(); Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options)); + Assert.Equal(expected, compareInfo.IsPrefix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength)); + Assert.Equal(expectedMatchLength, actualMatchLength); } [Fact] public void IsPrefix_UnassignedUnicode() { bool result = PlatformDetection.IsNlsGlobalization ? true : false; - IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result); - IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result); + int expectedMatchLength = (result) ? 6 : 0; + IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result, expectedMatchLength); + IsPrefix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result, expectedMatchLength); } [Fact] diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs index 361f399f7b1294..06b0f7e4766877 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs @@ -18,82 +18,95 @@ public class CompareInfoIsSuffixTests public static IEnumerable IsSuffix_TestData() { // Empty strings - yield return new object[] { s_invariantCompare, "foo", "", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "", "", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "foo", "", CompareOptions.None, true, 0 }; + yield return new object[] { s_invariantCompare, "", "", CompareOptions.None, true, 0 }; // Long strings - yield return new object[] { s_invariantCompare, new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000), CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", CompareOptions.None, false }; + yield return new object[] { s_invariantCompare, new string('a', 5555), "aaaaaaaaaaaaaaa", CompareOptions.None, true, 15 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000), CompareOptions.None, true, 5000 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", CompareOptions.None, false, 0 }; // Hungarian - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "dz", "z", CompareOptions.None, true }; - yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.None, false }; - yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.Ordinal, true }; + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "dz", "z", CompareOptions.None, true, 1 }; + yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.None, false, 0 }; + yield return new object[] { s_hungarianCompare, "dz", "z", CompareOptions.Ordinal, true, 1 }; // Slovak - yield return new object[] { s_slovakCompare, "ch", "h", CompareOptions.None, false }; - yield return new object[] { s_slovakCompare, "velmi chora", "hora", CompareOptions.None, false }; - yield return new object[] { s_slovakCompare, "chh", "H", CompareOptions.IgnoreCase, true }; + yield return new object[] { s_slovakCompare, "ch", "h", CompareOptions.None, false, 0 }; + yield return new object[] { s_slovakCompare, "velmi chora", "hora", CompareOptions.None, false, 0 }; + yield return new object[] { s_slovakCompare, "chh", "H", CompareOptions.IgnoreCase, true, 1 }; // Turkish - yield return new object[] { s_turkishCompare, "Hi", "I", CompareOptions.None, false }; - yield return new object[] { s_turkishCompare, "Hi", "I", CompareOptions.IgnoreCase, false }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", CompareOptions.None, false }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "Hi", "I", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "Hi", "I", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "Hi", "\u0130", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "Hi", "\u0130", CompareOptions.IgnoreCase, false }; + yield return new object[] { s_turkishCompare, "Hi", "I", CompareOptions.None, false, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "I", CompareOptions.IgnoreCase, false, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", CompareOptions.None, false, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "Hi", "I", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "Hi", "I", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", CompareOptions.IgnoreCase, false, 0 }; // Unicode - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.IgnoreCase, true }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.OrdinalIgnoreCase, false }; - yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true }; - yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false }; - yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, false }; - yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.Ordinal, true }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.IgnoreCase, true, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", CompareOptions.OrdinalIgnoreCase, false, 0 }; + yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "FooBA\u0300R", "FooB\u00C0R", CompareOptions.IgnoreNonSpace, true, 7 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", CompareOptions.Ordinal, false, 0 }; + yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "o\u0308o", "o", CompareOptions.Ordinal, true, 1 }; // Weightless comparisons - yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "", "\u200d", CompareOptions.None, true, 0 }; + yield return new object[] { s_invariantCompare, "xy\u200d", "y", CompareOptions.None, true, 2 }; // 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 }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.Ordinal, true }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.OrdinalIgnoreCase, true }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.None, true, 2 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uD800\uDC00", CompareOptions.IgnoreCase, true, 2 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.Ordinal, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.OrdinalIgnoreCase, true, 1 }; // Malformed Unicode - Invalid Surrogates (there is nothing special about them, they don't have a special treatment) - yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true }; + yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uD800", "\uD800\uD800", CompareOptions.None, true, 2 }; // Ignore symbols - yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true }; - yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.IgnoreSymbols, true, 6 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", CompareOptions.None, false, 0 }; // NULL character - yield return new object[] { s_invariantCompare, "a\u0000b", "a\u0000b", CompareOptions.None, true }; - yield return new object[] { s_invariantCompare, "a\u0000b", "b\u0000b", CompareOptions.None, false }; + yield return new object[] { s_invariantCompare, "a\u0000b", "a\u0000b", CompareOptions.None, true, 3 }; + yield return new object[] { s_invariantCompare, "a\u0000b", "b\u0000b", CompareOptions.None, false, 0 }; // Platform differences - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.None, PlatformDetection.IsIcuGlobalization ? false : true }; - yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, PlatformDetection.IsIcuGlobalization ? false : true }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.None, PlatformDetection.IsIcuGlobalization ? false : true }; - yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.IgnoreCase, PlatformDetection.IsIcuGlobalization ? false : true }; + if (PlatformDetection.IsNlsGlobalization) + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.None, true, 7 }; + yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.None, true, 1 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.IgnoreCase, true, 1 }; + } else + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", CompareOptions.None, false, 0 }; + yield return new object[] { s_frenchCompare, "\u0153", "oe", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.None, false, 0 }; + yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.IgnoreCase, false, 0 }; + } + + // We don't validate 'options' for empty suffix + yield return new object[] { s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0 }; } [Theory] [MemberData(nameof(IsSuffix_TestData))] - public void IsSuffix(CompareInfo compareInfo, string source, string value, CompareOptions options, bool expected) + public void IsSuffix(CompareInfo compareInfo, string source, string value, CompareOptions options, bool expected, int expectedMatchLength) { if (options == CompareOptions.None) { @@ -117,15 +130,18 @@ public void IsSuffix(CompareInfo compareInfo, string source, string value, Compa valueBoundedMemory.MakeReadonly(); Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options)); + Assert.Equal(expected, compareInfo.IsSuffix(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength)); + Assert.Equal(expectedMatchLength, actualMatchLength); } [Fact] public void IsSuffix_UnassignedUnicode() { bool result = PlatformDetection.IsIcuGlobalization ? false : true; + int expectedMatchLength = (result) ? 6 : 0; - IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result); - IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result); + IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.None, result, expectedMatchLength); + IsSuffix(s_invariantCompare, "FooBar", "Foo\uFFFFBar", CompareOptions.IgnoreNonSpace, result, expectedMatchLength); } [Fact] diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs index 31d5e5061943ca..fcfaa4d63845fa 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs @@ -20,79 +20,85 @@ public static IEnumerable 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, "", "a", 0, 0, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0 }; - yield return new object[] { s_invariantCompare, "", "a", -1, 0, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "", "", 0, -1, CompareOptions.None, 0 }; - yield return new object[] { s_invariantCompare, "", "a", 0, -1, CompareOptions.None, -1 }; + yield return new object[] { s_invariantCompare, "foo", "", 2, 3, CompareOptions.None, 3, 0 }; + yield return new object[] { s_invariantCompare, "", "", 0, 0, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "", "a", 0, 0, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "", "", -1, 0, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "", "a", -1, 0, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "", "", 0, -1, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "", "a", 0, -1, CompareOptions.None, -1, 0 }; // Start index = source.Length - yield return new object[] { s_invariantCompare, "Hello", "l", 5, 5, CompareOptions.None, 3 }; - yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1 }; + yield return new object[] { s_invariantCompare, "Hello", "l", 5, 5, CompareOptions.None, 3, 1 }; + yield return new object[] { s_invariantCompare, "Hello", "b", 5, 5, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Hello", "l", 5, 0, CompareOptions.None, -1, 0 }; - yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5 }; - yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5 }; + yield return new object[] { s_invariantCompare, "Hello", "", 5, 5, CompareOptions.None, 5, 0 }; + yield return new object[] { s_invariantCompare, "Hello", "", 5, 0, CompareOptions.None, 5, 0 }; // OrdinalIgnoreCase - yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 }; - yield return new object[] { s_invariantCompare, "Hello", "L", 4, 5, CompareOptions.OrdinalIgnoreCase, 3 }; - yield return new object[] { s_invariantCompare, "Hello", "h", 4, 5, CompareOptions.OrdinalIgnoreCase, 0 }; + yield return new object[] { s_invariantCompare, "Hello", "l", 4, 5, CompareOptions.OrdinalIgnoreCase, 3, 1 }; + yield return new object[] { s_invariantCompare, "Hello", "L", 4, 5, CompareOptions.OrdinalIgnoreCase, 3, 1 }; + yield return new object[] { s_invariantCompare, "Hello", "h", 4, 5, CompareOptions.OrdinalIgnoreCase, 0, 1 }; // Long strings - yield return new object[] { s_invariantCompare, new string('a', 5555) + new string('b', 100), "aaaaaaaaaaaaaaa", 5654, 5655, CompareOptions.None, 5540 }; - yield return new object[] { s_invariantCompare, new string('b', 101) + new string('a', 5555), new string('a', 5000), 5655, 5656, CompareOptions.None, 656 }; - yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", 5554, 5555, CompareOptions.None, -1 }; + yield return new object[] { s_invariantCompare, new string('a', 5555) + new string('b', 100), "aaaaaaaaaaaaaaa", 5654, 5655, CompareOptions.None, 5540, 15 }; + yield return new object[] { s_invariantCompare, new string('b', 101) + new string('a', 5555), new string('a', 5000), 5655, 5656, CompareOptions.None, 656, 5000 }; + yield return new object[] { s_invariantCompare, new string('a', 5555), new string('a', 5000) + "b", 5554, 5555, CompareOptions.None, -1, 0 }; // Hungarian - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.Ordinal, -1 }; + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.Ordinal, -1, 0 }; // Slovak - yield return new object[] { s_slovakCompare, "ch", "h", 0, 1, CompareOptions.None, -1 }; - yield return new object[] { s_slovakCompare, "hore chodit", "HO", 11, 12, CompareOptions.IgnoreCase, 0 }; - yield return new object[] { s_slovakCompare, "chh", "h", 2, 2, CompareOptions.None, 2 }; + yield return new object[] { s_slovakCompare, "ch", "h", 0, 1, CompareOptions.None, -1, 0 }; + yield return new object[] { s_slovakCompare, "hore chodit", "HO", 11, 12, CompareOptions.IgnoreCase, 0, 2 }; + yield return new object[] { s_slovakCompare, "chh", "h", 2, 2, CompareOptions.None, 2, 1 }; // Turkish - yield return new object[] { s_turkishCompare, "Hi", "I", 1, 2, CompareOptions.None, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "I", 1, 2, CompareOptions.IgnoreCase, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", 1, 2, CompareOptions.None, -1 }; - yield return new object[] { s_turkishCompare, "Hi", "\u0130", 1, 2, CompareOptions.IgnoreCase, 1 }; + yield return new object[] { s_turkishCompare, "Hi", "I", 1, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "I", 1, 2, CompareOptions.IgnoreCase, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", 1, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_turkishCompare, "Hi", "\u0130", 1, 2, CompareOptions.IgnoreCase, 1, 1 }; - yield return new object[] { s_invariantCompare, "Hi", "I", 1, 2, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Hi", "I", 1, 2, CompareOptions.IgnoreCase, 1 }; - yield return new object[] { s_invariantCompare, "Hi", "\u0130", 1, 2, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Hi", "\u0130", 1, 2, CompareOptions.IgnoreCase, -1 }; + yield return new object[] { s_invariantCompare, "Hi", "I", 1, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Hi", "I", 1, 2, CompareOptions.IgnoreCase, 1, 1 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", 1, 2, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Hi", "\u0130", 1, 2, CompareOptions.IgnoreCase, -1, 0 }; // Unicode - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 8, 9, CompareOptions.None, 8 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 8, 9, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.None, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.IgnoreCase, 8 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.OrdinalIgnoreCase, -1 }; - yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 5, 6, CompareOptions.Ordinal, -1 }; - 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 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 8, 9, CompareOptions.None, 8, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "A\u0300", 8, 9, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.IgnoreCase, 8, 1 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.OrdinalIgnoreCase, -1, 0 }; + yield return new object[] { s_invariantCompare, "Exhibit \u00C0", "a\u0300", 8, 9, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "FooBar", "Foo\u0400Bar", 5, 6, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, "TestFooBA\u0300R", "FooB\u00C0R", 10, 11, CompareOptions.IgnoreNonSpace, 4, 7 }; + yield return new object[] { s_invariantCompare, "o\u0308", "o", 1, 2, CompareOptions.None, -1, 0 }; // 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 }; + yield return new object[] { s_invariantCompare, "", "\u200d", 0, 0, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "", "\u200d", -1, 0, CompareOptions.None, 0, 0 }; + yield return new object[] { s_invariantCompare, "hello", "\u200d", 4, 5, CompareOptions.IgnoreCase, useNls ? 5 : 4 , 0}; // 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 }; - yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 12, 13, CompareOptions.None, 10 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.IgnoreSymbols, 5, 6 }; + yield return new object[] { s_invariantCompare, "More Test's", "Tests", 10, 11, CompareOptions.None, -1, 0 }; + yield return new object[] { s_invariantCompare, "cbabababdbaba", "ab", 12, 13, CompareOptions.None, 10, 2 }; // Platform differences - yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, PlatformDetection.IsNlsGlobalization ? 5 : -1 }; - + if (PlatformDetection.IsNlsGlobalization) + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, 5, 7 }; + } + else + { + yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, -1, 0 }; + } } public static IEnumerable LastIndexOf_Aesc_Ligature_TestData() @@ -101,42 +107,42 @@ public static IEnumerable LastIndexOf_Aesc_Ligature_TestData() // Searches for the ligature \u00C6 string source = "Is AE or ae the same as \u00C6 or \u00E6?"; - yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.None, useNls ? 24 : -1 }; - yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.None, useNls ? 9 : -1 }; - yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.Ordinal, 9 }; - yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.Ordinal, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.IgnoreCase, useNls ? 24 : 9 }; - yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.IgnoreCase, useNls ? 24 : 9 }; - yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.IgnoreCase, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.IgnoreCase, 24 }; + yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.None, useNls ? 24 : -1, useNls ? 1 : 0 }; + yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.None, 9, 2 }; + yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.None, useNls ? 9 : -1, useNls ? 2 : 0 }; + yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.Ordinal, 9, 2 }; + yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.Ordinal, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "AE", 25, 18, CompareOptions.IgnoreCase, useNls ? 24 : 9, useNls ? 1 : 2 }; + yield return new object[] { s_invariantCompare, source, "ae", 25, 18, CompareOptions.IgnoreCase, useNls ? 24 : 9, useNls ? 1 : 2 }; + yield return new object[] { s_invariantCompare, source, '\u00C6', 25, 18, CompareOptions.IgnoreCase, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00E6', 25, 18, CompareOptions.IgnoreCase, 24, 1 }; } public static IEnumerable LastIndexOf_U_WithDiaeresis_TestData() { // Searches for the combining character sequence Latin capital letter U with diaeresis or Latin small letter u with diaeresis. string source = "Is \u0055\u0308 or \u0075\u0308 the same as \u00DC or \u00FC?"; - yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.None, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.None, 9 }; - yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.Ordinal, 9 }; - yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.Ordinal, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.Ordinal, -1 }; - yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.IgnoreCase, 24 }; - yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.IgnoreCase, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.IgnoreCase, 24 }; - yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.IgnoreCase, 24 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.None, 9, 2 }; + yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.None, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.None, 9, 2 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.Ordinal, 9, 2 }; + yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.Ordinal, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.Ordinal, -1, 0 }; + yield return new object[] { s_invariantCompare, source, "U\u0308", 25, 18, CompareOptions.IgnoreCase, 24, 1 }; + yield return new object[] { s_invariantCompare, source, "u\u0308", 25, 18, CompareOptions.IgnoreCase, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00DC', 25, 18, CompareOptions.IgnoreCase, 24, 1 }; + yield return new object[] { s_invariantCompare, source, '\u00FC', 25, 18, CompareOptions.IgnoreCase, 24, 1 }; } [Theory] [MemberData(nameof(LastIndexOf_TestData))] [MemberData(nameof(LastIndexOf_U_WithDiaeresis_TestData))] - public void LastIndexOf_String(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected) + public void LastIndexOf_String(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected, int expectedMatchLength) { if (value.Length == 1) { @@ -206,9 +212,9 @@ public void LastIndexOf_String(CompareInfo compareInfo, string source, string va // Now test the span-based versions - use BoundedMemory to detect buffer overruns - RunSpanLastIndexOfTest(compareInfo, sourceSpan, value, options, expected - adjustmentFactor); + RunSpanLastIndexOfTest(compareInfo, sourceSpan, value, options, expected - adjustmentFactor, expectedMatchLength); - static void RunSpanLastIndexOfTest(CompareInfo compareInfo, ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, int expected) + static void RunSpanLastIndexOfTest(CompareInfo compareInfo, ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, int expected, int expectedMatchLength) { using BoundedMemory sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source); sourceBoundedMemory.MakeReadonly(); @@ -217,6 +223,8 @@ static void RunSpanLastIndexOfTest(CompareInfo compareInfo, ReadOnlySpan s valueBoundedMemory.MakeReadonly(); Assert.Equal(expected, compareInfo.LastIndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options)); + Assert.Equal(expected, compareInfo.LastIndexOf(sourceBoundedMemory.Span, valueBoundedMemory.Span, options, out int actualMatchLength)); + Assert.Equal(expectedMatchLength, actualMatchLength); if (TryCreateRuneFrom(value, out Rune rune)) { @@ -255,17 +263,18 @@ private static void LastIndexOf_Char(CompareInfo compareInfo, string source, cha [Theory] [MemberData(nameof(LastIndexOf_Aesc_Ligature_TestData))] - public void LastIndexOf_Aesc_Ligature(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected) + public void LastIndexOf_Aesc_Ligature(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int expected, int expectedMatchLength) { - LastIndexOf_String(compareInfo, source, value, startIndex, count, options, expected); + LastIndexOf_String(compareInfo, source, value, startIndex, count, options, expected, expectedMatchLength); } [Fact] public void LastIndexOf_UnassignedUnicode() { bool useNls = PlatformDetection.IsNlsGlobalization; - LastIndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 5, 6, CompareOptions.None, useNls ? 0 : -1); - LastIndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 6, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1); + int expectedMatchLength = (useNls) ? 6 : 0; + LastIndexOf_String(s_invariantCompare, "FooBar", "Foo\uFFFFBar", 5, 6, CompareOptions.None, useNls ? 0 : -1, expectedMatchLength); + LastIndexOf_String(s_invariantCompare, "~FooBar", "Foo\uFFFFBar", 6, 7, CompareOptions.IgnoreNonSpace, useNls ? 1 : -1, expectedMatchLength); } [Fact] @@ -305,6 +314,8 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.StringSort)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.StringSort)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.StringSort, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); @@ -312,6 +323,8 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.Ordinal | CompareOptions.IgnoreWidth, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); @@ -319,6 +332,8 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), CompareOptions.OrdinalIgnoreCase | CompareOptions.IgnoreWidth, out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, (CompareOptions)(-1))); @@ -326,6 +341,8 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, (CompareOptions)(-1))); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)(-1))); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)(-1), out int matchLength)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "Tests", 0, (CompareOptions)0x11111111)); @@ -333,6 +350,8 @@ public void LastIndexOf_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, (CompareOptions)0x11111111)); AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", 'a', 0, 1, (CompareOptions)0x11111111)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)0x11111111)); + AssertExtensions.Throws("options", () => s_invariantCompare.LastIndexOf("Test's", "a".AsSpan(), (CompareOptions)0x11111111, out int matchLength)); // StartIndex < 0 AssertExtensions.Throws("startIndex", () => s_invariantCompare.LastIndexOf("Test", "Test", -1, CompareOptions.None)); diff --git a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs index dccc2db90dedae..94ec2003e3157b 100644 --- a/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs +++ b/src/libraries/System.Globalization/tests/Invariant/InvariantMode.cs @@ -800,9 +800,44 @@ public void TestIndexOf(string source, string value, int startIndex, int count, { foreach (string cul in s_cultureNames) { - Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IndexOf(source, value, startIndex, count, options)); + CompareInfo compareInfo = CultureInfo.GetCultureInfo(cul).CompareInfo; + TestCore(compareInfo, source, value, startIndex, count, options, result); + } + + // static test helper method to avoid mutating input args when called in a loop + static void TestCore(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int result) + { + Assert.Equal(result, compareInfo.IndexOf(source, value, startIndex, count, options)); Assert.Equal(result, source.IndexOf(value, startIndex, count, GetStringComparison(options))); - Assert.Equal((result == -1) ? -1 : (result - startIndex), source.AsSpan(startIndex, count).IndexOf(value.AsSpan(), GetStringComparison(options))); + + // Span versions - using BoundedMemory to check for buffer overruns + + using BoundedMemory sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source.AsSpan(startIndex, count)); + sourceBoundedMemory.MakeReadonly(); + ReadOnlySpan sourceBoundedSpan = sourceBoundedMemory.Span; + + using BoundedMemory valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value); + valueBoundedMemory.MakeReadonly(); + ReadOnlySpan valueBoundedSpan = valueBoundedMemory.Span; + + int offsetResult = result; + if (offsetResult >= 0) + { + offsetResult -= startIndex; // account for span slicing + Assert.True(offsetResult >= 0, "Shouldn't have made an affirmative result go negative."); + } + + Assert.Equal(offsetResult, sourceBoundedSpan.IndexOf(valueBoundedSpan, GetStringComparison(options))); + Assert.Equal(offsetResult, compareInfo.IndexOf(sourceBoundedSpan, valueBoundedSpan, options)); + Assert.Equal(offsetResult, compareInfo.IndexOf(sourceBoundedSpan, valueBoundedSpan, options, out int matchLength)); + if (offsetResult >= 0) + { + Assert.Equal(valueBoundedSpan.Length, matchLength); // Invariant mode should perform non-linguistic comparisons + } + else + { + Assert.Equal(0, matchLength); // not found + } } } @@ -812,14 +847,21 @@ public void TestLastIndexOf(string source, string value, int startIndex, int cou { foreach (string cul in s_cultureNames) { - Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.LastIndexOf(source, value, startIndex, count, options)); + CompareInfo compareInfo = CultureInfo.GetCultureInfo(cul).CompareInfo; + TestCore(compareInfo, source, value, startIndex, count, options, result); + } + + // static test helper method to avoid mutating input args when called in a loop + static void TestCore(CompareInfo compareInfo, string source, string value, int startIndex, int count, CompareOptions options, int result) + { + Assert.Equal(result, compareInfo.LastIndexOf(source, value, startIndex, count, options)); Assert.Equal(result, source.LastIndexOf(value, startIndex, count, GetStringComparison(options))); // Filter differences betweeen string-based and Span-based LastIndexOf // - Empty value handling - https://github.com/dotnet/runtime/issues/13382 // - Negative count if (value.Length == 0 || count < 0) - continue; + return; if (startIndex == source.Length) { @@ -828,7 +870,34 @@ public void TestLastIndexOf(string source, string value, int startIndex, int cou count--; } int leftStartIndex = (startIndex - count + 1); - Assert.Equal((result == -1) ? -1 : (result - leftStartIndex), source.AsSpan(leftStartIndex, count).LastIndexOf(value.AsSpan(), GetStringComparison(options))); + + // Span versions - using BoundedMemory to check for buffer overruns + + using BoundedMemory sourceBoundedMemory = BoundedMemory.AllocateFromExistingData(source.AsSpan(leftStartIndex, count)); + sourceBoundedMemory.MakeReadonly(); + ReadOnlySpan sourceBoundedSpan = sourceBoundedMemory.Span; + + using BoundedMemory valueBoundedMemory = BoundedMemory.AllocateFromExistingData(value); + valueBoundedMemory.MakeReadonly(); + ReadOnlySpan valueBoundedSpan = valueBoundedMemory.Span; + + if (result >= 0) + { + result -= leftStartIndex; // account for span slicing + Assert.True(result >= 0, "Shouldn't have made an affirmative result go negative."); + } + + Assert.Equal(result, sourceBoundedSpan.LastIndexOf(valueBoundedSpan, GetStringComparison(options))); + Assert.Equal(result, compareInfo.LastIndexOf(sourceBoundedSpan, valueBoundedSpan, options)); + Assert.Equal(result, compareInfo.LastIndexOf(sourceBoundedSpan, valueBoundedSpan, options, out int matchLength)); + if (result >= 0) + { + Assert.Equal(valueBoundedSpan.Length, matchLength); // Invariant mode should perform non-linguistic comparisons + } + else + { + Assert.Equal(0, matchLength); // not found + } } } @@ -838,7 +907,9 @@ public void TestIsPrefix(string source, string value, CompareOptions options, bo { foreach (string cul in s_cultureNames) { - Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsPrefix(source, value, options)); + CompareInfo compareInfo = CultureInfo.GetCultureInfo(cul).CompareInfo; + + Assert.Equal(result, compareInfo.IsPrefix(source, value, options)); Assert.Equal(result, source.StartsWith(value, GetStringComparison(options))); // Span versions - using BoundedMemory to check for buffer overruns @@ -852,6 +923,16 @@ public void TestIsPrefix(string source, string value, CompareOptions options, bo ReadOnlySpan valueBoundedSpan = valueBoundedMemory.Span; Assert.Equal(result, sourceBoundedSpan.StartsWith(valueBoundedSpan, GetStringComparison(options))); + Assert.Equal(result, compareInfo.IsPrefix(sourceBoundedSpan, valueBoundedSpan, options)); + Assert.Equal(result, compareInfo.IsPrefix(sourceBoundedSpan, valueBoundedSpan, options, out int matchLength)); + if (result) + { + Assert.Equal(valueBoundedSpan.Length, matchLength); // Invariant mode should perform non-linguistic comparisons + } + else + { + Assert.Equal(0, matchLength); // not found + } } } @@ -861,7 +942,9 @@ public void TestIsSuffix(string source, string value, CompareOptions options, bo { foreach (string cul in s_cultureNames) { - Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsSuffix(source, value, options)); + CompareInfo compareInfo = CultureInfo.GetCultureInfo(cul).CompareInfo; + + Assert.Equal(result, compareInfo.IsSuffix(source, value, options)); Assert.Equal(result, source.EndsWith(value, GetStringComparison(options))); // Span versions - using BoundedMemory to check for buffer overruns @@ -874,8 +957,17 @@ public void TestIsSuffix(string source, string value, CompareOptions options, bo valueBoundedMemory.MakeReadonly(); ReadOnlySpan valueBoundedSpan = valueBoundedMemory.Span; - Assert.Equal(result, CultureInfo.GetCultureInfo(cul).CompareInfo.IsSuffix(sourceBoundedSpan, valueBoundedSpan, options)); Assert.Equal(result, sourceBoundedSpan.EndsWith(valueBoundedSpan, GetStringComparison(options))); + Assert.Equal(result, compareInfo.IsSuffix(sourceBoundedSpan, valueBoundedSpan, options)); + Assert.Equal(result, compareInfo.IsSuffix(sourceBoundedSpan, valueBoundedSpan, options, out int matchLength)); + if (result) + { + Assert.Equal(valueBoundedSpan.Length, matchLength); // Invariant mode should perform non-linguistic comparisons + } + else + { + Assert.Equal(0, matchLength); // not found + } } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index e268c80791dd6d..a1a6ed09f4383d 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -411,7 +411,8 @@ private unsafe int IndexOfOrdinalHelper(ReadOnlySpan source, ReadOnlySpan< } } - private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options) + // this method sets '*matchLengthPtr' (if not nullptr) on exit + private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); Debug.Assert(!GlobalizationMode.UseNls); @@ -419,24 +420,29 @@ private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan Debug.Assert(!prefix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); + if (matchLengthPtr != null) + { + *matchLengthPtr = 0; + } + if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)) { if ((options & CompareOptions.IgnoreCase) != 0) - return StartsWithOrdinalIgnoreCaseHelper(source, prefix, options); + return StartsWithOrdinalIgnoreCaseHelper(source, prefix, options, matchLengthPtr); else - return StartsWithOrdinalHelper(source, prefix, options); + return StartsWithOrdinalHelper(source, prefix, options, matchLengthPtr); } else { fixed (char* pSource = &MemoryMarshal.GetReference(source)) // could be null (or otherwise unable to be dereferenced) fixed (char* pPrefix = &MemoryMarshal.GetReference(prefix)) { - return Interop.Globalization.StartsWith(_sortHandle, pPrefix, prefix.Length, pSource, source.Length, options); + return Interop.Globalization.StartsWith(_sortHandle, pPrefix, prefix.Length, pSource, source.Length, options, matchLengthPtr); } } } - private unsafe bool StartsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options) + private unsafe bool StartsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -499,14 +505,19 @@ private unsafe bool StartsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, if (*a >= 0x80) goto InteropCall; } + + if (matchLengthPtr != null) + { + *matchLengthPtr = prefix.Length; // non-linguistic match doesn't change UTF-16 length + } return true; InteropCall: - return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options); + return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options, matchLengthPtr); } } - private unsafe bool StartsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options) + private unsafe bool StartsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -558,14 +569,20 @@ private unsafe bool StartsWithOrdinalHelper(ReadOnlySpan source, ReadOnlyS if (*a >= 0x80) goto InteropCall; } + + if (matchLengthPtr != null) + { + *matchLengthPtr = prefix.Length; // non-linguistic match doesn't change UTF-16 length + } return true; InteropCall: - return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options); + return Interop.Globalization.StartsWith(_sortHandle, bp, prefix.Length, ap, source.Length, options, matchLengthPtr); } } - private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options) + // this method sets '*matchLengthPtr' (if not nullptr) on exit + private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); Debug.Assert(!GlobalizationMode.UseNls); @@ -573,24 +590,29 @@ private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan su Debug.Assert(!suffix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); + if (matchLengthPtr != null) + { + *matchLengthPtr = 0; + } + if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)) { if ((options & CompareOptions.IgnoreCase) != 0) - return EndsWithOrdinalIgnoreCaseHelper(source, suffix, options); + return EndsWithOrdinalIgnoreCaseHelper(source, suffix, options, matchLengthPtr); else - return EndsWithOrdinalHelper(source, suffix, options); + return EndsWithOrdinalHelper(source, suffix, options, matchLengthPtr); } else { fixed (char* pSource = &MemoryMarshal.GetReference(source)) // could be null (or otherwise unable to be dereferenced) fixed (char* pSuffix = &MemoryMarshal.GetReference(suffix)) { - return Interop.Globalization.EndsWith(_sortHandle, pSuffix, suffix.Length, pSource, source.Length, options); + return Interop.Globalization.EndsWith(_sortHandle, pSuffix, suffix.Length, pSource, source.Length, options, matchLengthPtr); } } } - private unsafe bool EndsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options) + private unsafe bool EndsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -653,14 +675,19 @@ private unsafe bool EndsWithOrdinalIgnoreCaseHelper(ReadOnlySpan source, R if (*a >= 0x80) goto InteropCall; } + + if (matchLengthPtr != null) + { + *matchLengthPtr = suffix.Length; // non-linguistic match doesn't change UTF-16 length + } return true; InteropCall: - return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options); + return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options, matchLengthPtr); } } - private unsafe bool EndsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options) + private unsafe bool EndsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -712,10 +739,15 @@ private unsafe bool EndsWithOrdinalHelper(ReadOnlySpan source, ReadOnlySpa if (*a >= 0x80) goto InteropCall; } + + if (matchLengthPtr != null) + { + *matchLengthPtr = suffix.Length; // non-linguistic match doesn't change UTF-16 length + } return true; InteropCall: - return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options); + return Interop.Globalization.EndsWith(_sortHandle, bp, suffix.Length, ap, source.Length, options, matchLengthPtr); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs index 2031d613d54840..bed1cd9a5db462 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs @@ -317,7 +317,7 @@ private unsafe int NlsIndexOfCore(ReadOnlySpan source, ReadOnlySpan return FindString(positionFlag | (uint)GetNativeCompareFlags(options), source, target, matchLengthPtr); } - private unsafe bool NlsStartsWith(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options) + private unsafe bool NlsStartsWith(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); Debug.Assert(GlobalizationMode.UseNls); @@ -325,10 +325,20 @@ private unsafe bool NlsStartsWith(ReadOnlySpan source, ReadOnlySpan Debug.Assert(!prefix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); - return FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, prefix, null) >= 0; + int idx = FindString(FIND_STARTSWITH | (uint)GetNativeCompareFlags(options), source, prefix, matchLengthPtr); + if (idx >= 0) + { + if (matchLengthPtr != null) + { + *matchLengthPtr += idx; // account for chars we skipped at the front of the string + } + return true; + } + + return false; } - private unsafe bool NlsEndsWith(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options) + private unsafe bool NlsEndsWith(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); Debug.Assert(GlobalizationMode.UseNls); @@ -336,7 +346,17 @@ private unsafe bool NlsEndsWith(ReadOnlySpan source, ReadOnlySpan su Debug.Assert(!suffix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); - return FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, null) >= 0; + int idx = FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, null /* matchLengthPtr */); + if (idx >= 0) + { + if (matchLengthPtr != null) + { + *matchLengthPtr = source.Length - idx; // all chars from idx to the end of the string are consumed + } + return true; + } + + return false; } private const uint LCMAP_SORTKEY = 0x00000400; diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index f68b83530439a8..3bc0bf214ae3da 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -769,7 +769,7 @@ public bool IsPrefix(string source, string prefix, CompareOptions options) /// /// contains an unsupported combination of flags. /// - public bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options = CompareOptions.None) + public unsafe bool IsPrefix(ReadOnlySpan source, ReadOnlySpan 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 @@ -788,7 +788,7 @@ public bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix, Compa if (!GlobalizationMode.Invariant) { - return StartsWithCore(source, prefix, options); + return StartsWithCore(source, prefix, options, null /* matchLengthPtr */); } else if ((options & CompareOptions.IgnoreCase) == 0) { @@ -825,10 +825,108 @@ public bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix, Compa return source.StartsWithOrdinalIgnoreCase(prefix); } - private unsafe bool StartsWithCore(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options) => + /// + /// Determines whether a string starts with a specific prefix. + /// + /// The string to search within. + /// The prefix to attempt to match at the start of . + /// The to use during the match. + /// When this method returns, contains the number of characters of + /// that matched the desired prefix. This may be different than the + /// length of if a linguistic comparison is performed. Set to 0 + /// if the prefix did not match. + /// + /// if occurs at the start of ; + /// otherwise, . + /// + /// + /// contains an unsupported combination of flags. + /// + /// + /// This method has greater overhead than other overloads which don't + /// take a argument. Call this overload only if you require + /// the match length information. + /// + public unsafe bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, out int matchLength) + { + // Just like the existing IsPrefix overloads, we'll allow an early + // exit here before 'options' is validated. + + if (prefix.IsEmpty) + { + matchLength = 0; + return true; + } + + 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) + { + int tempMatchLength; + if (StartsWithCore(source, prefix, options, &tempMatchLength)) + { + Debug.Assert(tempMatchLength >= 0 && tempMatchLength <= source.Length); + matchLength = tempMatchLength; + return true; + } + else + { + matchLength = default; + return false; + } + } + else if ((options & CompareOptions.IgnoreCase) == 0) + { + goto ReturnOrdinal; + } + else + { + goto ReturnOrdinalIgnoreCase; + } + } + else + { + // Less common case: caller is attempting to perform non-linguistic comparison, + // 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: + bool retVal = source.StartsWith(prefix); + goto OrdinalReturn; + + ReturnOrdinalIgnoreCase: + retVal = source.StartsWithOrdinalIgnoreCase(prefix); + + 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. + + matchLength = (retVal) ? prefix.Length : 0; + return retVal; + } + + private unsafe bool StartsWithCore(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) => GlobalizationMode.UseNls ? - NlsStartsWith(source, prefix, options) : - IcuStartsWith(source, prefix, options); + NlsStartsWith(source, prefix, options, matchLengthPtr) : + IcuStartsWith(source, prefix, options, matchLengthPtr); public bool IsPrefix(string source, string prefix) { @@ -866,7 +964,7 @@ public bool IsSuffix(string source, string suffix, CompareOptions options) /// /// contains an unsupported combination of flags. /// - public bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options = CompareOptions.None) + public unsafe bool IsSuffix(ReadOnlySpan source, ReadOnlySpan 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 @@ -885,7 +983,7 @@ public bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix, Compa if (!GlobalizationMode.Invariant) { - return EndsWithCore(source, suffix, options); + return EndsWithCore(source, suffix, options, null /* matchLengthPtr */); } else if ((options & CompareOptions.IgnoreCase) == 0) { @@ -922,15 +1020,114 @@ public bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix, Compa return source.EndsWithOrdinalIgnoreCase(suffix); } + /// + /// Determines whether a string ends with a specific suffix. + /// + /// The string to search within. + /// The suffix to attempt to match at the end of . + /// The to use during the match. + /// When this method returns, contains the number of characters of + /// that matched the desired suffix. This may be different than the + /// length of if a linguistic comparison is performed. Set to 0 + /// if the suffix did not match. + /// + /// if occurs at the end of ; + /// otherwise, . + /// + /// + /// contains an unsupported combination of flags. + /// + /// + /// This method has greater overhead than other overloads which don't + /// take a argument. Call this overload only if you require + /// the match length information. + /// + public unsafe bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, out int matchLength) + { + // 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) + { + matchLength = 0; + return true; + } + + 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) + { + int tempMatchLength; + if (EndsWithCore(source, suffix, options, &tempMatchLength)) + { + Debug.Assert(tempMatchLength >= 0 && tempMatchLength <= source.Length); + matchLength = tempMatchLength; + return true; + } + else + { + matchLength = default; + return false; + } + } + else if ((options & CompareOptions.IgnoreCase) == 0) + { + goto ReturnOrdinal; + } + else + { + goto ReturnOrdinalIgnoreCase; + } + } + else + { + // Less common case: caller is attempting to perform non-linguistic comparison, + // 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: + bool retVal = source.EndsWith(suffix); + goto OrdinalReturn; + + ReturnOrdinalIgnoreCase: + retVal = source.EndsWithOrdinalIgnoreCase(suffix); + + 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. + + matchLength = (retVal) ? suffix.Length : 0; + return retVal; + } + public bool IsSuffix(string source, string suffix) { return IsSuffix(source, suffix, CompareOptions.None); } - private unsafe bool EndsWithCore(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options) => + private unsafe bool EndsWithCore(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) => GlobalizationMode.UseNls ? - NlsEndsWith(source, suffix, options) : - IcuEndsWith(source, suffix, options); + NlsEndsWith(source, suffix, options, matchLengthPtr) : + IcuEndsWith(source, suffix, options, matchLengthPtr); /// /// Returns the first index where value is found in string. The @@ -1146,6 +1343,36 @@ public unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, C return IndexOfOrdinalIgnoreCase(source, value, fromBeginning: true); } + /// + /// Searches for the first occurrence of a substring within a source string. + /// + /// The string to search within. + /// The substring to locate within . + /// The to use during the search. + /// When this method returns, contains the number of characters of + /// that matched the desired value. This may be different than the + /// length of if a linguistic comparison is performed. Set to 0 + /// if is not found within . + /// + /// The zero-based index into where the substring + /// first appears; or -1 if cannot be found within . + /// + /// + /// contains an unsupported combination of flags. + /// + /// + /// This method has greater overhead than other overloads which don't + /// take a argument. Call this overload only if you require + /// the match length information. + /// + public unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, out int matchLength) + { + int tempMatchLength; + int retVal = IndexOf(source, value, &tempMatchLength, options, fromBeginning: true); + matchLength = tempMatchLength; + return retVal; + } + /// /// Searches for the first occurrence of a within a source string. /// @@ -1200,8 +1427,9 @@ internal static int IndexOfOrdinalIgnoreCase(ReadOnlySpan source, ReadOnly } /// - /// 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. + /// IndexOf overload used when the caller needs the length of the matching substring. + /// Caller needs to ensure is non-null and points + /// to a valid address. This method will validate . /// internal unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, int* matchLengthPtr, CompareOptions options, bool fromBeginning) { @@ -1584,6 +1812,36 @@ public unsafe int LastIndexOf(ReadOnlySpan source, ReadOnlySpan valu return IndexOfOrdinalIgnoreCase(source, value, fromBeginning: false); } + /// + /// Searches for the last occurrence of a substring within a source string. + /// + /// The string to search within. + /// The substring to locate within . + /// The to use during the search. + /// When this method returns, contains the number of characters of + /// that matched the desired value. This may be different than the + /// length of if a linguistic comparison is performed. Set to 0 + /// if is not found within . + /// + /// The zero-based index into where the substring + /// last appears; or -1 if cannot be found within . + /// + /// + /// contains an unsupported combination of flags. + /// + /// + /// This method has greater overhead than other overloads which don't + /// take a argument. Call this overload only if you require + /// the match length information. + /// + public unsafe int LastIndexOf(ReadOnlySpan source, ReadOnlySpan value, CompareOptions options, out int matchLength) + { + int tempMatchLength; + int retVal = IndexOf(source, value, &tempMatchLength, options, fromBeginning: false); + matchLength = tempMatchLength; + return retVal; + } + /// /// Searches for the last occurrence of a within a source string. /// diff --git a/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.Searching.cs b/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.Searching.cs index e5309a9ecc195f..a004812e61a5a4 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.Searching.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Text/Utf8Span.Searching.cs @@ -280,7 +280,9 @@ private unsafe bool TryFind(Utf8Span value, StringComparison comparisonType, out } else { - idx = compareInfo.IndexOf(thisTranscodedToUtf16, otherTranscodedToUtf16, &matchLength, compareOptions, fromBeginning); + idx = (fromBeginning) + ? compareInfo.IndexOf(thisTranscodedToUtf16, otherTranscodedToUtf16, compareOptions, out matchLength) + : compareInfo.LastIndexOf(thisTranscodedToUtf16, otherTranscodedToUtf16, compareOptions, out matchLength); } #else Debug.Assert(comparisonType == StringComparison.OrdinalIgnoreCase); diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6481c43f380832..ef8bbb78d73bf4 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -6044,6 +6044,7 @@ internal CompareInfo() { } public System.Globalization.SortKey GetSortKey(string source, System.Globalization.CompareOptions options) { throw null; } public int GetSortKeyLength(System.ReadOnlySpan source, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; } public int IndexOf(System.ReadOnlySpan source, System.ReadOnlySpan value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; } + public int IndexOf(System.ReadOnlySpan source, System.ReadOnlySpan value, System.Globalization.CompareOptions options, out int matchLength) { throw null; } public int IndexOf(System.ReadOnlySpan source, System.Text.Rune value, 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; } @@ -6058,6 +6059,7 @@ internal CompareInfo() { } 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 bool IsPrefix(System.ReadOnlySpan source, System.ReadOnlySpan prefix, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; } + public bool IsPrefix(System.ReadOnlySpan source, System.ReadOnlySpan suffix, System.Globalization.CompareOptions options, out int matchLength) { 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 static bool IsSortable(char ch) { throw null; } @@ -6065,9 +6067,11 @@ internal CompareInfo() { } public static bool IsSortable(string text) { throw null; } public static bool IsSortable(System.Text.Rune value) { throw null; } public bool IsSuffix(System.ReadOnlySpan source, System.ReadOnlySpan suffix, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; } + public bool IsSuffix(System.ReadOnlySpan source, System.ReadOnlySpan suffix, System.Globalization.CompareOptions options, out int matchLength) { 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 int LastIndexOf(System.ReadOnlySpan source, System.ReadOnlySpan value, System.Globalization.CompareOptions options = System.Globalization.CompareOptions.None) { throw null; } + public int LastIndexOf(System.ReadOnlySpan source, System.ReadOnlySpan value, System.Globalization.CompareOptions options, out int matchLength) { throw null; } public int LastIndexOf(System.ReadOnlySpan source, System.Text.Rune value, 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; } From 5137178bbd00312d66d536a3ca27d6c19a2da0f1 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Wed, 12 Aug 2020 22:43:35 -0700 Subject: [PATCH 2/3] Make IndexOf private; remove unsafe code from String --- .../src/System/Globalization/CompareInfo.cs | 2 +- .../System.Private.CoreLib/src/System/String.Manipulation.cs | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index 3bc0bf214ae3da..a4b6b9f66aaa92 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -1431,7 +1431,7 @@ internal static int IndexOfOrdinalIgnoreCase(ReadOnlySpan source, ReadOnly /// Caller needs to ensure is non-null and points /// to a valid address. This method will validate . /// - internal unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, int* matchLengthPtr, CompareOptions options, bool fromBeginning) + private unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, int* matchLengthPtr, CompareOptions options, bool fromBeginning) { Debug.Assert(matchLengthPtr != null); *matchLengthPtr = 0; diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs index 3c01bc84d2bb40..87bae29b5e8841 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs @@ -1015,7 +1015,7 @@ private string ReplaceCore(string oldValue, string? newValue, CompareInfo? ci, C ?? this; } - private static unsafe string? ReplaceCore(ReadOnlySpan searchSpace, ReadOnlySpan oldValue, ReadOnlySpan newValue, CompareInfo compareInfo, CompareOptions options) + private static string? ReplaceCore(ReadOnlySpan searchSpace, ReadOnlySpan oldValue, ReadOnlySpan newValue, CompareInfo compareInfo, CompareOptions options) { Debug.Assert(!oldValue.IsEmpty); Debug.Assert(compareInfo != null); @@ -1023,12 +1023,11 @@ private string ReplaceCore(string oldValue, string? newValue, CompareInfo? ci, C var result = new ValueStringBuilder(stackalloc char[256]); result.EnsureCapacity(searchSpace.Length); - int matchLength = 0; bool hasDoneAnyReplacements = false; while (true) { - int index = compareInfo.IndexOf(searchSpace, oldValue, &matchLength, options, fromBeginning: true); + int index = compareInfo.IndexOf(searchSpace, oldValue, options, out int matchLength); // 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. From 73d160c74042ca7b4993d01e120af4a35ede7e36 Mon Sep 17 00:00:00 2001 From: Levi Broderick Date: Thu, 13 Aug 2020 10:53:40 -0700 Subject: [PATCH 3/3] PR feedback --- .../Common/src/Interop/Interop.Collation.cs | 4 +- .../pal_collation.c | 9 +- .../CompareInfo/CompareInfoTests.IndexOf.cs | 12 +- .../CompareInfo/CompareInfoTests.IsPrefix.cs | 16 +- .../CompareInfo/CompareInfoTests.IsSuffix.cs | 16 +- .../CompareInfoTests.LastIndexOf.cs | 9 + .../System/Globalization/CompareInfo.Icu.cs | 14 +- .../System/Globalization/CompareInfo.Nls.cs | 2 +- .../src/System/Globalization/CompareInfo.cs | 159 +++--------------- .../tests/System/StringTests.cs | 5 + 10 files changed, 90 insertions(+), 156 deletions(-) diff --git a/src/libraries/Common/src/Interop/Interop.Collation.cs b/src/libraries/Common/src/Interop/Interop.Collation.cs index ce7f411b473b8c..40e1821d1092ee 100644 --- a/src/libraries/Common/src/Interop/Interop.Collation.cs +++ b/src/libraries/Common/src/Interop/Interop.Collation.cs @@ -32,11 +32,11 @@ internal static partial class Globalization [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_StartsWith")] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern unsafe bool StartsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* pMatchedLength); + internal static extern unsafe bool StartsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* matchedLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_EndsWith")] [return: MarshalAs(UnmanagedType.Bool)] - internal static extern unsafe bool EndsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* pMatchedLength); + internal static extern unsafe bool EndsWith(IntPtr sortHandle, char* target, int cwTargetLength, char* source, int cwSourceLength, CompareOptions options, int* matchedLength); [DllImport(Libraries.GlobalizationNative, CharSet = CharSet.Unicode, EntryPoint = "GlobalizationNative_StartsWith")] [return: MarshalAs(UnmanagedType.Bool)] diff --git a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c index f9de0314e05b71..c75d68ae9953e0 100644 --- a/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c +++ b/src/libraries/Native/Unix/System.Globalization.Native/pal_collation.c @@ -689,7 +689,7 @@ static int32_t GetCollationElementMask(UColAttributeValue strength) } } -static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator, UCollationElements* pSourceIterator, UColAttributeValue strength, int32_t forwardSearch, int32_t captureOffset, int32_t* pCapturedOffset) +static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator, UCollationElements* pSourceIterator, UColAttributeValue strength, int32_t forwardSearch, int32_t* pCapturedOffset) { assert(strength >= UCOL_SECONDARY); @@ -708,7 +708,7 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator } if (moveSource) { - if (captureOffset) + if (pCapturedOffset != NULL) { capturedOffset = ucol_getOffset(pSourceIterator); // need to capture offset before advancing iterator } @@ -750,9 +750,8 @@ static int32_t inline SimpleAffix_Iterators(UCollationElements* pPatternIterator } ReturnTrue: - if (captureOffset) + if (pCapturedOffset != NULL) { - assert(pCapturedOffset != NULL); *pCapturedOffset = capturedOffset; } return TRUE; @@ -771,7 +770,7 @@ static int32_t SimpleAffix(const UCollator* pCollator, UErrorCode* pErrorCode, c UColAttributeValue strength = ucol_getStrength(pCollator); int32_t capturedOffset = 0; - result = SimpleAffix_Iterators(pPatternIterator, pSourceIterator, strength, forwardSearch, (pMatchedLength != NULL) /* captureOffset */, &capturedOffset); + result = SimpleAffix_Iterators(pPatternIterator, pSourceIterator, strength, forwardSearch, (pMatchedLength != NULL) ? &capturedOffset : NULL); if (result && pMatchedLength != NULL) { diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs index a5c4e420c65d0c..ba030eb0d4afe7 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IndexOf.cs @@ -12,6 +12,7 @@ public class CompareInfoIndexOfTests { private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo; private static CompareInfo s_currentCompare = CultureInfo.CurrentCulture.CompareInfo; + private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo; private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo; private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo; private static CompareInfo s_slovakCompare = new CultureInfo("sk-SK").CompareInfo; @@ -117,10 +118,19 @@ public static IEnumerable IndexOf_TestData() if (PlatformDetection.IsNlsGlobalization) { yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, 5, 7 }; - } else + } + else { yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 0, 12, CompareOptions.None, -1, 0 }; } + + // Inputs where matched length does not equal value string length + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 0, 8, CompareOptions.IgnoreNonSpace, 3, 2 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 0, 7, CompareOptions.IgnoreNonSpace, 3, 1 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 0, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 4, 7 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 0, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 0, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 4, 6 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 0, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; } public static IEnumerable IndexOf_Aesc_Ligature_TestData() diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs index 17c17ab1a8784a..067f1403d9182a 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsPrefix.cs @@ -10,6 +10,7 @@ namespace System.Globalization.Tests public class CompareInfoIsPrefixTests { private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo; + private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo; private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo; private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo; private static CompareInfo s_frenchCompare = new CultureInfo("fr-FR").CompareInfo; @@ -100,8 +101,13 @@ public static IEnumerable IsPrefix_TestData() yield return new object[] { s_frenchCompare, "b", new string('a', UInt16.MaxValue + 1), CompareOptions.None, false, 0 }; } - // We don't validate 'options' for empty prefix - yield return new object[] { s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0 }; + // Prefixes where matched length does not equal value string length + yield return new object[] { s_invariantCompare, "dzxyz", "\u01F3", CompareOptions.IgnoreNonSpace, true, 2 }; + yield return new object[] { s_invariantCompare, "\u01F3xyz", "dz", CompareOptions.IgnoreNonSpace, true, 1 }; + yield return new object[] { s_germanCompare, "Strasse xyz", "stra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 7 }; + yield return new object[] { s_germanCompare, "Strasse xyz", "xtra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 }; + yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Strasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 6 }; + yield return new object[] { s_germanCompare, "stra\u00DFe xyz", "Xtrasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 }; } [Theory] @@ -165,5 +171,11 @@ public void IsPrefix_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.IsPrefix("Test's", "Tests", (CompareOptions)0x11111111)); } + + [Fact] + public void IsPrefix_WithEmptyPrefix_DoesNotValidateOptions() + { + IsPrefix(s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0); + } } } diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs index 06b0f7e4766877..4c5fe8d6b5c53a 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.IsSuffix.cs @@ -10,6 +10,7 @@ namespace System.Globalization.Tests public class CompareInfoIsSuffixTests { private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo; + private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo; private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo; private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo; private static CompareInfo s_frenchCompare = new CultureInfo("fr-FR").CompareInfo; @@ -100,8 +101,13 @@ public static IEnumerable IsSuffix_TestData() yield return new object[] { s_invariantCompare, "\uD800\uDC00", "\uDC00", CompareOptions.IgnoreCase, false, 0 }; } - // We don't validate 'options' for empty suffix - yield return new object[] { s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0 }; + // Suffixes where matched length does not equal value string length + yield return new object[] { s_invariantCompare, "xyzdz", "\u01F3", CompareOptions.IgnoreNonSpace, true, 2 }; + yield return new object[] { s_invariantCompare, "xyz\u01F3", "dz", CompareOptions.IgnoreNonSpace, true, 1 }; + yield return new object[] { s_germanCompare, "xyz Strasse", "stra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 7 }; + yield return new object[] { s_germanCompare, "xyz Strasse", "xtra\u00DFe", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 }; + yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Strasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, true, 6 }; + yield return new object[] { s_germanCompare, "xyz stra\u00DFe", "Xtrasse", CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, false, 0 }; } [Theory] @@ -166,5 +172,11 @@ public void IsSuffix_Invalid() AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", (CompareOptions)(-1))); AssertExtensions.Throws("options", () => s_invariantCompare.IsSuffix("Test's", "Tests", (CompareOptions)0x11111111)); } + + [Fact] + public void IsSuffix_WithEmptyPrefix_DoesNotValidateOptions() + { + IsSuffix(s_invariantCompare, "Hello", "", (CompareOptions)(-1), true, 0); + } } } diff --git a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs index fcfaa4d63845fa..3481c6c3f581fc 100644 --- a/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs +++ b/src/libraries/System.Globalization/tests/CompareInfo/CompareInfoTests.LastIndexOf.cs @@ -11,6 +11,7 @@ namespace System.Globalization.Tests public class CompareInfoLastIndexOfTests { private static CompareInfo s_invariantCompare = CultureInfo.InvariantCulture.CompareInfo; + private static CompareInfo s_germanCompare = new CultureInfo("de-DE").CompareInfo; private static CompareInfo s_hungarianCompare = new CultureInfo("hu-HU").CompareInfo; private static CompareInfo s_turkishCompare = new CultureInfo("tr-TR").CompareInfo; private static CompareInfo s_slovakCompare = new CultureInfo("sk-SK").CompareInfo; @@ -99,6 +100,14 @@ public static IEnumerable LastIndexOf_TestData() { yield return new object[] { s_hungarianCompare, "foobardzsdzs", "rddzs", 11, 12, CompareOptions.None, -1, 0 }; } + + // Inputs where matched length does not equal value string length + yield return new object[] { s_invariantCompare, "abcdzxyz", "\u01F3", 7, 8, CompareOptions.IgnoreNonSpace, 3, 2 }; + yield return new object[] { s_invariantCompare, "abc\u01F3xyz", "dz", 6, 7, CompareOptions.IgnoreNonSpace, 3, 1 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "stra\u00DFe", 22, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 12, 7 }; + yield return new object[] { s_germanCompare, "abc Strasse Strasse xyz", "xtra\u00DFe", 22, 23, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Strasse", 20, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, 11, 6 }; + yield return new object[] { s_germanCompare, "abc stra\u00DFe stra\u00DFe xyz", "Xtrasse", 20, 21, CompareOptions.IgnoreCase | CompareOptions.IgnoreNonSpace, -1, 0 }; } public static IEnumerable LastIndexOf_Aesc_Ligature_TestData() diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs index a1a6ed09f4383d..3b959d44e0a013 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Icu.cs @@ -411,7 +411,7 @@ private unsafe int IndexOfOrdinalHelper(ReadOnlySpan source, ReadOnlySpan< } } - // this method sets '*matchLengthPtr' (if not nullptr) on exit + // this method sets '*matchLengthPtr' (if not nullptr) only on success private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -420,11 +420,6 @@ private unsafe bool IcuStartsWith(ReadOnlySpan source, ReadOnlySpan Debug.Assert(!prefix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); - if (matchLengthPtr != null) - { - *matchLengthPtr = 0; - } - if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)) { if ((options & CompareOptions.IgnoreCase) != 0) @@ -581,7 +576,7 @@ private unsafe bool StartsWithOrdinalHelper(ReadOnlySpan source, ReadOnlyS } } - // this method sets '*matchLengthPtr' (if not nullptr) on exit + // this method sets '*matchLengthPtr' (if not nullptr) only on success private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, int* matchLengthPtr) { Debug.Assert(!GlobalizationMode.Invariant); @@ -590,11 +585,6 @@ private unsafe bool IcuEndsWith(ReadOnlySpan source, ReadOnlySpan su Debug.Assert(!suffix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); - if (matchLengthPtr != null) - { - *matchLengthPtr = 0; - } - if (_isAsciiEqualityOrdinal && CanUseAsciiOrdinalForOptions(options)) { if ((options & CompareOptions.IgnoreCase) != 0) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs index bed1cd9a5db462..0e0454bb201e9c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.Nls.cs @@ -346,7 +346,7 @@ private unsafe bool NlsEndsWith(ReadOnlySpan source, ReadOnlySpan su Debug.Assert(!suffix.IsEmpty); Debug.Assert((options & (CompareOptions.Ordinal | CompareOptions.OrdinalIgnoreCase)) == 0); - int idx = FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, null /* matchLengthPtr */); + int idx = FindString(FIND_ENDSWITH | (uint)GetNativeCompareFlags(options), source, suffix, pcchFound: null); if (idx >= 0) { if (matchLengthPtr != null) diff --git a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs index a4b6b9f66aaa92..71e65028c50247 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Globalization/CompareInfo.cs @@ -788,7 +788,7 @@ public unsafe bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix if (!GlobalizationMode.Invariant) { - return StartsWithCore(source, prefix, options, null /* matchLengthPtr */); + return StartsWithCore(source, prefix, options, matchLengthPtr: null); } else if ((options & CompareOptions.IgnoreCase) == 0) { @@ -849,78 +849,27 @@ public unsafe bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix /// public unsafe bool IsPrefix(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, out int matchLength) { - // Just like the existing IsPrefix overloads, we'll allow an early - // exit here before 'options' is validated. + bool matched; - if (prefix.IsEmpty) + if (GlobalizationMode.Invariant || prefix.IsEmpty || (options & ValidIndexMaskOffFlags) != 0) { - matchLength = 0; - return true; - } - - 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]. + // Non-linguistic (ordinal) comparison requested, or options are invalid. + // Delegate to other overload, which validates options and throws on failure. + // If success, non-linguistic matches will always preserve prefix length. - if (!GlobalizationMode.Invariant) - { - int tempMatchLength; - if (StartsWithCore(source, prefix, options, &tempMatchLength)) - { - Debug.Assert(tempMatchLength >= 0 && tempMatchLength <= source.Length); - matchLength = tempMatchLength; - return true; - } - else - { - matchLength = default; - return false; - } - } - else if ((options & CompareOptions.IgnoreCase) == 0) - { - goto ReturnOrdinal; - } - else - { - goto ReturnOrdinalIgnoreCase; - } + matched = IsPrefix(source, prefix, options); + matchLength = (matched) ? prefix.Length : 0; } else { - // Less common case: caller is attempting to perform non-linguistic comparison, - // or an invalid combination of flags was supplied. + // Linguistic comparison requested and we don't need to special-case any args. - if (options == CompareOptions.Ordinal) - { - goto ReturnOrdinal; - } - else if (options == CompareOptions.OrdinalIgnoreCase) - { - goto ReturnOrdinalIgnoreCase; - } - else - { - ThrowCompareOptionsCheckFailed(options); - } + int tempMatchLength = 0; + matched = StartsWithCore(source, prefix, options, &tempMatchLength); + matchLength = tempMatchLength; } - ReturnOrdinal: - bool retVal = source.StartsWith(prefix); - goto OrdinalReturn; - - ReturnOrdinalIgnoreCase: - retVal = source.StartsWithOrdinalIgnoreCase(prefix); - - 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. - - matchLength = (retVal) ? prefix.Length : 0; - return retVal; + return matched; } private unsafe bool StartsWithCore(ReadOnlySpan source, ReadOnlySpan prefix, CompareOptions options, int* matchLengthPtr) => @@ -983,7 +932,7 @@ public unsafe bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix if (!GlobalizationMode.Invariant) { - return EndsWithCore(source, suffix, options, null /* matchLengthPtr */); + return EndsWithCore(source, suffix, options, matchLengthPtr: null); } else if ((options & CompareOptions.IgnoreCase) == 0) { @@ -1044,79 +993,27 @@ public unsafe bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix /// public unsafe bool IsSuffix(ReadOnlySpan source, ReadOnlySpan suffix, CompareOptions options, out int matchLength) { - // 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. + bool matched; - if (suffix.IsEmpty) + if (GlobalizationMode.Invariant || suffix.IsEmpty || (options & ValidIndexMaskOffFlags) != 0) { - matchLength = 0; - return true; - } + // Non-linguistic (ordinal) comparison requested, or options are invalid. + // Delegate to other overload, which validates options and throws on failure. + // If success, non-linguistic matches will always preserve prefix length. - 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) - { - int tempMatchLength; - if (EndsWithCore(source, suffix, options, &tempMatchLength)) - { - Debug.Assert(tempMatchLength >= 0 && tempMatchLength <= source.Length); - matchLength = tempMatchLength; - return true; - } - else - { - matchLength = default; - return false; - } - } - else if ((options & CompareOptions.IgnoreCase) == 0) - { - goto ReturnOrdinal; - } - else - { - goto ReturnOrdinalIgnoreCase; - } + matched = IsSuffix(source, suffix, options); + matchLength = (matched) ? suffix.Length : 0; } else { - // Less common case: caller is attempting to perform non-linguistic comparison, - // or an invalid combination of flags was supplied. + // Linguistic comparison requested and we don't need to special-case any args. - if (options == CompareOptions.Ordinal) - { - goto ReturnOrdinal; - } - else if (options == CompareOptions.OrdinalIgnoreCase) - { - goto ReturnOrdinalIgnoreCase; - } - else - { - ThrowCompareOptionsCheckFailed(options); - } + int tempMatchLength = 0; + matched = EndsWithCore(source, suffix, options, &tempMatchLength); + matchLength = tempMatchLength; } - ReturnOrdinal: - bool retVal = source.EndsWith(suffix); - goto OrdinalReturn; - - ReturnOrdinalIgnoreCase: - retVal = source.EndsWithOrdinalIgnoreCase(suffix); - - 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. - - matchLength = (retVal) ? suffix.Length : 0; - return retVal; + return matched; } public bool IsSuffix(string source, string suffix) @@ -1305,7 +1202,7 @@ public unsafe int IndexOf(ReadOnlySpan source, ReadOnlySpan value, C } else { - return IndexOfCore(source, value, options, null /* matchLengthPtr */, fromBeginning: true); + return IndexOfCore(source, value, options, matchLengthPtr: null, fromBeginning: true); } } else if ((options & CompareOptions.IgnoreCase) == 0) @@ -1772,7 +1669,7 @@ public unsafe int LastIndexOf(ReadOnlySpan source, ReadOnlySpan valu } else { - return IndexOfCore(source, value, options, null /* matchLengthPtr */, fromBeginning: false); + return IndexOfCore(source, value, options, matchLengthPtr: null, fromBeginning: false); } } else if ((options & CompareOptions.IgnoreCase) == 0) diff --git a/src/libraries/System.Runtime/tests/System/StringTests.cs b/src/libraries/System.Runtime/tests/System/StringTests.cs index dabb22f2027ce9..12a0e4ccec91ae 100644 --- a/src/libraries/System.Runtime/tests/System/StringTests.cs +++ b/src/libraries/System.Runtime/tests/System/StringTests.cs @@ -684,6 +684,11 @@ public static IEnumerable Replace_StringComparison_TestData() yield return new object[] { turkishSource, "\u0130", "a", StringComparison.InvariantCulture, "\u0069a" }; yield return new object[] { turkishSource, "\u0130", "a", StringComparison.InvariantCultureIgnoreCase, "\u0069a" }; } + + // To catch regressions when dealing with zero-length "this" inputs + yield return new object[] { "", "x", "y", StringComparison.InvariantCulture, "" }; + yield return new object[] { "", "\u200d", "y", StringComparison.InvariantCulture, "" }; + yield return new object[] { "", "\0", "y", StringComparison.InvariantCulture, "" }; } [Theory]