dotnet / dotnet/dotnet-api-docs
Doc suggestion: explanatory text for new CompareInfo APIs
- 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
Assessment
This issue has not been assessed yet.