[API Proposal]: MemoryExtensions.CommonPrefixLength with StringComparison parameter
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
### Background and motivation
Presciently, the api review for `MemoryExtensions.CommonPrefixLength` [noted](https://github.com/dotnet/runtime/issues/64271#issuecomment-1097103048):
> - We might need to add an overload that is specific to char and takes StringComparison
There are two major reasons to add this overload:
1. It enables vectorization when doing an ordinalignorecase comparison. (Performance)
2. It no longer requires you to implement your own `IEqualityComparer` to gain the desired behavior. (Usability)
Currently, if you are looking for a common prefix case-insensitively among strings, you have to entirely give up on vectorization by passing an `IEqualityComparer` (which you also have to define yourself) which will then be called into for every char.
Prior art is the [`MemoryExtensions.Equals`](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.equals) extension on spans of chars that takes a `StringComparison` parameter. The rationale that applied there, applies here.
### API Proposal
```csharp
namespace System;
public static class MemoryExtensions
{
extension(ReadOnlySpan span)
{
public int CommonPrefixLength(ReadOnlySpan other, StringComparison comparisonType);
}
}
```
ℹ️ An extension is not defined on `Span`. This is due to the C# 14 language feature [_first-class spans_](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14#implicit-span-conversions) which enables the `ReadOnlySpan` extensions to appear when dotting off any `Span` expression.
### API Usage
Finding a common base folder among a bunch of file and folder paths, where paths are case-insensitive:
```csharp
public static string GetCommonPath(params IEnumerable paths)
{
using var enumerator = paths.GetEnumerator();
if (!enumerator.MoveNext())
return string.Empty;
var firstString = enumerator.Current;
var commonLength = firstString.Length;
if (commonLength == 0)
return firstString;
var first = firstString.AsSpan();
while (enumerator.MoveNext())
{
var current = enumerator.Current;
var newLength = first[..commonLength].CommonPrefixLength(current, StringComparison.OrdinalIgnoreCase);
var atSegmentBoundary =
(newLength == commonLength || first[newLength] is '/' or '\\')
&& (newLength == current.Length || current[newLength] is '/' or '\\');
commonLength = atSegmentBoundary
? newLength
: first[..newLength].LastIndexOfAny('/', '\\');
if (commonLength <= 0)
return "";
}
return commonLength == first.Length ? firstString : first[..commonLength].ToString();
}
```
### Alternative Designs
_No response_
### Risks
_No response_
Contributor guide
Assessment
This issue has not been assessed yet.