# Comparing, casing and splitting text the C# way

> The members of C#'s `string` that take a `StringComparison`, a `CultureInfo`, a `StringSplitOptions` or a `NormalizationForm` work as in C#, with C#'s answers. They work in a function, in the browser and, where the database can say the same thing, inside a query.

<!-- id: types-string-comparison · area: types · stability: stable · html: https://osysharp.com/reference/types/string-comparison/ -->

## Summary        {#summary}

Write the `string` code you would write in C#. `s.Contains(v, StringComparison.OrdinalIgnoreCase)`,
`string.Compare(a, b, StringComparison.Ordinal)`, `s.ToLower(new CultureInfo("tr-TR"))`,
`s.Split(',', StringSplitOptions.RemoveEmptyEntries)` and `string.Format("{0} items", n)` all answer what C# answers.
They give the same answer in a function, in the browser and inside a query.

## Signature      {#signature}

```osy syntax
bool   s.Contains(string value, StringComparison mode)
int    string.Compare(string a, string b, StringComparison mode)
string s.ToLower(CultureInfo culture)
List<string> s.Split(char separator, StringSplitOptions options)
string s.Normalize(NormalizationForm form)
string string.Format(string format, object arg0, …)
int    a.CompareTo(string b) · string.CompareOrdinal(string a, string b)
int    s.IndexOfAny(char[] anyOf) · s.LastIndexOfAny(char[] anyOf)
string s.ReplaceLineEndings() · s.ReplaceLineEndings(string replacement)
bool   s.IsNormalized() · s.IsNormalized(NormalizationForm form)
string new string(char[] chars) · new string(char[] chars, int start, int length)
string string.Intern(string s) · s.Clone()
int    s.GetHashCode() · TypeCode s.GetTypeCode()
```

## Description    {#description}

### How do the comparison modes differ?     {#comparison}
`StringComparison.Ordinal` compares UTF-16 code units. `OrdinalIgnoreCase` compares them after .NET's invariant
upper-casing. The two `InvariantCulture` modes compare linguistically. A `CurrentCulture` mode compares in the app's
current culture — the viewer's, else `app.DefaultCulture` — and is the invariant culture in an app that declares none;
it is never the host machine's ([Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/)). So is a culture-less `ToUpper()`/`ToLower()`.

Inside a query, a mode you write out as a literal picks the SQL. `Ordinal` becomes the plain comparison and
`OrdinalIgnoreCase` an upper-cased one. A **linguistic** mode has no SQL form, because the database compares under its
own collation and not .NET's, so a query that uses one is refused by name. Compare after `.ToList()` instead.

### Cultures are their names     {#culture}
A culture is held as its name. `new CultureInfo("en-US")` and `CultureInfo.GetCultureInfo("en-US")` mean exactly
that name, and `CultureInfo.InvariantCulture` means the culture-free one. Any member that takes a culture takes any of
these. A name .NET does not know is refused when you compile, where C# would throw `CultureNotFoundException` when it
runs.

Casing under a culture follows .NET exactly, and it matters for the Turkish i.

| | `"İ".ToLower(c)` | `"ı".ToUpper(c)` | `"i".ToUpper(c)` |
|---|---|---|---|
| `CultureInfo.InvariantCulture` | `"İ"` | `"ı"` | `"I"` |
| `new CultureInfo("en-US")` | `"i"` | `"I"` | `"I"` |
| `new CultureInfo("tr-TR")` | `"i"` | `"I"` | `"İ"` |

### How do splitting, trimming and normalising work?     {#split}
`Split` takes a `char`, a `string`, or an array of either, and also a count and `StringSplitOptions`. The options are
`RemoveEmptyEntries`, `TrimEntries` or both, combined with `|`. `Trim`, `TrimStart` and `TrimEnd` take a character
set; an empty set trims white space. `Normalize` and `IsNormalized` take a `NormalizationForm`; with none they
mean `FormC`, so `"cafe\u0301".Normalize()` is the one-character `"café"` and `"cafe\u0301".IsNormalized()` is
`false`. `s.ReplaceLineEndings("\n")` turns every line ending — `\r\n`, `\r`, `\n` and the Unicode ones — into the
text you give; with no argument it uses `"\n"`, on the server and in the browser alike.

### Where in the string is it?     {#positions}
`IndexOf`, `LastIndexOf`, `IndexOfAny` and `LastIndexOfAny` count in UTF-16 units and follow .NET's argument rules.
In particular, `LastIndexOf(value, startIndex)` accepts a start of exactly `Length`, and an empty value is found just
past the start. Inside a query the same numbers come back, including for text that holds emoji. A cut that would
split an emoji in half raises an error rather than storing half a character.

### How do I write values into text?     {#format}
`string.Format("{0} of {1}", done, total)` and `string.Concat(name, 5)` take values of any type, as C# does.
`string.Format` reads its format when you compile and turns it into an interpolation, so the format must be written
out in the source. A format held in a variable is refused, and the refusal shows the interpolation to write instead.

### What about the rest of `string`?     {#more-members}
`a.CompareTo(b)` and `string.Compare(a, b)` compare under the current culture, as C#'s do, and `string.CompareOrdinal(a, b)` by code unit —
so `string.CompareOrdinal("a", "B")` is positive, because `'a'` comes after `'B'` in UTF-16. `new string(chars)` and
`new string(chars, start, length)` build a string from a `char[]`; `new string(c, n)` repeats one character
([Text.TrimStart, Text.TrimEnd, Text.PadStart, Text.PadEnd, PadLeft, PadRight](https://osysharp.com/reference/function/text-affix/)). `s.GetHashCode()` is equal for equal strings within a run (as in C#, never store it),
`s.GetTypeCode()` is `TypeCode.String`, and `s.Clone()` and `string.Intern(s)` answer the same string — a string
is immutable, so there is nothing to copy or pool.

## Examples       {#examples}

```osy title="a case-insensitive search, in a query" test app=types-string-comparison
entity Customer {
  [Required, MaxLength(120)] string Name;
  security { allow read, create when IsAuthenticated; }
}

List<Customer> Matching(string term) {
  return Customer.Where(c => c.Name.Contains(term, StringComparison.OrdinalIgnoreCase)).ToList();
}
```

```osy title="casing under a culture" test app=types-string-comparison
string ForTurkishReaders(string s) {
  return s.ToUpper(new CultureInfo("tr-TR"));
}
```

```osy title="split, dropping empty fields and trimming the rest" test app=types-string-comparison
List<string> Fields(string row) {
  return row.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
```

```osy title="blank or missing" test app=types-string-comparison
bool HasNote(string? note) {
  return !string.IsNullOrWhiteSpace(note);
}
```

```osy title="a composite format with values of any type" test app=types-string-comparison
string Summary(int done, decimal total) {
  return string.Format("{0} done, {1} left", done, total);
}
```

```osy title="the less common string members, pinned" run app=types-string-members
[Test]
void String_members_answer_what_csharp_answers() {
  Assert.Equal("a|b|c", "a\r\nb\nc".ReplaceLineEndings("|"));
  Assert.Equal("café", "cafe\u0301".Normalize());
  Assert.False("cafe\u0301".IsNormalized());
  Assert.Equal(1, "a,b;c".IndexOfAny([',', ';']));
  Assert.Equal(3, "a,b;c".LastIndexOfAny([',', ';']));

  Assert.Equal("a\nb", "a\r\nb".ReplaceLineEndings());
  Assert.True("a".CompareTo("B") < 0);                // linguistic: a before B
  Assert.True(string.CompareOrdinal("a", "B") > 0);   // by code unit: 'a' (97) after 'B' (66)

  char[] letters = ['o', 's', 'y'];
  Assert.Equal("osy", new string(letters));
  Assert.Equal("sy", new string(letters, 1, 2));

  Assert.Equal("abc", "abc".Clone());
  Assert.Equal("x", string.Intern("x"));
  Assert.Equal("abc".GetHashCode(), ("ab" + "c").GetHashCode());
  Assert.Equal(TypeCode.String, "abc".GetTypeCode());
}
```

## See also       {#see-also}
- [char](https://osysharp.com/reference/types/char/) — one character, and why `Split(',')` and `Split(",")` differ
- [Text.Length, Text.IsEmpty, Text.IsBlank, Text.Contains, Text.StartsWith, Text.EndsWith](https://osysharp.com/reference/function/text-inspect/) — the everyday questions: contains, starts with, index of
- [Culture formatting — ToString(format, culture)](https://osysharp.com/reference/stdlib/culture-formatting/) — numbers and dates written for a reader's culture
