dotnet / dotnet/dotnet-api-docs

Doc suggestion: explanatory text for new CompareInfo APIs

Open
#4,676 1 comment 1 reaction 0 assignees View on GitHub
area-System.Runtime Pri3 untriaged
Dominant language
C#
Stars
949
Forks
1.7k
Avg merge
3d 27m
Merged PRs (30d)
49

Description

With https://github.com/dotnet/runtime/issues/27935, we're introducing new APIs on `CompareInfo` which return _matchLength_ as an _out int_ parameter. However, usage of this is a bit nuanced, and it might not be entirely intuitive as to how callers should use the return values.

We have a code comment [here](https://github.com/dotnet/runtime/blob/aa5fdab9654d74bc6274c0b5d820272c8e859621/src/libraries/System.Private.CoreLib/src/System/String.Searching.cs#L223-L300) which explains the logic behind these APIs. But it would be good to formalize this into a doc replete with examples.

Here's the tl;dr:

Given a `CompareInfo` _compareInfo_, `ReadOnlySpan` _source_ and _target_, and `CompareOptions` _options_:

### StartsWith

If there exists an integer value _matchLength_ which causes the following expression to evaluate to _true_:

```cs
bool areEqual = compareInfo.Compare(source.Slice(0, matchLength), target, options) == 0;
```

Then `compareInfo.StartsWith(source, target, options, out int matchLength)` will return _true_ and output _matchLength_. If there are multiple possible _matchLength_ values which cause the above expression to evaluate to _true_, then `compareInfo.StartsWith(...)` can output any of them.

### EndsWith

If there exists an integer value _matchLength_ which causes the following expression to evaluate to _true_:

```cs
bool areEqual = compareInfo.Compare(source.Slice(source.Length - matchLength), target, options) == 0;
```

Then `compareInfo.EndsWith(source, target, options, out int matchLength)` will return _true_ and output _matchLength_. If there are multiple possible _matchLength_ values which cause the above expression to evaluate to _true_, then `compareInfo.EndsWith(...)` can output any of them.

### IndexOf

For all integers _index_ for which the expression `compareInfo.StartsWith(source.Slice(index), target, options)` evaluates to _true_, define the function `endpos(index)` to return the largest integer _endpos_ for which the following expression evaluates to _true_:

```cs
bool areEqual = compareInfo.Compare(source.Slice(index, endpos - index), target, options) == 0;
```

If no integers _index_ fulfill the above requirement, then `compareInfo.IndexOf(source, target, options, out int matchLength)` will return `-1` and output `0` for _matchLength_.

Otherwise, `compareInfo.IndexOf(source, target, options, out int matchLength)` will return a non-negative _index_ and output _matchLength_, subject to the following constraints:

* There is no integer _index'_ less than _index_ for which `endpos(index') < endpos(index)`; and
* The constraint `compareInfo.Compare(source.Slice(index, matchLength), target, options) == 0` holds. Note that `endpos(index)` may be (but is not required to be) equal to `index + matchLength`.

### LastIndexOf

For all integers _index_ for which the expression `compareInfo.StartsWith(source.Slice(index), target, options)` evaluates to _true_, define the function `endpos(index)` to return the largest integer _endpos_ for which the following expression evaluates to _true_:

```cs
bool areEqual = compareInfo.Compare(source.Slice(index, endpos - index), target, options) == 0;
```

If no integers _index_ fulfill the above requirement, then `compareInfo.LastIndexOf(source, target, options, out int matchLength)` will return `-1` and output `0` for _matchLength_.

Otherwise, `compareInfo.LastIndexOf(source, target, options, out int matchLength)` will return a non-negative _index_ and output _matchLength_, subject to the following constraints:

* There is no integer _index'_ greater than _index_ for which `endpos(index') > endpos(index)`; and
* The constraint `compareInfo.Compare(source.Slice(index, matchLength), target, options) == 0` holds. Note that `endpos(index)` may be (but is not required to be) equal to `index + matchLength`.

## But why?

In a _linguistic_ search, certain characters are ignorable. When passing `CompareOptions.IgnoreSymbols`, symbols like the apostrophe can be skipped for the purpose of searching.

An example:

```cs
CompareInfo compareInfo = CultureInfo.InvariantCulture.CompareInfo;

int matchLength;
int index = compareInfo.IndexOf("abcd'efg", "cde", CompareOptions.IgnoreSymbols, out int matchLength);
Assert.Equal(2, index);
Assert.Equal(4, matchLength);
```

That is, even though the value string was only 3 chars (`"cde"`), it matched 4 chars (`"cd'e"`) in the source string, since we consumed the apostrophe character as part of the match.

```cs
ReadOnlySpan slice = "abcd'efg".AsSpan(index, matchLength); // = "cd'e"
int compareResult = compareInfo.Compare(slice, "cde", CompareOptions.IgnoreSymbols);
Assert.Equal(0, compareResult); // they match!
```

It would be incorrect to assume that since the value string has only 3 chars, then only 3 chars of the source string must match.

```cs
ReadOnlySpan slice = "abcd'efg".AsSpan(index, "cde".Length); // = "cd'"
compareResult = compareInfo.Compare(slice, "cde", CompareOptions.IgnoreSymbols);
Assert.Equal(0, compareResult); // !! ERROR !!
```

When ignorable characters are on a boundary, they _may_ be (but are not required to be) consumed as part of the match, as shown below.

```cs
index = compareInfo.IndexOf("a'b'c", "b", CompareOptions.IgnoreSymbols, out matchLength);
Console.WriteLine($"index: {index}, matchLength: {matchLength}");
```

The above example might match any of the substrings `"b"`, `"'b"`, `"b'"`, or `"'b'"` within the source. This means that the above sample could write _any_ of the below four lines to the console depending on runtime and OS version, and any of these four would be correct.

```txt
index: 1, matchLength: 2
index: 1, matchLength: 3
index: 2, matchLength: 1
index: 2, matchLength: 2
```

## Scenarios for using this API

Most consumers shouldn't need to use these APIs. If you only care about whether a string begins with, ends with, or contains a substring, use the existing APIs on `string` or `CompareInfo`. Those APIs also offer better performance than APIs that return _matchLength_.

Scenarios where you might need the match length are:

* Syntax highlighting, so that you know how many characters to highlight.
* String replacement functions, where you need to know how many characters to delete from a string before inserting the replacement. (The `string.Replace` method uses these APIs.)
* Linguistic-aware trimming functions, so that you know how many characters to remove from the start or the end of the string.

Be aware that _matchLength_ might be `0`, even in a success case. This can occur if the _value_ string is empty or is non-empty but contains only ignorable characters.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.