Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
0f5ba8a
Add missing overloads
Joy-less Sep 30, 2025
6e63506
Add refs to added missing overloads
Joy-less Sep 30, 2025
77fa228
Fix IndexOf/LastIndexOf char OrdinalIgnoreCase
Joy-less Oct 1, 2025
444d4b8
Add tests & fixes
Joy-less Oct 1, 2025
20b8733
Fix Copilot suggestions
Joy-less Oct 2, 2025
8166e82
Rename `right` to `other`
Joy-less Oct 2, 2025
45fd035
Make `LastIndexOf` public
Joy-less Oct 2, 2025
6853c73
Add commented-out test
Joy-less Oct 2, 2025
0812e82
Rename `right` to `other` in refs
Joy-less Oct 2, 2025
f2e0027
Add doc comments to IndexOf char
Joy-less Oct 2, 2025
b7956ac
Update docs from `-1` to `a negative value (e.g. -1)`
Joy-less Oct 2, 2025
832a748
Refactor duplicate return statements
Joy-less Oct 2, 2025
347d59c
Add foreign cases to char Equals StringComparison test
Joy-less Oct 2, 2025
97dba93
Add foreign/empty cases to IndexOf char/Rune
Joy-less Oct 2, 2025
4d126f4
Fix length bounds
Joy-less Oct 2, 2025
2d255b5
Add convert to string test case
Joy-less Oct 2, 2025
48cecde
Fix \0 edge case?
Joy-less Oct 2, 2025
d31505c
Add more convert to string test cases
Joy-less Oct 2, 2025
d2dbd8f
Remove most likely incorrect fix
Joy-less Oct 3, 2025
54c2f80
Add `Length == 0` check to last index of methods
Joy-less Oct 4, 2025
8abd5d3
Merge branch 'main' into add-missing-overloads-to-flow-rune-proposal
tarekgh Oct 5, 2025
7447cee
Simplify `AsSpan` calls
Joy-less Oct 5, 2025
8afe6a6
Fix invalid test arguments
Joy-less Oct 5, 2025
4aa3a29
Add `IsNotAndroid` checks for Turkish "i" tests
Joy-less Oct 6, 2025
b97b5c1
Also add `IsNotAndroid` checks for `char` `EqualsTest`
Joy-less Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/libraries/System.Private.CoreLib/src/System/Char.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ public bool Equals(char obj)
return m_value == obj;
}

internal bool Equals(char right, StringComparison comparisonType)
/// <summary>
/// Returns a value that indicates whether the current instance and a specified character are equal using the specified comparison option.
/// </summary>
/// <param name="right">The character to compare with the current instance.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules to use in the comparison.</param>
/// <returns><see langword="true"/> if the current instance and <paramref name="right"/> are equal; otherwise, <see langword="false"/>.</returns>
public bool Equals(char right, StringComparison comparisonType)
{
switch (comparisonType)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,10 @@ internal static bool NonPackedContainsValueType<T>(ref T searchSpace, T value, i
internal static int IndexOfChar(ref char searchSpace, char value, int length)
=> IndexOfValueType(ref Unsafe.As<char, short>(ref searchSpace), (short)value, length);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int LastIndexOfChar(ref char searchSpace, char value, int length)
=> LastIndexOfValueType(ref Unsafe.As<char, short>(ref searchSpace), (short)value, length);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int NonPackedIndexOfChar(ref char searchSpace, char value, int length) =>
NonPackedIndexOfValueType<short, DontNegate<short>>(ref Unsafe.As<char, short>(ref searchSpace), (short)value, length);
Expand Down Expand Up @@ -1655,6 +1659,10 @@ internal static int NonPackedIndexOfValueType<TValue, TNegator>(ref TValue searc
internal static int IndexOfAnyChar(ref char searchSpace, char value0, char value1, int length)
=> IndexOfAnyValueType(ref Unsafe.As<char, short>(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<char, short>(ref searchSpace), (short)value0, (short)value1, length);

[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static int IndexOfAnyValueType<T>(ref T searchSpace, T value0, T value1, int length) where T : struct, INumber<T>
=> IndexOfAnyValueType<T, DontNegate<T>>(ref searchSpace, value0, value1, length);
Expand Down
161 changes: 142 additions & 19 deletions src/libraries/System.Private.CoreLib/src/System/String.Searching.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,34 +76,59 @@ public int IndexOf(char value, int startIndex)
}

public int IndexOf(char value, StringComparison comparisonType)
{
return IndexOf(value, 0, comparisonType);
}

public int IndexOf(char value, int startIndex, StringComparison comparisonType)
{
return IndexOf(value, startIndex, Length - startIndex, comparisonType);
}

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<char>(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<char>(in value));
}

return SpanHelpers.IndexOfChar(ref _firstChar, value, Length);
return subIndex < 0 ? subIndex : startIndex + subIndex;
}

public int IndexOf(char value, int startIndex, int count)
Expand Down Expand Up @@ -357,7 +382,7 @@ public int IndexOf(Rune value, StringComparison comparisonType)
/// The zero-based index position of <paramref name="value"/> from the start of the current instance
/// if that rune is found, or -1 if it is not.
/// </returns>
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);
}
Expand All @@ -375,7 +400,7 @@ internal int IndexOf(Rune value, int startIndex, StringComparison comparisonType
/// The zero-based index position of <paramref name="value"/> from the start of the current instance
/// if that rune is found, or -1 if it is not.
/// </returns>
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);
Expand Down Expand Up @@ -423,6 +448,104 @@ public int LastIndexOf(char value, int startIndex, int count)
return result < 0 ? result : result + startSearchAt;
}

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The character to seek.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the search.</param>
/// <returns>
/// The zero-based index position of <paramref name="value"/> from the end of the current instance
/// if that character is found, or -1 if it is not.
/// </returns>
internal int LastIndexOf(char value, StringComparison comparisonType)
{
return LastIndexOf(value, Length - 1, comparisonType);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The character to seek.</param>
/// <param name="startIndex">The search starting position. The search proceeds from <paramref name="startIndex"/> toward the beginning of this instance.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the search.</param>
/// <returns>
/// The zero-based index position of <paramref name="value"/> from the end of the current instance
/// if that character is found, or -1 if it is not.
/// </returns>
public int LastIndexOf(char value, int startIndex, StringComparison comparisonType)
{
return LastIndexOf(value, startIndex, startIndex + 1, comparisonType);
}

/// <summary>
/// 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.
/// </summary>
/// <param name="value">The character to seek.</param>
/// <param name="startIndex">The search starting position. The search proceeds from <paramref name="startIndex"/> toward the beginning of this instance.</param>
/// <param name="count">The number of character positions to examine.</param>
/// <param name="comparisonType">One of the enumeration values that specifies the rules for the search.</param>
/// <returns>
/// The zero-based index position of <paramref name="value"/> from the end of the current instance
/// if that character is found, or -1 if it is not.
/// </returns>
public int LastIndexOf(char value, int startIndex, int count, StringComparison comparisonType)
{
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);
}

return subIndex < 0 ? subIndex : startSearchAt + subIndex;
}
else
{
subIndex = Ordinal.LastIndexOfOrdinalIgnoreCase(this.AsSpan(startSearchAt, count), new ReadOnlySpan<char>(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
Expand Down Expand Up @@ -585,7 +708,7 @@ public int LastIndexOf(Rune value, StringComparison comparisonType)
/// The zero-based index position of <paramref name="value"/> from the end of the current instance
/// if that rune is found, or -1 if it is not.
/// </returns>
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);
}
Expand All @@ -603,7 +726,7 @@ internal int LastIndexOf(Rune value, int startIndex, StringComparison comparison
/// The zero-based index position of <paramref name="value"/> from the end of the current instance
/// if that rune is found, or -1 if it is not.
/// </returns>
internal int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType)
public int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType)
{
ArgumentOutOfRangeException.ThrowIfLessThan(startIndex, 0);
ArgumentOutOfRangeException.ThrowIfLessThan(count, 0);
Expand Down
9 changes: 9 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 right, 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; }
Expand Down Expand Up @@ -5706,6 +5707,8 @@ public void CopyTo(System.Span<char> 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; }
Expand All @@ -5716,6 +5719,8 @@ public void CopyTo(System.Span<char> 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; }
Expand All @@ -5742,6 +5747,8 @@ public void CopyTo(System.Span<char> 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, 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; }
Expand All @@ -5752,6 +5759,8 @@ public void CopyTo(System.Span<char> 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; }
Expand Down
Loading
Loading