Skip to content

Conversation

Joy-less
Copy link
Contributor

@Joy-less Joy-less commented Oct 2, 2025

Closes #120015 proposal.

Adds the following APIs with refs and tests:

namespace System
{
    public partial sealed class String
    {
        public int IndexOf(Rune value, int startIndex, StringComparison comparisonType);
        public int IndexOf(Rune value, int startIndex, int count, StringComparison comparisonType);
        public int IndexOf(char value, int startIndex, StringComparison comparisonType);
        public int IndexOf(char value, int startIndex, int count, StringComparison comparisonType);

        public int LastIndexOf(Rune value, int startIndex, StringComparison comparisonType);
        public int LastIndexOf(Rune value, int startIndex, int count, StringComparison comparisonType);
        public int LastIndexOf(char value, int startIndex, StringComparison comparisonType);
        public int LastIndexOf(char value, int startIndex, int count, StringComparison comparisonType);
        public int LastIndexOf(char value, StringComparison comparisonType);
    }

    public readonly struct Char
    {
        public bool Equals(char right, StringComparison comparisonType);
    }
}

NOTE:
This pull request ignores the two amendments listed in #120015 (comment). It should not be merged until those amendments are approved/rejected.

@tarekgh

@Copilot Copilot AI review requested due to automatic review settings October 2, 2025 01:13
@github-actions github-actions bot added the needs-area-label An area label is needed to ensure this gets routed to the appropriate area owners label Oct 2, 2025
@dotnet-policy-service dotnet-policy-service bot added the community-contribution Indicates that the PR has been added by a community member label Oct 2, 2025
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

Adds new String and Char overloads to support searching for char and Rune with StringComparison and startIndex/count parameters, plus a Char.Equals overload with StringComparison. Updates reference assembly, core implementations, span helpers, and expands tests accordingly.

  • Adds IndexOf/LastIndexOf overloads for char and Rune with (startIndex, comparisonType) and (startIndex, count, comparisonType).
  • Exposes Char.Equals(char, StringComparison) publicly and adds corresponding tests.
  • Implements ordinal ignore-case search helpers and supporting SpanHelpers last-index routines.

Reviewed Changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
System.Runtime.Tests/System/StringTests.cs Updated and expanded tests for new IndexOf/LastIndexOf overloads (char and Rune) with startIndex/count + comparison.
System.Runtime.Tests/System/CharTests.cs Added tests for new Char.Equals with StringComparison and adjusted existing equality tests.
System.Runtime/ref/System.Runtime.cs Added new public API surface for String and Char overloads.
System.Private.CoreLib/src/System/String.Searching.cs Implemented new overloads, publicized Rune variants, and added ordinal ignore-case logic for ranged search.
System.Private.CoreLib/src/System/SpanHelpers.T.cs Added LastIndexOfChar / LastIndexOfAnyChar helpers for performance support.
System.Private.CoreLib/src/System/Char.cs Made Char.Equals(char, StringComparison) public with XML docs.

@tarekgh
Copy link
Member

tarekgh commented Oct 3, 2025

@Joy-less let us not stop continuing updating this PR so we can be ready when we decide to merge. Thanks!

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 3, 2025

@Joy-less let us not stop continuing updating this PR so we can be ready when we decide to merge. Thanks!

Understood, please could you help with this issue if it is clear? #120314 (comment)

@tarekgh
Copy link
Member

tarekgh commented Oct 3, 2025

Understood, please could you help with this issue if it is clear? #120314 (comment)

Let’s proceed with the following behavior:

  • LastIndexOf(char, int, StringComparison) should throw when startIndex equals or greater than the string’s Length.
  • For an empty string "".LastIndexOf(...) should always return -1, regardless of the search value.

I chose this behavior for the following reasons:

  1. We’re not fully consistent today, so let’s do it correctly for the new APIs.
  2. A startIndex that is negative or greater than or equal to Length is an invalid index within the string — this is logically correct.
  3. We should never return an index from IndexOf or LastIndexOf that falls outside the valid range of the string. For an empty string, the correct result is always -1. Returning 0 could cause the caller to access the string at that index and encounter an exception.

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 4, 2025

@tarekgh I have added the if (Length == 0) { return -1; } checks. Please let me know what I need to fix in the pull request now, thanks!

@tarekgh
Copy link
Member

tarekgh commented Oct 5, 2025

       int subIndex = this.AsSpan(startIndex..(startIndex + count)).IndexOf(valueChars, comparisonType);

nit: this is correct, but I am wondering would be better to use Slice? no need to use Range and then eventually Slice internally will get called anyway.


Refers to: src/libraries/System.Private.CoreLib/src/System/String.Searching.cs:438 in 8abd5d3. [](commit_id = 8abd5d3, deletion_comment = False)

@tarekgh
Copy link
Member

tarekgh commented Oct 5, 2025

@Joy-less I left minor comments. It looks good to me otherwise. Let's wait to resolve the F# issue and then we can proceed.

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 5, 2025

       int subIndex = this.AsSpan(startIndex..(startIndex + count)).IndexOf(valueChars, comparisonType);

nit: this is correct, but I am wondering would be better to use Slice? no need to use Range and then eventually Slice internally will get called anyway.


Refers to: src/libraries/System.Private.CoreLib/src/System/String.Searching.cs:438 in 8abd5d3. [](commit_id = 8abd5d3, deletion_comment = False)

I assume you mean .AsSpan().Slice(startIndex, count);? Sounds good but we should be able to just do .AsSpan(startIndex, count);.

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 6, 2025

I am kind of confused about the errors we're getting here:

10-05 16:16:33.228 30751 31171 I DOTNET  : 1) 	[FAIL] System.Tests.StringTests.IndexOf_SingleLetter_StringComparison   Test name: System.Tests.StringTests.IndexOf_SingleLetter_StringComparison(s: "ı", target: 'I', startIndex: 0, count: 2147483647, stringComparison: CurrentCultureIgnoreCase, cultureName: "tr-TR", expected: 0)   Test case: System.Tests.StringTests.IndexOf_SingleLetter_StringComparison
10-05 16:16:33.228 30751 31171 I DOTNET  :    Assembly:  [System.Runtime.Tests, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51]
10-05 16:16:33.228 30751 31171 I DOTNET  :    Exception messages: Assert.Equal() Failure: Values differ
10-05 16:16:33.228 30751 31171 I DOTNET  : Expected: 0
10-05 16:16:33.228 30751 31171 I DOTNET  : Actual:   -1   Exception stack traces:    at System.Tests.StringTests.IndexOf_SingleLetter_StringComparison(String s, Char target, Int32 startIndex, Int32 count, StringComparison stringComparison, String cultureName, Int32 expected)
10-05 16:16:33.228 30751 31171 I DOTNET  :    at InvokeStub_StringTests.IndexOf_SingleLetter_StringComparison(Object, Span`1)
10-05 16:16:33.228 30751 31171 I DOTNET  :    at System.Reflection.MethodBaseInvoker.InvokeWithManyArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
10-05 16:16:33.228 30751 31171 I DOTNET  :    Execution time: 0.03915

Relevant test:

[InlineData("ı", 'I', 0, int.MaxValue, StringComparison.CurrentCultureIgnoreCase, "tr-TR", 0)]
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<char> targetSpan = [target];
        int subIndex = s.AsSpan(startIndex, count).IndexOf(targetSpan, stringComparison);
        Assert.Equal(expected, subIndex < 0 ? subIndex : startIndex + subIndex);
    }
}

On this line, the following method is called:

Assert.Equal(expected, s.IndexOf(target, startIndex, count, stringComparison));
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, 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)),
    };
}

The relevant method called is CultureInfo.CurrentCulture.CompareInfo.IndexOf(this, value, startIndex, count, GetCaseCompareOfComparisonCulture(comparisonType)).

// in GetCaseCompareOfComparisonCulture method

// StringComparison.CurrentCultureIgnoreCase:   0x01

So if you run the method that the test calls in e.g. dotnetfiddle.net you get:

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine(CultureInfo.CurrentCulture.CompareInfo.IndexOf("ı", 'I', 0, 1, (CompareOptions)0x01));
0

Not sure how it is returning -1 on Android ARM/ARM64.

@tarekgh
Copy link
Member

tarekgh commented Oct 6, 2025

@Joy-less please have a look at #106560. Please exclude this test case for Android for now.

CC @matouskozak

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 6, 2025

@Joy-less please have a look at #106560. Please exclude this test case for Android for now.

CC @matouskozak

Okay, how do I exclude the test for Android?

@tarekgh
Copy link
Member

tarekgh commented Oct 6, 2025

Put the Turkish i validation lines under the condition:

if (PlatformDetection.IsNotAndroid)

@Joy-less
Copy link
Contributor Author

Joy-less commented Oct 7, 2025

@tarekgh All tests pass now, so this PR should be ready whenever the F# issue is resolved.

@Joy-less
Copy link
Contributor Author

@tarekgh Well, the pull request with the F# discussion now got merged with fixes for F#, does that mean this pull request is good to go?

@tarekgh
Copy link
Member

tarekgh commented Oct 13, 2025

the dotnet/dotnet#2627 (comment) now got merged with fixes for F#, does that mean this pull request is good to go?

That PR merged just to unblock the code flow to that repo. We still need to discuss the issue and decide which way we should go with.

@tarekgh tarekgh removed the NO-MERGE The PR is not ready for merge yet (see discussion for detailed reasons) label Oct 15, 2025
@tarekgh
Copy link
Member

tarekgh commented Oct 15, 2025

I’m merging this now. If we need to make any changes related to F#, we can handle them together with the previously added APIs in the same classes.

@tarekgh tarekgh merged commit bb41b0c into dotnet:main Oct 15, 2025
142 of 143 checks passed
@Joy-less Joy-less deleted the add-missing-overloads-to-flow-rune-proposal branch October 15, 2025 16:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Runtime community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[API Proposal]: Add missing overloads to "Flow System.Text.Rune through more APIs" proposal

2 participants