dotnet / dotnet/runtime

[API Proposal]: String.EqualsIgnoreCase()

Open
#123,808 11 comments 14 reactions 0 assignees View on GitHub
api-suggestion area-System.Runtime
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

### Background and motivation

As stated in #14065 invoking `string.Equals` with the comparison overload feels unintuitive. I believe this comes from two reasons:

1. `Ordinal` isn't something people think of when comparing strings. It feels like low-level lingo.
2. Operands are often compound expressions, which either forces me to assign them to a temporary or live with the fact the operation (`Equals`) and the way it's being conducted (`Ordinal`, `OrdinalIgnoreCase`) is split far part. One place where using a temporary is often not desirable is in lambda expressions where doing so requires converting the expression into a statement block. In case of LinQ this can start to feel really bulky. I have often resorted to extracting helper functions that just do the comparison, but that feels heavy handed too.

Since `String.Equals` is doing an ordinal comparison anyway, it seems we could address some of this by just introducing `EqualsIgnoreCase`.

> [!NOTE]
>
> **Culture-aware vs Ordinal.** It's worth pointing out that even if we're excluding the default of `Equals` being ordinal, the overwhelming majority of comparisons use `Ordinal` and `OrdinalIgnoreCase` (see API usage below). I don't believe these to be bugs. There are definitely cases where one wants to compare strings in a culture-aware fashion but most of the comparisons are for looking up things in dictionaries or for parsing, where culture not only isn't relevant but undesirable.

### API Proposal

```C#
namespace System;

public partial class String
{
public static bool EqualsIgnoreCase(string? a, string? b)
{
return Equals(a, b, StringComparison.OrdinalIgnoreCase);
}
}
```

### API Usage

Before:

```C#
var result = docs.Where(d => string.Equals(d.DocumentInfo?.DocumentType, "BusinessReport", StringComparison.OrdinalIgnoreCase);
```

After:

```C#
var result = docs.Where(d => string.EqualsIgnoreCase(d.DocumentInfo?.DocumentType, "BusinessReport");
```

### Alternative Designs

#### Minor variation

* `String.Equals()` already is doing an Ordinal comparison. So just naming it `EqualsIgnoreCase` rather than `EqualsOrdinalIgnoreCase` makes more sense.
* We could decide to include the other enum members of `StringComparison` as well, but I don't think they are used enough to warrant this, but they could of course be added later.
* #14065 points out that `Ordinal` and `OrdinalIgnoreCase` aren't intuitive terms, which this proposal partially addresses by removing `Ordinal`.

#### Relying on target-typed enums

* We could wait for
* In order to simplify this, we could decide to have aliases for `StringComparison.Ordinal` and `StringComparison.OrdinalIgnoreCase` that feel more intuitive, such as:

```C#
namespace System;

public partial enum StringComparison
{
CaseSensitive = Ordinal,
CaseInsensitive = OrdinalIgnoreCase,
}
```

Before:

```C#
var result = docs.Where(d => string.Equals(d.DocumentInfo?.DocumentType, "BusinessReport", StringComparison.OrdinalIgnoreCase);
```

After:

```C#
var result = docs.Where(d => string.Equals(d.DocumentInfo?.DocumentType, "BusinessReport", .CaseInsensitive);
```

The advantage of that is that it works for all methods that use a comparison, such as:

```C#
var result = text.StartsWith("API Review", .CaseInsensitive);
var result = text.Replace("API Review", "", .CaseInsensitive);
```

### Risks

None that I can think of.

### API Usages

Usages of different string comparisons


On GitHub according to grep.app:

| API | StringComparison | StringComparer |
| ---------------------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `Ordinal` | [75k](https://grep.app/search?q=StringComparison.Ordinal) | [24k](https://grep.app/search?q=StringComparer.Ordinal) |
| `OrdinalIgnoreCase` | [53k](https://grep.app/search?q=StringComparison.OrdinalIgnoreCase) | [19k](https://grep.app/search?q=StringComparer.OrdinalIgnoreCase) |
| `CurrentCulture` | [6k](https://grep.app/search?q=StringComparison.CurrentCulture) | [1k](https://grep.app/search?q=StringComparer.CurrentCulture) |
| `CurrentCultureIgnoreCase` | [5k](https://grep.app/search?q=StringComparison.CurrentCultureIgnoreCase) | [1k](https://grep.app/search?q=StringComparer.CurrentCultureIgnoreCase) |
| `InvariantCulture` | [14k](https://grep.app/search?q=StringComparison.InvariantCulture) | [3k](https://grep.app/search?q=StringComparer.InvariantCulture) |
| `InvariantCultureIgnoreCase` | [12k](https://grep.app/search?q=StringComparison.InvariantCultureIgnoreCase) | [3k](https://grep.app/search?q=StringComparer.InvariantCultureIgnoreCase) |

On nuget.org according to API Catalog:

| API | StringComparer |
| ---------------------------- | ------------------------------------------------------------------------------ |
| `Ordinal` | [2.6%](https://catalog.apireview.net/catalog/94b2021e462639653b07497f5204fd5c) |
| `OrdinalIgnoreCase` | [6%](https://catalog.apireview.net/catalog/38c3b959414244799b9f792a01b72886) |
| `CurrentCulture` | [0.3%](https://catalog.apireview.net/catalog/c28d9a3e75272c31a8a49077b6185ab5) |
| `CurrentCultureIgnoreCase` | [0.9%](https://catalog.apireview.net/catalog/0959cd427a4d12cdb9dec8c9e02b4402) |
| `InvariantCulture` | [0.4%](https://catalog.apireview.net/catalog/0b53ffbe5611a664c4bf5c974fa1f710) |
| `InvariantCultureIgnoreCase` | [1.8%](https://catalog.apireview.net/catalog/ef788e51f51ea5c59699b8cd8b968b6b) |

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.