diff --git a/src/libraries/System.Private.CoreLib/src/System/Char.cs b/src/libraries/System.Private.CoreLib/src/System/Char.cs index 6b7d11bda27386..e8ee0692a82d32 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Char.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Char.cs @@ -129,16 +129,22 @@ public bool Equals(char obj) return m_value == obj; } - internal bool Equals(char right, StringComparison comparisonType) + /// + /// Returns a value that indicates whether the current instance and a specified character are equal using the specified comparison option. + /// + /// The character to compare with the current instance. + /// One of the enumeration values that specifies the rules to use in the comparison. + /// if the current instance and are equal; otherwise, . + public bool Equals(char other, StringComparison comparisonType) { switch (comparisonType) { case StringComparison.Ordinal: - return Equals(right); + return Equals(other); default: - ReadOnlySpan leftCharsSlice = [this]; - ReadOnlySpan rightCharsSlice = [right]; - return leftCharsSlice.Equals(rightCharsSlice, comparisonType); + ReadOnlySpan thisCharsSlice = [this]; + ReadOnlySpan otherCharsSlice = [other]; + return thisCharsSlice.Equals(otherCharsSlice, comparisonType); } } diff --git a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs index cf8e536c5fded1..c97347cdecc89c 100644 --- a/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs +++ b/src/libraries/System.Private.CoreLib/src/System/SpanHelpers.T.cs @@ -1464,6 +1464,10 @@ internal static bool NonPackedContainsValueType(ref T searchSpace, T value, i internal static int IndexOfChar(ref char searchSpace, char value, int length) => IndexOfValueType(ref Unsafe.As(ref searchSpace), (short)value, length); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int LastIndexOfChar(ref char searchSpace, char value, int length) + => LastIndexOfValueType(ref Unsafe.As(ref searchSpace), (short)value, length); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int NonPackedIndexOfChar(ref char searchSpace, char value, int length) => NonPackedIndexOfValueType>(ref Unsafe.As(ref searchSpace), (short)value, length); @@ -1655,6 +1659,10 @@ internal static int NonPackedIndexOfValueType(ref TValue searc internal static int IndexOfAnyChar(ref char searchSpace, char value0, char value1, int length) => IndexOfAnyValueType(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, length); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static int LastIndexOfAnyChar(ref char searchSpace, char value0, char value1, int length) + => LastIndexOfAnyValueType(ref Unsafe.As(ref searchSpace), (short)value0, (short)value1, length); + [MethodImpl(MethodImplOptions.AggressiveInlining)] internal static int IndexOfAnyValueType(ref T searchSpace, T value0, T value1, int length) where T : struct, INumber => IndexOfAnyValueType>(ref searchSpace, value0, value1, length); diff --git a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs index 11e1e950f9d973..f2218eb276603b 100644 --- a/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs +++ b/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs @@ -76,34 +76,84 @@ public int IndexOf(char value, int startIndex) } public int IndexOf(char value, StringComparison comparisonType) + { + return IndexOf(value, 0, comparisonType); + } + + /// + /// Reports the zero-based index of the first occurrence of the specified character in the current String object. + /// Parameters specify the starting search position in the current string and the type of search to use for + /// the specified character. + /// + /// The character to seek. + /// The search starting position. + /// One of the enumeration values that specifies the rules for the search. + /// + /// The zero-based index position of from the start of the current instance + /// if that character is found, or a negative value (e.g. -1) if it is not. + /// + public int IndexOf(char value, int startIndex, StringComparison comparisonType) + { + return IndexOf(value, startIndex, Length - startIndex, comparisonType); + } + + /// + /// Reports the zero-based index of the first occurrence of the specified character in the current String object. + /// Parameters specify the starting search position in the current string, the number of characters in the + /// current string to search, and the type of search to use for the specified character. + /// + /// The character to seek. + /// The search starting position. + /// The number of character positions to examine. + /// One of the enumeration values that specifies the rules for the search. + /// + /// The zero-based index position of from the start of the current instance + /// if that character is found, or a negative value (e.g. -1) if it is not. + /// + public int IndexOf(char value, int startIndex, int count, StringComparison comparisonType) { return comparisonType switch { - StringComparison.CurrentCulture or StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, GetCaseCompareOfComparisonCulture(comparisonType)), - StringComparison.InvariantCulture or StringComparison.InvariantCultureIgnoreCase => CompareInfo.Invariant.IndexOf(this, value, GetCaseCompareOfComparisonCulture(comparisonType)), - StringComparison.Ordinal => IndexOf(value), - StringComparison.OrdinalIgnoreCase => IndexOfCharOrdinalIgnoreCase(value), + StringComparison.CurrentCulture or StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType)), + StringComparison.InvariantCulture or StringComparison.InvariantCultureIgnoreCase => CompareInfo.Invariant.IndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType)), + StringComparison.Ordinal => IndexOf(value, startIndex, count), + StringComparison.OrdinalIgnoreCase => IndexOfCharOrdinalIgnoreCase(value, startIndex, count), _ => throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)), }; } - private int IndexOfCharOrdinalIgnoreCase(char value) + private int IndexOfCharOrdinalIgnoreCase(char value, int startIndex, int count) { - if (!char.IsAscii(value)) + ArgumentOutOfRangeException.ThrowIfNegative(startIndex); + ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, Length); + ArgumentOutOfRangeException.ThrowIfNegative(count); + ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex + count, Length); + + int subIndex; + + if (char.IsAscii(value)) { - return Ordinal.IndexOfOrdinalIgnoreCase(this, new ReadOnlySpan(in value)); + ref char startChar = ref Unsafe.Add(ref _firstChar, startIndex); + + if (char.IsAsciiLetter(value)) + { + char valueLc = (char)(value | 0x20); + char valueUc = (char)(value & ~0x20); + subIndex = PackedSpanHelpers.PackedIndexOfIsSupported + ? PackedSpanHelpers.IndexOfAnyIgnoreCase(ref startChar, valueLc, count) + : SpanHelpers.IndexOfAnyChar(ref startChar, valueLc, valueUc, count); + } + else + { + subIndex = SpanHelpers.IndexOfChar(ref startChar, value, count); + } } - - if (char.IsAsciiLetter(value)) + else { - char valueLc = (char)(value | 0x20); - char valueUc = (char)(value & ~0x20); - return PackedSpanHelpers.PackedIndexOfIsSupported - ? PackedSpanHelpers.IndexOfAnyIgnoreCase(ref _firstChar, valueLc, Length) - : SpanHelpers.IndexOfAnyChar(ref _firstChar, valueLc, valueUc, Length); + subIndex = Ordinal.IndexOfOrdinalIgnoreCase(this.AsSpan(startIndex, count), new ReadOnlySpan(in value)); } - return SpanHelpers.IndexOfChar(ref _firstChar, value, Length); + return subIndex < 0 ? subIndex : startIndex + subIndex; } public int IndexOf(char value, int startIndex, int count) @@ -291,7 +341,7 @@ public int IndexOf(string value, int startIndex, int count, StringComparison com /// The rune to seek. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int IndexOf(Rune value) { @@ -306,7 +356,7 @@ public int IndexOf(Rune value) /// The search starting position. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int IndexOf(Rune value, int startIndex) { @@ -323,7 +373,7 @@ public int IndexOf(Rune value, int startIndex) /// The number of character positions to examine. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int IndexOf(Rune value, int startIndex, int count) { @@ -338,7 +388,7 @@ public int IndexOf(Rune value, int startIndex, int count) /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int IndexOf(Rune value, StringComparison comparisonType) { @@ -355,9 +405,9 @@ public int IndexOf(Rune value, StringComparison comparisonType) /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// - internal int IndexOf(Rune value, int startIndex, StringComparison comparisonType) + public int IndexOf(Rune value, int startIndex, StringComparison comparisonType) { return IndexOf(value, startIndex, Length - startIndex, comparisonType); } @@ -373,9 +423,9 @@ internal int IndexOf(Rune value, int startIndex, StringComparison comparisonType /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the start of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// - internal int IndexOf(Rune value, int startIndex, int count, StringComparison comparisonType) + public int IndexOf(Rune value, int startIndex, int count, StringComparison comparisonType) { ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0); ArgumentOutOfRangeException.ThrowIfLessThan(count, 0); @@ -384,7 +434,7 @@ internal int IndexOf(Rune value, int startIndex, int count, StringComparison com // Convert value to span ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); - int subIndex = this.AsSpan(startIndex..(startIndex + count)).IndexOf(valueChars, comparisonType); + int subIndex = this.AsSpan(startIndex, count).IndexOf(valueChars, comparisonType); return subIndex < 0 ? subIndex : startIndex + subIndex; } @@ -423,6 +473,107 @@ public int LastIndexOf(char value, int startIndex, int count) return result < 0 ? result : result + startSearchAt; } + /// + /// Reports the zero-based index of the last occurrence of the specified character in the current String object. + /// A parameter specifies the type of search to use for the specified character. + /// + /// The character to seek. + /// One of the enumeration values that specifies the rules for the search. + /// + /// The zero-based index position of from the end of the current instance + /// if that character is found, or a negative value (e.g. -1) if it is not. + /// + public int LastIndexOf(char value, StringComparison comparisonType) + { + return LastIndexOf(value, Length - 1, comparisonType); + } + + /// + /// Reports the zero-based index of the last occurrence of the specified character in the current String object. + /// Parameters specify the starting search position in the current string and the type of search to use for + /// the specified character. + /// + /// The character to seek. + /// The search starting position. The search proceeds from toward the beginning of this instance. + /// One of the enumeration values that specifies the rules for the search. + /// + /// The zero-based index position of from the end of the current instance + /// if that character is found, or a negative value (e.g. -1) if it is not. + /// + public int LastIndexOf(char value, int startIndex, StringComparison comparisonType) + { + return LastIndexOf(value, startIndex, startIndex + 1, comparisonType); + } + + /// + /// Reports the zero-based index of the last occurrence of the specified character in the current String object. + /// Parameters specify the starting search position in the current string, the number of characters in the + /// current string to search, and the type of search to use for the specified character. + /// + /// The character to seek. + /// The search starting position. The search proceeds from toward the beginning of this instance. + /// The number of character positions to examine. + /// One of the enumeration values that specifies the rules for the search. + /// + /// The zero-based index position of from the end of the current instance + /// if that character is found, or a negative value (e.g. -1) if it is not. + /// + public int LastIndexOf(char value, int startIndex, int count, StringComparison comparisonType) + { + if (Length == 0) + { + return -1; + } + + return comparisonType switch + { + StringComparison.CurrentCulture or StringComparison.CurrentCultureIgnoreCase => CultureInfo.CurrentCulture.CompareInfo.LastIndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType)), + StringComparison.InvariantCulture or StringComparison.InvariantCultureIgnoreCase => CompareInfo.Invariant.LastIndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType)), + StringComparison.Ordinal => LastIndexOf(value, startIndex, count), + StringComparison.OrdinalIgnoreCase => LastIndexOfCharOrdinalIgnoreCase(value, startIndex, count), + _ => throw new ArgumentException(SR.NotSupported_StringComparison, nameof(comparisonType)), + }; + } + + private int LastIndexOfCharOrdinalIgnoreCase(char value, int startIndex, int count) + { + int startSearchAt = startIndex + 1 - count; + + ArgumentOutOfRangeException.ThrowIfNegative(startIndex); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(startIndex, Length); + ArgumentOutOfRangeException.ThrowIfNegative(count); + ArgumentOutOfRangeException.ThrowIfNegative(startSearchAt); + + int subIndex; + + if (char.IsAscii(value)) + { + ref char startChar = ref Unsafe.Add(ref _firstChar, startSearchAt); + + if (char.IsAsciiLetter(value)) + { + char valueLc = (char)(value | 0x20); + char valueUc = (char)(value & ~0x20); + /* + * Potential optimization possible here if there was a + * PackedSpanHelpers.LastIndexOfAnyIgnoreCase(ref startChar, valueLc, count) + * method, which would be complex to implement + */ + subIndex = SpanHelpers.LastIndexOfAnyChar(ref startChar, valueLc, valueUc, count); + } + else + { + subIndex = SpanHelpers.LastIndexOfChar(ref startChar, value, count); + } + } + else + { + subIndex = Ordinal.LastIndexOfOrdinalIgnoreCase(this.AsSpan(startSearchAt, count), new ReadOnlySpan(in value)); + } + + return subIndex < 0 ? subIndex : startSearchAt + subIndex; + } + // Returns the index of the last occurrence of any specified character in the current instance. // The search starts at startIndex and runs backwards to startIndex - count + 1. // The character at position startIndex is included in the search. startIndex is the larger @@ -519,7 +670,7 @@ public int LastIndexOf(string value, int startIndex, int count, StringComparison /// The rune to seek. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int LastIndexOf(Rune value) { @@ -534,7 +685,7 @@ public int LastIndexOf(Rune value) /// The search starting position. The search proceeds from toward the beginning of this instance. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int LastIndexOf(Rune value, int startIndex) { @@ -551,7 +702,7 @@ public int LastIndexOf(Rune value, int startIndex) /// The number of character positions to examine. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int LastIndexOf(Rune value, int startIndex, int count) { @@ -566,7 +717,7 @@ public int LastIndexOf(Rune value, int startIndex, int count) /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// public int LastIndexOf(Rune value, StringComparison comparisonType) { @@ -583,9 +734,9 @@ public int LastIndexOf(Rune value, StringComparison comparisonType) /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// - internal int LastIndexOf(Rune value, int startIndex, StringComparison comparisonType) + public int LastIndexOf(Rune value, int startIndex, StringComparison comparisonType) { return LastIndexOf(value, startIndex, startIndex + 1, comparisonType); } @@ -601,21 +752,26 @@ internal int LastIndexOf(Rune value, int startIndex, StringComparison comparison /// One of the enumeration values that specifies the rules for the search. /// /// The zero-based index position of from the end of the current instance - /// if that rune is found, or -1 if it is not. + /// if that rune is found, or a negative value (e.g. -1) if it is not. /// - internal int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType) + public int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType) { + if (Length == 0) + { + return -1; + } + ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0); ArgumentOutOfRangeException.ThrowIfLessThan(count, 0); ArgumentOutOfRangeException.ThrowIfLessThan(startIndex - count + 1, 0); - ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, Length - 1); + ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual(startIndex, Length); // Convert value to span ReadOnlySpan valueChars = value.AsSpan(stackalloc char[Rune.MaxUtf16CharsPerRune]); int startIndexFromZero = startIndex - count + 1; - int subIndex = this.AsSpan(startIndexFromZero..(startIndexFromZero + count)).LastIndexOf(valueChars, comparisonType); + int subIndex = this.AsSpan(startIndexFromZero, count).LastIndexOf(valueChars, comparisonType); return subIndex < 0 ? subIndex : startIndexFromZero + subIndex; } } diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 6869aeb397ceb0..fd7d1260f388d0 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -1089,6 +1089,7 @@ public CannotUnloadAppDomainException(string? message, System.Exception? innerEx public static int ConvertToUtf32(char highSurrogate, char lowSurrogate) { throw null; } public static int ConvertToUtf32(string s, int index) { throw null; } public bool Equals(char obj) { throw null; } + public bool Equals(char other, System.StringComparison comparisonType) { throw null; } public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } public override int GetHashCode() { throw null; } public static double GetNumericValue(char c) { throw null; } @@ -5706,6 +5707,8 @@ public void CopyTo(System.Span destination) { } public int IndexOf(char value, int startIndex) { throw null; } public int IndexOf(char value, int startIndex, int count) { throw null; } public int IndexOf(char value, System.StringComparison comparisonType) { throw null; } + public int IndexOf(char value, int startIndex, System.StringComparison comparisonType) { throw null; } + public int IndexOf(char value, int startIndex, int count, System.StringComparison comparisonType) { throw null; } public int IndexOf(string value) { throw null; } public int IndexOf(string value, int startIndex) { throw null; } public int IndexOf(string value, int startIndex, int count) { throw null; } @@ -5716,6 +5719,8 @@ public void CopyTo(System.Span destination) { } public int IndexOf(System.Text.Rune value, int startIndex) { throw null; } public int IndexOf(System.Text.Rune value, int startIndex, int count) { throw null; } public int IndexOf(System.Text.Rune value, System.StringComparison comparisonType) { throw null; } + public int IndexOf(System.Text.Rune value, int startIndex, System.StringComparison comparisonType) { throw null; } + public int IndexOf(System.Text.Rune value, int startIndex, int count, System.StringComparison comparisonType) { throw null; } public int IndexOfAny(char[] anyOf) { throw null; } public int IndexOfAny(char[] anyOf, int startIndex) { throw null; } public int IndexOfAny(char[] anyOf, int startIndex, int count) { throw null; } @@ -5742,6 +5747,9 @@ public void CopyTo(System.Span destination) { } public int LastIndexOf(char value) { throw null; } public int LastIndexOf(char value, int startIndex) { throw null; } public int LastIndexOf(char value, int startIndex, int count) { throw null; } + public int LastIndexOf(char value, System.StringComparison comparisonType) { throw null; } + public int LastIndexOf(char value, int startIndex, System.StringComparison comparisonType) { throw null; } + public int LastIndexOf(char value, int startIndex, int count, System.StringComparison comparisonType) { throw null; } public int LastIndexOf(string value) { throw null; } public int LastIndexOf(string value, int startIndex) { throw null; } public int LastIndexOf(string value, int startIndex, int count) { throw null; } @@ -5752,6 +5760,8 @@ public void CopyTo(System.Span destination) { } public int LastIndexOf(System.Text.Rune value, int startIndex) { throw null; } public int LastIndexOf(System.Text.Rune value, int startIndex, int count) { throw null; } public int LastIndexOf(System.Text.Rune value, System.StringComparison comparisonType) { throw null; } + public int LastIndexOf(System.Text.Rune value, int startIndex, System.StringComparison comparisonType) { throw null; } + public int LastIndexOf(System.Text.Rune value, int startIndex, int count, System.StringComparison comparisonType) { throw null; } public int LastIndexOfAny(char[] anyOf) { throw null; } public int LastIndexOfAny(char[] anyOf, int startIndex) { throw null; } public int LastIndexOfAny(char[] anyOf, int startIndex, int count) { throw null; } diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/CharTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/CharTests.cs index 65112375551d3f..77854eed48bb30 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/CharTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/CharTests.cs @@ -147,21 +147,71 @@ public static void ConvertToUtf32_Char_Char_Invalid() AssertExtensions.Throws("highSurrogate", () => char.ConvertToUtf32('\u0000', '\u0000')); // Non-surrogate, non-surrogate } + public static IEnumerable EqualsTest_TestData() + { + yield return new object[] { 'a', 'a', StringComparison.Ordinal, null, true }; + yield return new object[] { 'a', 'A', StringComparison.Ordinal, null, false }; + yield return new object[] { 'a', 'b', StringComparison.Ordinal, null, false }; + yield return new object[] { 'a', (int)'a', StringComparison.Ordinal, null, false }; + yield return new object[] { 'a', "a", StringComparison.Ordinal, null, false }; + yield return new object[] { 'a', null, StringComparison.Ordinal, null, false }; + yield return new object[] { 'F', 'f', StringComparison.Ordinal, null, false }; + yield return new object[] { 'F', 'f', StringComparison.OrdinalIgnoreCase, null, true }; + yield return new object[] { 'F', 'f', StringComparison.CurrentCulture, null, false }; + yield return new object[] { 'F', 'f', StringComparison.CurrentCultureIgnoreCase, null, true }; + yield return new object[] { 'F', 'f', StringComparison.InvariantCulture, null, false }; + yield return new object[] { 'F', 'f', StringComparison.InvariantCultureIgnoreCase, null, true }; + yield return new object[] { 'ü', 'Ü', StringComparison.Ordinal, null, false }; + yield return new object[] { 'ü', 'Ü', StringComparison.OrdinalIgnoreCase, null, true }; + yield return new object[] { 'ü', 'Ü', StringComparison.CurrentCulture, null, false }; + yield return new object[] { 'ü', 'Ü', StringComparison.CurrentCultureIgnoreCase, null, true }; + yield return new object[] { 'ü', 'Ü', StringComparison.InvariantCulture, null, false }; + yield return new object[] { 'ü', 'Ü', StringComparison.InvariantCultureIgnoreCase, null, true }; + yield return new object[] { 'ı', 'I', StringComparison.Ordinal, null, false }; + yield return new object[] { 'ı', 'I', StringComparison.OrdinalIgnoreCase, null, false }; + yield return new object[] { 'ı', 'I', StringComparison.CurrentCulture, null, false }; + yield return new object[] { 'ı', 'I', StringComparison.CurrentCultureIgnoreCase, null, false }; + yield return new object[] { 'ı', 'I', StringComparison.InvariantCulture, null, false }; + yield return new object[] { 'ı', 'I', StringComparison.InvariantCultureIgnoreCase, null, false }; + + // Android has different results with "tr-TR" culture + // See https://github.com/dotnet/runtime/issues/106560 + if (PlatformDetection.IsNotAndroid) + { + yield return new object[] { 'ı', 'I', StringComparison.Ordinal, "tr-TR", false }; + yield return new object[] { 'ı', 'I', StringComparison.OrdinalIgnoreCase, "tr-TR", false }; + yield return new object[] { 'ı', 'I', StringComparison.CurrentCulture, "tr-TR", false }; + yield return new object[] { 'ı', 'I', StringComparison.CurrentCultureIgnoreCase, "tr-TR", true }; + yield return new object[] { 'ı', 'I', StringComparison.InvariantCulture, "tr-TR", false }; + yield return new object[] { 'ı', 'I', StringComparison.InvariantCultureIgnoreCase, "tr-TR", false }; + } + } + [Theory] - [InlineData('a', 'a', true)] - [InlineData('a', 'A', false)] - [InlineData('a', 'b', false)] - [InlineData('a', (int)'a', false)] - [InlineData('a', "a", false)] - [InlineData('a', null, false)] - public static void EqualsTest(char c, object? obj, bool expected) - { - if (obj is char) + [MemberData(nameof(EqualsTest_TestData))] + public static void EqualsTest(char c, object? other, StringComparison comparisonType, string? cultureName, bool expected) + { + using (new ThreadCultureChange(cultureName)) { - Assert.Equal(expected, c.Equals((char)obj)); - Assert.Equal(expected, c.GetHashCode().Equals(obj.GetHashCode())); + if (other is char otherChar) + { + if (comparisonType is StringComparison.Ordinal) + { + Assert.Equal(expected, c.Equals(otherChar)); + Assert.Equal(expected, c.ToString().Equals(otherChar.ToString())); + + Assert.Equal(expected, c.GetHashCode().Equals(other.GetHashCode())); + } + + Assert.Equal(expected, c.Equals(otherChar, comparisonType)); + Assert.Equal(expected, c.ToString().Equals(otherChar.ToString(), comparisonType)); + } + + if (comparisonType is StringComparison.Ordinal) + { + Assert.Equal(expected, c.Equals(other)); + } } - Assert.Equal(expected, c.Equals(obj)); } [Theory] diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs index 57673383fc225b..3768f9feeba903 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests/System/StringTests.cs @@ -1346,37 +1346,93 @@ public static void ImplicitCast_NullString_ReturnsDefaultSpan() Assert.True(span == default); } + public static IEnumerable IndexOf_SingleLetter_StringComparison_TestData() + { + yield return new object[] { "Hello", 'l', 0, int.MaxValue, StringComparison.Ordinal, null, 2 }; + yield return new object[] { "Hello", 'x', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "Hello", 'h', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "Hello", 'o', 0, int.MaxValue, StringComparison.Ordinal, null, 4 }; + yield return new object[] { "Hello", 'h', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 0 }; + yield return new object[] { "HelLo", 'L', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 2 }; + yield return new object[] { "HelLo", 'L', 0, int.MaxValue, StringComparison.Ordinal, null, 3 }; + yield return new object[] { "HelLo", '\0', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "!@#$%", '%', 0, int.MaxValue, StringComparison.Ordinal, null, 4 }; + yield return new object[] { "!@#$", '!', 0, int.MaxValue, StringComparison.Ordinal, null, 0 }; + yield return new object[] { "!@#$", '@', 0, int.MaxValue, StringComparison.Ordinal, null, 1 }; + yield return new object[] { "!@#$%", '%', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 4 }; + yield return new object[] { "!@#$", '!', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 0 }; + yield return new object[] { "!@#$", '@', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 1 }; + yield return new object[] { "_____________\u807f", '\u007f', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "_____________\u807f__", '\u007f', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "_____________\u807f\u007f_", '\u007f', 0, int.MaxValue, StringComparison.Ordinal, null, 14 }; + yield return new object[] { "__\u807f_______________", '\u007f', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "__\u807f___\u007f___________", '\u007f', 0, int.MaxValue, StringComparison.Ordinal, null, 6 }; + yield return new object[] { "_____________\u807f", '\u007f', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + yield return new object[] { "_____________\u807f__", '\u007f', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + yield return new object[] { "_____________\u807f\u007f_", '\u007f', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 14 }; + yield return new object[] { "__\u807f_______________", '\u007f', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + yield return new object[] { "__\u807f___\u007f___________", '\u007f', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 6 }; + yield return new object[] { "hello", 'e', 3, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "hello", 'o', 3, int.MaxValue, StringComparison.Ordinal, null, 4 }; + yield return new object[] { "hello", 'o', 3, 0, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "hello", 'o', 3, 2, StringComparison.Ordinal, null, 4 }; + yield return new object[] { "HELLO", 'o', 3, 2, StringComparison.OrdinalIgnoreCase, null, 4 }; + yield return new object[] { "abacus", 'a', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 0 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 0 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.CurrentCulture, null, -1 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, null, 0 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.InvariantCulture, null, -1 }; + yield return new object[] { "ü", 'Ü', 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, null, 0 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.CurrentCulture, null, -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, null, -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.InvariantCulture, null, -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, null, -1 }; + + // Android has different results with "tr-TR" culture + // See https://github.com/dotnet/runtime/issues/106560 + if (PlatformDetection.IsNotAndroid) + { + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.Ordinal, "tr-TR", -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, "tr-TR", -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.CurrentCulture, "tr-TR", -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, "tr-TR", 0 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.InvariantCulture, "tr-TR", -1 }; + yield return new object[] { "ı", 'I', 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, "tr-TR", -1 }; + } + + yield return new object[] { "", 'm', 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "", 'm', 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + } + [Theory] - [InlineData("Hello", 'l', StringComparison.Ordinal, 2)] - [InlineData("Hello", 'x', StringComparison.Ordinal, -1)] - [InlineData("Hello", 'h', StringComparison.Ordinal, -1)] - [InlineData("Hello", 'o', StringComparison.Ordinal, 4)] - [InlineData("Hello", 'h', StringComparison.OrdinalIgnoreCase, 0)] - [InlineData("HelLo", 'L', StringComparison.OrdinalIgnoreCase, 2)] - [InlineData("HelLo", 'L', StringComparison.Ordinal, 3)] - [InlineData("HelLo", '\0', StringComparison.Ordinal, -1)] - [InlineData("!@#$%", '%', StringComparison.Ordinal, 4)] - [InlineData("!@#$", '!', StringComparison.Ordinal, 0)] - [InlineData("!@#$", '@', StringComparison.Ordinal, 1)] - [InlineData("!@#$%", '%', StringComparison.OrdinalIgnoreCase, 4)] - [InlineData("!@#$", '!', StringComparison.OrdinalIgnoreCase, 0)] - [InlineData("!@#$", '@', StringComparison.OrdinalIgnoreCase, 1)] - [InlineData("_____________\u807f", '\u007f', StringComparison.Ordinal, -1)] - [InlineData("_____________\u807f__", '\u007f', StringComparison.Ordinal, -1)] - [InlineData("_____________\u807f\u007f_", '\u007f', StringComparison.Ordinal, 14)] - [InlineData("__\u807f_______________", '\u007f', StringComparison.Ordinal, -1)] - [InlineData("__\u807f___\u007f___________", '\u007f', StringComparison.Ordinal, 6)] - [InlineData("_____________\u807f", '\u007f', StringComparison.OrdinalIgnoreCase, -1)] - [InlineData("_____________\u807f__", '\u007f', StringComparison.OrdinalIgnoreCase, -1)] - [InlineData("_____________\u807f\u007f_", '\u007f', StringComparison.OrdinalIgnoreCase, 14)] - [InlineData("__\u807f_______________", '\u007f', StringComparison.OrdinalIgnoreCase, -1)] - [InlineData("__\u807f___\u007f___________", '\u007f', StringComparison.OrdinalIgnoreCase, 6)] - public static void IndexOf_SingleLetter_StringComparison(string s, char target, StringComparison stringComparison, int expected) - { - Assert.Equal(expected, s.IndexOf(target, stringComparison)); - var charArray = new char[1]; - charArray[0] = target; - Assert.Equal(expected, s.AsSpan().IndexOf(charArray, stringComparison)); + [MemberData(nameof(IndexOf_SingleLetter_StringComparison_TestData))] + public static void IndexOf_SingleLetter_StringComparison(string s, char target, int startIndex, int count, StringComparison stringComparison, string? cultureName, int expected) + { + using (new ThreadCultureChange(cultureName)) + { + if (count == int.MaxValue) + { + count = s.Length - startIndex; + } + + if (startIndex == 0 && count == s.Length) + { + Assert.Equal(expected, s.IndexOf(target, stringComparison)); + } + if (s.Length - startIndex == count) + { + Assert.Equal(expected, s.IndexOf(target, startIndex, stringComparison)); + } + + Assert.Equal(expected, s.IndexOf(target, startIndex, count, stringComparison)); + + ReadOnlySpan targetSpan = [target]; + int subIndex = s.AsSpan(startIndex, count).IndexOf(targetSpan, stringComparison); + Assert.Equal(expected, subIndex < 0 ? subIndex : startIndex + subIndex); + } } [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotInvariantGlobalization))] @@ -1518,18 +1574,71 @@ public static void IndexOf_Invalid_Char() public static IEnumerable IndexOf_Rune_StringComparison_TestData() { - yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC4f'), StringComparison.Ordinal, -1}; - yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 5}; + yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC4f'), 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 5 }; + + yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 5 }; + yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + + yield return new object[] { "hello", new Rune('o'), 3, 2, StringComparison.Ordinal, null, 4 }; + yield return new object[] { "HELLO", new Rune('o'), 3, 2, StringComparison.OrdinalIgnoreCase, null, 4 }; + yield return new object[] { "hello\uD801\uDC28", new Rune('\uD801', '\uDC28'), 3, 4, StringComparison.Ordinal, null, 5 }; + yield return new object[] { "HELLO\uD801\uDC00", new Rune('\uD801', '\uDC28'), 3, 4, StringComparison.OrdinalIgnoreCase, null, 5 }; + + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, 0 }; + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.CurrentCulture, null, -1 }; + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, null, 0 }; + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.InvariantCulture, null, -1 }; + yield return new object[] { "ü", new Rune('Ü'), 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, null, 0 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.CurrentCulture, null, -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, null, -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.InvariantCulture, null, -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, null, -1 }; + + // Android has different results with "tr-TR" culture + // See https://github.com/dotnet/runtime/issues/106560 + if (PlatformDetection.IsNotAndroid) + { + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.Ordinal, "tr-TR", -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, "tr-TR", -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.CurrentCulture, "tr-TR", -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, "tr-TR", 0 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.InvariantCulture, "tr-TR", -1 }; + yield return new object[] { "ı", new Rune('I'), 0, int.MaxValue, StringComparison.InvariantCultureIgnoreCase, "tr-TR", -1 }; + } - yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 5}; - yield return new object[] { "Hello\u0200\u0202", new Rune('\u0201'), StringComparison.Ordinal, -1}; + yield return new object[] { "", new Rune('m'), 0, int.MaxValue, StringComparison.Ordinal, null, -1 }; + yield return new object[] { "", new Rune('m'), 0, int.MaxValue, StringComparison.OrdinalIgnoreCase, null, -1 }; } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))] [MemberData(nameof(IndexOf_Rune_StringComparison_TestData))] - public static void IndexOf_Rune_StringComparison(string source, Rune target, StringComparison stringComparison, int expected) + public static void IndexOf_Rune_StringComparison(string source, Rune target, int startIndex, int count, StringComparison stringComparison, string? cultureName, int expected) { - Assert.Equal(expected, source.IndexOf(target, stringComparison)); + using (new ThreadCultureChange(cultureName)) + { + if (count == int.MaxValue) + { + count = source.Length - startIndex; + } + + if (startIndex == 0 && count == source.Length) + { + Assert.Equal(expected, source.IndexOf(target, stringComparison)); + Assert.Equal(expected, source.IndexOf(target.ToString(), stringComparison)); + } + if (source.Length - startIndex == count) + { + Assert.Equal(expected, source.IndexOf(target, startIndex, stringComparison)); + Assert.Equal(expected, source.IndexOf(target.ToString(), startIndex, stringComparison)); + } + + Assert.Equal(expected, source.IndexOf(target, startIndex, count, stringComparison)); + Assert.Equal(expected, source.IndexOf(target.ToString(), startIndex, count, stringComparison)); + } } public static IEnumerable IndexOf_String_StringComparison_TestData() @@ -1553,22 +1662,100 @@ public static void IndexOf_Ordinal_Misc(string source, string target, StringComp Assert.Equal(expected, source.IndexOf(target, stringComparison)); } + [Theory] + [InlineData("Hello", 'l', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 3)] + [InlineData("Hello", 'x', int.MaxValue, int.MaxValue, StringComparison.Ordinal, -1)] + [InlineData("Hello", 'h', int.MaxValue, int.MaxValue, StringComparison.Ordinal, -1)] + [InlineData("Hello", 'o', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 4)] + [InlineData("Hello", 'h', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 0)] + [InlineData("HeLlo", 'L', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 3)] + [InlineData("HeLlo", 'L', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 2)] + [InlineData("HeLlo", '\0', int.MaxValue, int.MaxValue, StringComparison.Ordinal, -1)] + [InlineData("!@#$%", '%', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 4)] + [InlineData("!@#$", '!', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 0)] + [InlineData("!@#$", '@', int.MaxValue, int.MaxValue, StringComparison.Ordinal, 1)] + [InlineData("!@#$%", '%', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 4)] + [InlineData("!@#$", '!', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 0)] + [InlineData("!@#$", '@', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 1)] + [InlineData("hello", 'o', 3, int.MaxValue, StringComparison.Ordinal, -1)] + [InlineData("hello", 'e', 3, int.MaxValue, StringComparison.Ordinal, 1)] + [InlineData("hello", 'e', 3, 0, StringComparison.Ordinal, -1)] + [InlineData("hello", 'e', 3, 3, StringComparison.Ordinal, 1)] + [InlineData("HELLO", 'e', 3, 3, StringComparison.OrdinalIgnoreCase, 1)] + [InlineData("abacus", 'a', int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 2)] + public static void LastIndexOf_SingleLetter_StringComparison(string s, char target, int startIndex, int count, StringComparison stringComparison, int expected) + { + if (startIndex == int.MaxValue) + { + startIndex = s.Length - 1; + } + if (count == int.MaxValue) + { + count = startIndex + 1; + } + + if (startIndex == 0 && count == s.Length) + { + Assert.Equal(expected, s.LastIndexOf(target, stringComparison)); + Assert.Equal(expected, s.LastIndexOf(target.ToString(), stringComparison)); + } + if (startIndex + 1 == count) + { + Assert.Equal(expected, s.LastIndexOf(target, startIndex, stringComparison)); + Assert.Equal(expected, s.LastIndexOf(target.ToString(), startIndex, stringComparison)); + } + + Assert.Equal(expected, s.LastIndexOf(target, startIndex, count, stringComparison)); + Assert.Equal(expected, s.LastIndexOf(target.ToString(), startIndex, count, stringComparison)); + + ReadOnlySpan targetSpan = [target]; + int startIndexFromZero = startIndex - count + 1; + int subIndex = s.AsSpan(startIndexFromZero, count).LastIndexOf(targetSpan, stringComparison); + Assert.Equal(expected, subIndex < 0 ? subIndex : startIndexFromZero + subIndex); + } + public static IEnumerable LastIndexOf_Rune_StringComparison_TestData() { - yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC4f'), StringComparison.Ordinal, -1}; - yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 0}; - yield return new object[] { "\uD801\uDC28Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), StringComparison.OrdinalIgnoreCase, 7}; + yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC4f'), int.MaxValue, int.MaxValue, StringComparison.Ordinal, -1}; + yield return new object[] { "\uD801\uDC28Hello", new Rune('\uD801', '\uDC00'), int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 0}; + yield return new object[] { "\uD801\uDC28Hello\uD801\uDC28", new Rune('\uD801', '\uDC00'), int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 7}; - yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 0}; - yield return new object[] { "\u0200\u0202Hello\u0200\u0202", new Rune('\u0201'), StringComparison.OrdinalIgnoreCase, 7}; // \u0200 is uppercase of \u0201 - yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), StringComparison.Ordinal, -1}; + yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 0}; + yield return new object[] { "\u0200\u0202Hello\u0200\u0202", new Rune('\u0201'), int.MaxValue, int.MaxValue, StringComparison.OrdinalIgnoreCase, 7}; // \u0200 is uppercase of \u0201 + yield return new object[] { "\u0200\u0202Hello", new Rune('\u0201'), int.MaxValue, int.MaxValue, StringComparison.Ordinal, -1}; + + yield return new object[] { "hello", new Rune('e'), 3, 3, StringComparison.Ordinal, 1 }; + yield return new object[] { "HELLO", new Rune('e'), 3, 3, StringComparison.OrdinalIgnoreCase, 1 }; + yield return new object[] { "hello\uD801\uDC28", new Rune('\uD801', '\uDC28'), 6, 4, StringComparison.Ordinal, 5 }; + yield return new object[] { "HELLO\uD801\uDC00", new Rune('\uD801', '\uDC28'), 6, 4, StringComparison.OrdinalIgnoreCase, 5 }; } [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsIcuGlobalization))] [MemberData(nameof(LastIndexOf_Rune_StringComparison_TestData))] - public static void LastIndexOf_Rune_StringComparison(string source, Rune target, StringComparison stringComparison, int expected) + public static void LastIndexOf_Rune_StringComparison(string source, Rune target, int startIndex, int count, StringComparison stringComparison, int expected) { - Assert.Equal(expected, source.LastIndexOf(target, stringComparison)); + if (startIndex == int.MaxValue) + { + startIndex = source.Length - 1; + } + if (count == int.MaxValue) + { + count = startIndex + 1; + } + + if (startIndex == 0 && count == source.Length) + { + Assert.Equal(expected, source.LastIndexOf(target, stringComparison)); + Assert.Equal(expected, source.LastIndexOf(target.ToString(), stringComparison)); + } + if (startIndex + 1 == count) + { + Assert.Equal(expected, source.LastIndexOf(target, startIndex, stringComparison)); + Assert.Equal(expected, source.LastIndexOf(target.ToString(), startIndex, stringComparison)); + } + + Assert.Equal(expected, source.LastIndexOf(target, startIndex, count, stringComparison)); + Assert.Equal(expected, source.LastIndexOf(target.ToString(), startIndex, count, stringComparison)); } public static IEnumerable LastIndexOf_String_StringComparison_TestData()