Skip to content

[API Proposal]: MemoryExtensions.CommonPrefixLength with StringComparison parameter #126756

@jnm2

Description

@jnm2

Background and motivation

Presciently, the api review for MemoryExtensions.CommonPrefixLength<T> noted:

  • 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<char> 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<char> (which you also have to define yourself) which will then be called into for every char.

Prior art is the MemoryExtensions.Equals extension on spans of chars that takes a StringComparison parameter. The rationale that applied there, applies here.

API Proposal

namespace System;

public static class MemoryExtensions
{
    extension(ReadOnlySpan<char> span)
    {
        public int CommonPrefixLength(ReadOnlySpan<char> other, StringComparison comparisonType);
    }
}

ℹ️ An extension is not defined on Span<T>. This is due to the C# 14 language feature first-class spans which enables the ReadOnlySpan<T> extensions to appear when dotting off any Span<T> expression.

API Usage

Finding a common base folder among a bunch of file and folder paths, where paths are case-insensitive:

public static string GetCommonPath(params IEnumerable<string> 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-suggestionEarly API idea and discussion, it is NOT ready for implementationarea-System.RuntimeuntriagedNew issue has not been triaged by the area owner

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions