From c1c06ed01083e18cef62a078a490f5beb33d6dab Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Wed, 9 Sep 2026 22:11:36 +1000 Subject: [PATCH] Add net11 ordinal casing APIs Adds the ordinal casing family from net11: * char.ToLowerOrdinal(char) and char.ToUpperOrdinal(char) * string.ToLowerOrdinal() and string.ToUpperOrdinal() * ReadOnlySpan.ToLowerOrdinal(Span) and ToUpperOrdinal * Rune.ToLowerOrdinal(Rune), Rune.ToUpperOrdinal(Rune) and Rune.Equals(Rune, StringComparison) Ordinal casing is not invariant casing. It deliberately declines any mapping that would move a character out of its ordinal upper-casing class, so that it stays consistent with OrdinalIgnoreCase. The Kelvin, Ohm and Angstrom signs, the capital sharp s, the Greek capital theta symbol and the long s are all left uncased where invariant casing would map them. The implementation follows the algorithm the runtime itself uses when the ICU ordinal table is unavailable: invariant casing, with the lower case mapping discarded when it would change the character's ordinal upper case form. Verified against net11 over the whole BMP, where it now agrees on every code point. Casing is applied per scalar rather than per char, so surrogate pairs are mapped, matching net11. The Rune members are only available from netcoreapp3.0, since Rune does not exist earlier and is not part of netstandard2.1. Also fixes char.Equals(char, StringComparison), which used invariant casing for OrdinalIgnoreCase and so reported the long s as equal to s where net11 reports it as not equal. --- apiCount.include.md | 2 +- api_list.include.md | 21 +++ assemblySize.include.md | 8 +- readme.md | 35 ++++- src/Consume/Consume.cs | 17 +++ src/Polyfill/CharPolyfill.cs | 86 +++++++++++ src/Polyfill/Polyfill_Char.cs | 2 +- src/Polyfill/Polyfill_Memory.cs | 52 +++++++ src/Polyfill/Polyfill_Rune.cs | 23 +++ src/Polyfill/Polyfill_String.cs | 31 ++++ src/Polyfill/RunePolyfill.cs | 41 +++++ src/Split/net10.0/CharPolyfill.cs | 71 +++++++++ src/Split/net10.0/Polyfill_Char.cs | 2 +- src/Split/net10.0/Polyfill_Memory.cs | 47 ++++++ src/Split/net10.0/Polyfill_Rune.cs | 18 +++ src/Split/net10.0/Polyfill_String.cs | 20 +++ src/Split/net10.0/RunePolyfill.cs | 30 ++++ src/Split/net461/CharPolyfill.cs | 62 ++++++++ src/Split/net461/Polyfill_Char.cs | 2 +- src/Split/net461/Polyfill_Memory.cs | 37 +++++ src/Split/net461/Polyfill_String.cs | 20 +++ src/Split/net462/CharPolyfill.cs | 62 ++++++++ src/Split/net462/Polyfill_Char.cs | 2 +- src/Split/net462/Polyfill_Memory.cs | 37 +++++ src/Split/net462/Polyfill_String.cs | 20 +++ src/Split/net47/CharPolyfill.cs | 62 ++++++++ src/Split/net47/Polyfill_Char.cs | 2 +- src/Split/net47/Polyfill_Memory.cs | 37 +++++ src/Split/net47/Polyfill_String.cs | 20 +++ src/Split/net471/CharPolyfill.cs | 62 ++++++++ src/Split/net471/Polyfill_Char.cs | 2 +- src/Split/net471/Polyfill_Memory.cs | 37 +++++ src/Split/net471/Polyfill_String.cs | 20 +++ src/Split/net472/CharPolyfill.cs | 62 ++++++++ src/Split/net472/Polyfill_Char.cs | 2 +- src/Split/net472/Polyfill_Memory.cs | 37 +++++ src/Split/net472/Polyfill_String.cs | 20 +++ src/Split/net48/CharPolyfill.cs | 62 ++++++++ src/Split/net48/Polyfill_Char.cs | 2 +- src/Split/net48/Polyfill_Memory.cs | 37 +++++ src/Split/net48/Polyfill_String.cs | 20 +++ src/Split/net481/CharPolyfill.cs | 62 ++++++++ src/Split/net481/Polyfill_Char.cs | 2 +- src/Split/net481/Polyfill_Memory.cs | 37 +++++ src/Split/net481/Polyfill_String.cs | 20 +++ src/Split/net5.0/CharPolyfill.cs | 62 ++++++++ src/Split/net5.0/Polyfill_Char.cs | 2 +- src/Split/net5.0/Polyfill_Memory.cs | 37 +++++ src/Split/net5.0/Polyfill_Rune.cs | 18 +++ src/Split/net5.0/Polyfill_String.cs | 20 +++ src/Split/net5.0/RunePolyfill.cs | 30 ++++ src/Split/net6.0/CharPolyfill.cs | 62 ++++++++ src/Split/net6.0/Polyfill_Char.cs | 2 +- src/Split/net6.0/Polyfill_Memory.cs | 47 ++++++ src/Split/net6.0/Polyfill_Rune.cs | 18 +++ src/Split/net6.0/Polyfill_String.cs | 20 +++ src/Split/net6.0/RunePolyfill.cs | 30 ++++ src/Split/net7.0/CharPolyfill.cs | 62 ++++++++ src/Split/net7.0/Polyfill_Char.cs | 2 +- src/Split/net7.0/Polyfill_Memory.cs | 47 ++++++ src/Split/net7.0/Polyfill_Rune.cs | 18 +++ src/Split/net7.0/Polyfill_String.cs | 20 +++ src/Split/net7.0/RunePolyfill.cs | 30 ++++ src/Split/net8.0/CharPolyfill.cs | 71 +++++++++ src/Split/net8.0/Polyfill_Char.cs | 2 +- src/Split/net8.0/Polyfill_Memory.cs | 47 ++++++ src/Split/net8.0/Polyfill_Rune.cs | 18 +++ src/Split/net8.0/Polyfill_String.cs | 20 +++ src/Split/net8.0/RunePolyfill.cs | 30 ++++ src/Split/net9.0/CharPolyfill.cs | 71 +++++++++ src/Split/net9.0/Polyfill_Char.cs | 2 +- src/Split/net9.0/Polyfill_Memory.cs | 47 ++++++ src/Split/net9.0/Polyfill_Rune.cs | 18 +++ src/Split/net9.0/Polyfill_String.cs | 20 +++ src/Split/net9.0/RunePolyfill.cs | 30 ++++ src/Split/netcoreapp2.0/CharPolyfill.cs | 62 ++++++++ src/Split/netcoreapp2.0/Polyfill_Char.cs | 2 +- src/Split/netcoreapp2.0/Polyfill_Memory.cs | 37 +++++ src/Split/netcoreapp2.0/Polyfill_String.cs | 20 +++ src/Split/netcoreapp2.1/CharPolyfill.cs | 62 ++++++++ src/Split/netcoreapp2.1/Polyfill_Char.cs | 2 +- src/Split/netcoreapp2.1/Polyfill_Memory.cs | 37 +++++ src/Split/netcoreapp2.1/Polyfill_String.cs | 20 +++ src/Split/netcoreapp2.2/CharPolyfill.cs | 62 ++++++++ src/Split/netcoreapp2.2/Polyfill_Char.cs | 2 +- src/Split/netcoreapp2.2/Polyfill_Memory.cs | 37 +++++ src/Split/netcoreapp2.2/Polyfill_String.cs | 20 +++ src/Split/netcoreapp3.0/CharPolyfill.cs | 62 ++++++++ src/Split/netcoreapp3.0/Polyfill_Char.cs | 2 +- src/Split/netcoreapp3.0/Polyfill_Memory.cs | 37 +++++ src/Split/netcoreapp3.0/Polyfill_Rune.cs | 18 +++ src/Split/netcoreapp3.0/Polyfill_String.cs | 20 +++ src/Split/netcoreapp3.0/RunePolyfill.cs | 30 ++++ src/Split/netcoreapp3.1/CharPolyfill.cs | 62 ++++++++ src/Split/netcoreapp3.1/Polyfill_Char.cs | 2 +- src/Split/netcoreapp3.1/Polyfill_Memory.cs | 37 +++++ src/Split/netcoreapp3.1/Polyfill_Rune.cs | 18 +++ src/Split/netcoreapp3.1/Polyfill_String.cs | 20 +++ src/Split/netcoreapp3.1/RunePolyfill.cs | 30 ++++ src/Split/netstandard2.0/CharPolyfill.cs | 62 ++++++++ src/Split/netstandard2.0/Polyfill_Char.cs | 2 +- src/Split/netstandard2.0/Polyfill_Memory.cs | 37 +++++ src/Split/netstandard2.0/Polyfill_String.cs | 20 +++ src/Split/netstandard2.1/CharPolyfill.cs | 62 ++++++++ src/Split/netstandard2.1/Polyfill_Char.cs | 2 +- src/Split/netstandard2.1/Polyfill_Memory.cs | 37 +++++ src/Split/netstandard2.1/Polyfill_String.cs | 20 +++ src/Split/uap10.0/CharPolyfill.cs | 62 ++++++++ src/Split/uap10.0/Polyfill_Char.cs | 2 +- src/Split/uap10.0/Polyfill_Memory.cs | 37 +++++ src/Split/uap10.0/Polyfill_String.cs | 20 +++ src/Tests/PolyfillTests_OrdinalCasing.cs | 160 ++++++++++++++++++++ 112 files changed, 3446 insertions(+), 34 deletions(-) create mode 100644 src/Polyfill/Polyfill_Rune.cs create mode 100644 src/Polyfill/RunePolyfill.cs create mode 100644 src/Split/net10.0/CharPolyfill.cs create mode 100644 src/Split/net10.0/Polyfill_Memory.cs create mode 100644 src/Split/net10.0/Polyfill_Rune.cs create mode 100644 src/Split/net10.0/RunePolyfill.cs create mode 100644 src/Split/net5.0/Polyfill_Rune.cs create mode 100644 src/Split/net5.0/RunePolyfill.cs create mode 100644 src/Split/net6.0/Polyfill_Memory.cs create mode 100644 src/Split/net6.0/Polyfill_Rune.cs create mode 100644 src/Split/net6.0/RunePolyfill.cs create mode 100644 src/Split/net7.0/Polyfill_Memory.cs create mode 100644 src/Split/net7.0/Polyfill_Rune.cs create mode 100644 src/Split/net7.0/RunePolyfill.cs create mode 100644 src/Split/net8.0/CharPolyfill.cs create mode 100644 src/Split/net8.0/Polyfill_Memory.cs create mode 100644 src/Split/net8.0/Polyfill_Rune.cs create mode 100644 src/Split/net8.0/RunePolyfill.cs create mode 100644 src/Split/net9.0/CharPolyfill.cs create mode 100644 src/Split/net9.0/Polyfill_Memory.cs create mode 100644 src/Split/net9.0/Polyfill_Rune.cs create mode 100644 src/Split/net9.0/RunePolyfill.cs create mode 100644 src/Split/netcoreapp3.0/Polyfill_Rune.cs create mode 100644 src/Split/netcoreapp3.0/RunePolyfill.cs create mode 100644 src/Split/netcoreapp3.1/Polyfill_Rune.cs create mode 100644 src/Split/netcoreapp3.1/RunePolyfill.cs create mode 100644 src/Tests/PolyfillTests_OrdinalCasing.cs diff --git a/apiCount.include.md b/apiCount.include.md index 05c596a5e..3aa326973 100644 --- a/apiCount.include.md +++ b/apiCount.include.md @@ -1,4 +1,4 @@ -**API count: 1038** +**API count: 1047** ### Per Target Framework diff --git a/api_list.include.md b/api_list.include.md index 7ba88a030..52855e7a5 100644 --- a/api_list.include.md +++ b/api_list.include.md @@ -131,6 +131,10 @@ * `bool IsAsciiLetterOrDigit(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isasciiletterordigit?view=net-11.0) * `bool IsAsciiLetterUpper(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isasciiletterupper?view=net-11.0) * `bool IsBetween(char, char, char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isbetween?view=net-11.0) + * `char ToLowerOrdinal(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `char ToUpperOrdinal(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. #### ConcurrentBag @@ -879,6 +883,10 @@ * `int SplitAny(Span, ReadOnlySpan, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.splitany?view=net-11.0#system-memoryextensions-splitany(system-readonlyspan((system-char))-system-span((system-range))-system-readonlyspan((system-char))-system-stringsplitoptions)) * `int SplitAny(Span, ReadOnlySpan, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.splitany?view=net-11.0#system-memoryextensions-splitany(system-readonlyspan((system-char))-system-span((system-range))-system-readonlyspan((system-string))-system-stringsplitoptions)) * `bool StartsWith(string, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith?view=net-11.0#system-memoryextensions-startswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `int ToLowerOrdinal(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `int ToUpperOrdinal(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. * `bool TryNormalize(Span, int, NormalizationForm)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.stringnormalizationextensions.trynormalize?view=net-11.0#system-stringnormalizationextensions-trynormalize(system-readonlyspan((system-char))-system-span((system-char))-system-int32@-system-text-normalizationform)) @@ -948,6 +956,15 @@ * `bool IsMatch(ReadOnlySpan, string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch?view=net-11.0#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string)) +#### Rune + + * `bool Equals(Rune, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.equals?view=net-11.0#system-text-rune-equals(system-text-rune-system-stringcomparison)) + * `Rune ToLowerOrdinal(Rune)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `Rune ToUpperOrdinal(Rune)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + + #### RuntimeFeature * `IsMultithreadingSupported` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.runtimefeature.ismultithreadingsupported?view=net-11.0) @@ -1138,6 +1155,10 @@ * `string[] Split(string, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split?view=net-11.0#system-string-split(system-string-system-stringsplitoptions)) * `bool StartsWith(char, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.startswith?view=net-11.0#system-string-startswith(system-char-system-stringcomparison)) * `bool StartsWith(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.startswith?view=net-11.0#system-string-startswith(system-char)) + * `string ToLowerOrdinal()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `string ToUpperOrdinal()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. * `bool TryCopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.trycopyto?view=net-11.0) * `string Concat(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.concat?view=net-11.0#system-string-concat(system-readonlyspan((system-object)))) * `string Concat(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.concat?view=net-11.0#system-string-concat(system-readonlyspan((system-string)))) diff --git a/assemblySize.include.md b/assemblySize.include.md index e6082d749..644fb47ec 100644 --- a/assemblySize.include.md +++ b/assemblySize.include.md @@ -18,7 +18,7 @@ | netcoreapp3.1 | 9.5KB | 310.5KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | net5.0 | 9.5KB | 274.5KB | +265.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | net6.0 | 10.0KB | 216.0KB | +206.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 178.0KB | +168.0KB | +12.0KB | +8.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 178.0KB | +168.0KB | +12.0KB | +8.0KB | +512bytes | +3.0KB | | net8.0 | 9.5KB | 148.5KB | +139.0KB | +8.5KB | | +512bytes | +3.0KB | | net9.0 | 9.5KB | 101.5KB | +92.0KB | +8.5KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 79.0KB | +69.0KB | +8.5KB | | +512bytes | +3.5KB | @@ -31,8 +31,8 @@ |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 530.1KB | +522.1KB | +15.2KB | +8.7KB | +12.9KB | +17.9KB | | netstandard2.1 | 8.5KB | 457.5KB | +449.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net461 | 8.5KB | 530.1KB | +521.6KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net462 | 7.0KB | 533.6KB | +526.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 530.2KB | +521.7KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | +| net462 | 7.0KB | 533.7KB | +526.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net47 | 7.0KB | 532.9KB | +525.9KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | | net471 | 8.5KB | 532.0KB | +523.5KB | +15.2KB | +6.7KB | +12.4KB | +17.4KB | | net472 | 8.5KB | 529.5KB | +521.0KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | @@ -45,7 +45,7 @@ | netcoreapp3.1 | 9.5KB | 447.3KB | +437.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | net5.0 | 9.5KB | 393.2KB | +383.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | net6.0 | 10.0KB | 314.5KB | +304.5KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 257.6KB | +247.6KB | +19.6KB | +9.4KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 257.7KB | +247.7KB | +19.6KB | +9.4KB | +1.1KB | +3.7KB | | net8.0 | 9.5KB | 212.7KB | +203.2KB | +16.0KB | +299bytes | +1.1KB | +3.7KB | | net9.0 | 9.5KB | 144.5KB | +135.0KB | +16.0KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 113.4KB | +103.4KB | +16.0KB | | +1.1KB | +4.2KB | diff --git a/readme.md b/readme.md index c970e6562..2406c98cd 100644 --- a/readme.md +++ b/readme.md @@ -13,7 +13,7 @@ The package targets `netstandard2.0` and is designed to support the following ru * `uap10` -**API count: 1038** +**API count: 1047** ### Per Target Framework @@ -112,7 +112,7 @@ This project uses features from the newest stable SDK and C# language. As such c | netcoreapp3.1 | 9.5KB | 310.5KB | +301.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | net5.0 | 9.5KB | 274.5KB | +265.0KB | +9.0KB | +6.5KB | +9.0KB | +14.0KB | | net6.0 | 10.0KB | 216.0KB | +206.0KB | +10.0KB | +7.0KB | +512bytes | +3.5KB | -| net7.0 | 10.0KB | 178.0KB | +168.0KB | +12.0KB | +8.0KB | +512bytes | +3.5KB | +| net7.0 | 10.0KB | 178.0KB | +168.0KB | +12.0KB | +8.0KB | +512bytes | +3.0KB | | net8.0 | 9.5KB | 148.5KB | +139.0KB | +8.5KB | | +512bytes | +3.0KB | | net9.0 | 9.5KB | 101.5KB | +92.0KB | +8.5KB | | +512bytes | +3.5KB | | net10.0 | 10.0KB | 79.0KB | +69.0KB | +8.5KB | | +512bytes | +3.5KB | @@ -125,8 +125,8 @@ This project uses features from the newest stable SDK and C# language. As such c |----------------|----------------|---------------|-----------|-----------|--------------------|---------------------|-------------| | netstandard2.0 | 8.0KB | 530.1KB | +522.1KB | +15.2KB | +8.7KB | +12.9KB | +17.9KB | | netstandard2.1 | 8.5KB | 457.5KB | +449.0KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | -| net461 | 8.5KB | 530.1KB | +521.6KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | -| net462 | 7.0KB | 533.6KB | +526.6KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | +| net461 | 8.5KB | 530.2KB | +521.7KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | +| net462 | 7.0KB | 533.7KB | +526.7KB | +16.7KB | +8.2KB | +13.9KB | +18.9KB | | net47 | 7.0KB | 532.9KB | +525.9KB | +17.2KB | +8.2KB | +14.4KB | +19.4KB | | net471 | 8.5KB | 532.0KB | +523.5KB | +15.2KB | +6.7KB | +12.4KB | +17.4KB | | net472 | 8.5KB | 529.5KB | +521.0KB | +15.2KB | +8.2KB | +12.4KB | +17.4KB | @@ -139,7 +139,7 @@ This project uses features from the newest stable SDK and C# language. As such c | netcoreapp3.1 | 9.5KB | 447.3KB | +437.8KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | net5.0 | 9.5KB | 393.2KB | +383.7KB | +16.7KB | +8.2KB | +13.9KB | +19.4KB | | net6.0 | 10.0KB | 314.5KB | +304.5KB | +17.7KB | +8.7KB | +1.1KB | +4.2KB | -| net7.0 | 10.0KB | 257.6KB | +247.6KB | +19.6KB | +9.4KB | +1.1KB | +4.2KB | +| net7.0 | 10.0KB | 257.7KB | +247.7KB | +19.6KB | +9.4KB | +1.1KB | +3.7KB | | net8.0 | 9.5KB | 212.7KB | +203.2KB | +16.0KB | +299bytes | +1.1KB | +3.7KB | | net9.0 | 9.5KB | 144.5KB | +135.0KB | +16.0KB | | +1.1KB | +4.2KB | | net10.0 | 10.0KB | 113.4KB | +103.4KB | +16.0KB | | +1.1KB | +4.2KB | @@ -746,6 +746,10 @@ The class `Polyfill` includes the following extension methods: * `bool IsAsciiLetterOrDigit(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isasciiletterordigit?view=net-11.0) * `bool IsAsciiLetterUpper(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isasciiletterupper?view=net-11.0) * `bool IsBetween(char, char, char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.isbetween?view=net-11.0) + * `char ToLowerOrdinal(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `char ToUpperOrdinal(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.char.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. #### ConcurrentBag @@ -1494,6 +1498,10 @@ The class `Polyfill` includes the following extension methods: * `int SplitAny(Span, ReadOnlySpan, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.splitany?view=net-11.0#system-memoryextensions-splitany(system-readonlyspan((system-char))-system-span((system-range))-system-readonlyspan((system-char))-system-stringsplitoptions)) * `int SplitAny(Span, ReadOnlySpan, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.splitany?view=net-11.0#system-memoryextensions-splitany(system-readonlyspan((system-char))-system-span((system-range))-system-readonlyspan((system-string))-system-stringsplitoptions)) * `bool StartsWith(string, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.startswith?view=net-11.0#system-memoryextensions-startswith-1(system-readonlyspan((-0))-system-readonlyspan((-0)))) + * `int ToLowerOrdinal(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `int ToUpperOrdinal(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. * `bool TryNormalize(Span, int, NormalizationForm)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.stringnormalizationextensions.trynormalize?view=net-11.0#system-stringnormalizationextensions-trynormalize(system-readonlyspan((system-char))-system-span((system-char))-system-int32@-system-text-normalizationform)) @@ -1563,6 +1571,15 @@ The class `Polyfill` includes the following extension methods: * `bool IsMatch(ReadOnlySpan, string)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.regularexpressions.regex.ismatch?view=net-11.0#system-text-regularexpressions-regex-ismatch(system-readonlyspan((system-char))-system-string)) +#### Rune + + * `bool Equals(Rune, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.equals?view=net-11.0#system-text-rune-equals(system-text-rune-system-stringcomparison)) + * `Rune ToLowerOrdinal(Rune)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `Rune ToUpperOrdinal(Rune)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + + #### RuntimeFeature * `IsMultithreadingSupported` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.runtimefeature.ismultithreadingsupported?view=net-11.0) @@ -1753,6 +1770,10 @@ The class `Polyfill` includes the following extension methods: * `string[] Split(string, StringSplitOptions)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.split?view=net-11.0#system-string-split(system-string-system-stringsplitoptions)) * `bool StartsWith(char, StringComparison)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.startswith?view=net-11.0#system-string-startswith(system-char-system-stringcomparison)) * `bool StartsWith(char)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.startswith?view=net-11.0#system-string-startswith(system-char)) + * `string ToLowerOrdinal()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.tolowerordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + * `string ToUpperOrdinal()` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.toupperordinal?view=net-11.0) + * Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. * `bool TryCopyTo(Span)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.trycopyto?view=net-11.0) * `string Concat(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.concat?view=net-11.0#system-string-concat(system-readonlyspan((system-object)))) * `string Concat(ReadOnlySpan)` [reference](https://learn.microsoft.com/en-us/dotnet/api/system.string.concat?view=net-11.0#system-string-concat(system-readonlyspan((system-string)))) @@ -2362,7 +2383,7 @@ void ObjectDisposedExceptionExample(bool isDisposed) ObjectDisposedException.ThrowIf(isDisposed, typeof(Consume)); } ``` -snippet source | anchor +snippet source | anchor @@ -2381,7 +2402,7 @@ void EnsureExample(Order order, Customer customer, string customerId, string ema this.quantity = Ensure.NotNegativeOrZero(quantity); } ``` -snippet source | anchor +snippet source | anchor diff --git a/src/Consume/Consume.cs b/src/Consume/Consume.cs index f26c62dbc..dca6aca4b 100644 --- a/src/Consume/Consume.cs +++ b/src/Consume/Consume.cs @@ -646,6 +646,23 @@ void Char_Methods() var isAsciiHexDigitUpper = char.IsAsciiHexDigitUpper('\u0041'); var charEquals = 'A'.Equals('a', StringComparison.OrdinalIgnoreCase); var isBetween = char.IsBetween('c', 'a', 'z'); + char toLowerOrdinalChar = char.ToLowerOrdinal('A'); + char toUpperOrdinalChar = char.ToUpperOrdinal('a'); + + string toLowerOrdinal = "ABC".ToLowerOrdinal(); + string toUpperOrdinal = "abc".ToUpperOrdinal(); + +#if FeatureMemory + Span casingDestination = new char[3]; + int loweredCount = "ABC".AsSpan().ToLowerOrdinal(casingDestination); + int upperedCount = "abc".AsSpan().ToUpperOrdinal(casingDestination); +#endif + +#if NETCOREAPP3_0_OR_GREATER + Rune loweredRune = Rune.ToLowerOrdinal(new('A')); + Rune upperedRune = Rune.ToUpperOrdinal(new('a')); + bool runeEquals = new Rune('A').Equals(new('a'), StringComparison.OrdinalIgnoreCase); +#endif } class WithMethods diff --git a/src/Polyfill/CharPolyfill.cs b/src/Polyfill/CharPolyfill.cs index dbdebc190..71407c65f 100644 --- a/src/Polyfill/CharPolyfill.cs +++ b/src/Polyfill/CharPolyfill.cs @@ -79,5 +79,91 @@ public static bool IsAsciiLetterLower(char c) => public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); #endif + +#if !NET11_0_OR_GREATER + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.char.tolowerordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.char.toupperordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); +#endif } + +#if !NET11_0_OR_GREATER + + internal static char ToUpperOrdinalChar(char c) => + // ordinal casing leaves the long s uncased, where invariant casing maps it to S + c == 'ſ' ? c : char.ToUpperInvariant(c); + + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + + // Ordinal lower casing must never move a character out of its ordinal upper-casing class, otherwise it + // stops being consistent with OrdinalIgnoreCase. The Kelvin, Ohm and Angstrom signs are the usual examples. + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; + } + +#endif } diff --git a/src/Polyfill/Polyfill_Char.cs b/src/Polyfill/Polyfill_Char.cs index c16c5a311..12d579f17 100644 --- a/src/Polyfill/Polyfill_Char.cs +++ b/src/Polyfill/Polyfill_Char.cs @@ -13,7 +13,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Polyfill/Polyfill_Memory.cs b/src/Polyfill/Polyfill_Memory.cs index 23cdb24d0..51280d15b 100644 --- a/src/Polyfill/Polyfill_Memory.cs +++ b/src/Polyfill/Polyfill_Memory.cs @@ -87,6 +87,58 @@ public static bool SequenceEqual( #endif +#if !NET11_0_OR_GREATER + + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.tolowerordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.toupperordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + + if (destination.Length < source.Length) + { + return -1; + } + + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + + return source.Length; + } + +#endif + } #endif diff --git a/src/Polyfill/Polyfill_Rune.cs b/src/Polyfill/Polyfill_Rune.cs new file mode 100644 index 000000000..c6e83bd58 --- /dev/null +++ b/src/Polyfill/Polyfill_Rune.cs @@ -0,0 +1,23 @@ +#if !NET11_0_OR_GREATER && NETCOREAPP3_0_OR_GREATER + +namespace Polyfills; + +using System; +using System.Text; + +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.equals?view=net-11.0#system-text-rune-equals(system-text-rune-system-stringcomparison) + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} + +#endif diff --git a/src/Polyfill/Polyfill_String.cs b/src/Polyfill/Polyfill_String.cs index 0e16714a4..2a68aa76c 100644 --- a/src/Polyfill/Polyfill_String.cs +++ b/src/Polyfill/Polyfill_String.cs @@ -259,4 +259,35 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); #endif + +#if !NET11_0_OR_GREATER + + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.string.tolowerordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.string.toupperordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } +#endif } diff --git a/src/Polyfill/RunePolyfill.cs b/src/Polyfill/RunePolyfill.cs new file mode 100644 index 000000000..b50b6383e --- /dev/null +++ b/src/Polyfill/RunePolyfill.cs @@ -0,0 +1,41 @@ +#if !NET11_0_OR_GREATER && NETCOREAPP3_0_OR_GREATER + +namespace Polyfills; + +using System.Text; + +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.tolowerordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + //Link: https://learn.microsoft.com/en-us/dotnet/api/system.text.rune.toupperordinal?view=net-11.0 + //Note: Derived from invariant casing, so the mapping follows the Unicode version of the running framework rather than the one net11 is built against. + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} + +#endif diff --git a/src/Split/net10.0/CharPolyfill.cs b/src/Split/net10.0/CharPolyfill.cs new file mode 100644 index 000000000..a4d449304 --- /dev/null +++ b/src/Split/net10.0/CharPolyfill.cs @@ -0,0 +1,71 @@ +// +#pragma warning disable +namespace Polyfills; +static partial class Polyfill +{ + extension(char) + { + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; + } +} diff --git a/src/Split/net10.0/Polyfill_Char.cs b/src/Split/net10.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net10.0/Polyfill_Char.cs +++ b/src/Split/net10.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net10.0/Polyfill_Memory.cs b/src/Split/net10.0/Polyfill_Memory.cs new file mode 100644 index 000000000..889e702e1 --- /dev/null +++ b/src/Split/net10.0/Polyfill_Memory.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable +#if FeatureMemory +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } +} +#endif diff --git a/src/Split/net10.0/Polyfill_Rune.cs b/src/Split/net10.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net10.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net10.0/Polyfill_String.cs b/src/Split/net10.0/Polyfill_String.cs index 99ec08a0c..7de4b00dd 100644 --- a/src/Split/net10.0/Polyfill_String.cs +++ b/src/Split/net10.0/Polyfill_String.cs @@ -49,4 +49,24 @@ public static int LastIndexOf(this string target, char value, int startIndex, St /// public static int LastIndexOf(this string target, char value, int startIndex, int count, StringComparison comparisonType) => target.LastIndexOf(value.ToString(), startIndex, count, comparisonType); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net10.0/RunePolyfill.cs b/src/Split/net10.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net10.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/net461/CharPolyfill.cs b/src/Split/net461/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net461/CharPolyfill.cs +++ b/src/Split/net461/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net461/Polyfill_Char.cs b/src/Split/net461/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net461/Polyfill_Char.cs +++ b/src/Split/net461/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net461/Polyfill_Memory.cs b/src/Split/net461/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net461/Polyfill_Memory.cs +++ b/src/Split/net461/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net461/Polyfill_String.cs b/src/Split/net461/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net461/Polyfill_String.cs +++ b/src/Split/net461/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net462/CharPolyfill.cs b/src/Split/net462/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net462/CharPolyfill.cs +++ b/src/Split/net462/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net462/Polyfill_Char.cs b/src/Split/net462/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net462/Polyfill_Char.cs +++ b/src/Split/net462/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net462/Polyfill_Memory.cs b/src/Split/net462/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net462/Polyfill_Memory.cs +++ b/src/Split/net462/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net462/Polyfill_String.cs b/src/Split/net462/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net462/Polyfill_String.cs +++ b/src/Split/net462/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net47/CharPolyfill.cs b/src/Split/net47/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net47/CharPolyfill.cs +++ b/src/Split/net47/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net47/Polyfill_Char.cs b/src/Split/net47/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net47/Polyfill_Char.cs +++ b/src/Split/net47/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net47/Polyfill_Memory.cs b/src/Split/net47/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net47/Polyfill_Memory.cs +++ b/src/Split/net47/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net47/Polyfill_String.cs b/src/Split/net47/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net47/Polyfill_String.cs +++ b/src/Split/net47/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net471/CharPolyfill.cs b/src/Split/net471/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net471/CharPolyfill.cs +++ b/src/Split/net471/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net471/Polyfill_Char.cs b/src/Split/net471/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net471/Polyfill_Char.cs +++ b/src/Split/net471/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net471/Polyfill_Memory.cs b/src/Split/net471/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net471/Polyfill_Memory.cs +++ b/src/Split/net471/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net471/Polyfill_String.cs b/src/Split/net471/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net471/Polyfill_String.cs +++ b/src/Split/net471/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net472/CharPolyfill.cs b/src/Split/net472/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net472/CharPolyfill.cs +++ b/src/Split/net472/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net472/Polyfill_Char.cs b/src/Split/net472/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net472/Polyfill_Char.cs +++ b/src/Split/net472/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net472/Polyfill_Memory.cs b/src/Split/net472/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net472/Polyfill_Memory.cs +++ b/src/Split/net472/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net472/Polyfill_String.cs b/src/Split/net472/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net472/Polyfill_String.cs +++ b/src/Split/net472/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net48/CharPolyfill.cs b/src/Split/net48/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net48/CharPolyfill.cs +++ b/src/Split/net48/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net48/Polyfill_Char.cs b/src/Split/net48/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net48/Polyfill_Char.cs +++ b/src/Split/net48/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net48/Polyfill_Memory.cs b/src/Split/net48/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net48/Polyfill_Memory.cs +++ b/src/Split/net48/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net48/Polyfill_String.cs b/src/Split/net48/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net48/Polyfill_String.cs +++ b/src/Split/net48/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net481/CharPolyfill.cs b/src/Split/net481/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net481/CharPolyfill.cs +++ b/src/Split/net481/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net481/Polyfill_Char.cs b/src/Split/net481/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net481/Polyfill_Char.cs +++ b/src/Split/net481/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net481/Polyfill_Memory.cs b/src/Split/net481/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/net481/Polyfill_Memory.cs +++ b/src/Split/net481/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net481/Polyfill_String.cs b/src/Split/net481/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/net481/Polyfill_String.cs +++ b/src/Split/net481/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net5.0/CharPolyfill.cs b/src/Split/net5.0/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/net5.0/CharPolyfill.cs +++ b/src/Split/net5.0/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net5.0/Polyfill_Char.cs b/src/Split/net5.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net5.0/Polyfill_Char.cs +++ b/src/Split/net5.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net5.0/Polyfill_Memory.cs b/src/Split/net5.0/Polyfill_Memory.cs index 236ca0574..9c0f5135b 100644 --- a/src/Split/net5.0/Polyfill_Memory.cs +++ b/src/Split/net5.0/Polyfill_Memory.cs @@ -14,5 +14,42 @@ static partial class Polyfill /// Returns an enumeration of lines over the provided span. /// public static SpanLineEnumerator EnumerateLines(this Span target) => new(target); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/net5.0/Polyfill_Rune.cs b/src/Split/net5.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net5.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net5.0/Polyfill_String.cs b/src/Split/net5.0/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/net5.0/Polyfill_String.cs +++ b/src/Split/net5.0/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net5.0/RunePolyfill.cs b/src/Split/net5.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net5.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/net6.0/CharPolyfill.cs b/src/Split/net6.0/CharPolyfill.cs index 6499f4254..5b4ae0cb7 100644 --- a/src/Split/net6.0/CharPolyfill.cs +++ b/src/Split/net6.0/CharPolyfill.cs @@ -50,5 +50,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net6.0/Polyfill_Char.cs b/src/Split/net6.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net6.0/Polyfill_Char.cs +++ b/src/Split/net6.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net6.0/Polyfill_Memory.cs b/src/Split/net6.0/Polyfill_Memory.cs new file mode 100644 index 000000000..889e702e1 --- /dev/null +++ b/src/Split/net6.0/Polyfill_Memory.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable +#if FeatureMemory +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } +} +#endif diff --git a/src/Split/net6.0/Polyfill_Rune.cs b/src/Split/net6.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net6.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net6.0/Polyfill_String.cs b/src/Split/net6.0/Polyfill_String.cs index 99ec08a0c..7de4b00dd 100644 --- a/src/Split/net6.0/Polyfill_String.cs +++ b/src/Split/net6.0/Polyfill_String.cs @@ -49,4 +49,24 @@ public static int LastIndexOf(this string target, char value, int startIndex, St /// public static int LastIndexOf(this string target, char value, int startIndex, int count, StringComparison comparisonType) => target.LastIndexOf(value.ToString(), startIndex, count, comparisonType); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net6.0/RunePolyfill.cs b/src/Split/net6.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net6.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/net7.0/CharPolyfill.cs b/src/Split/net7.0/CharPolyfill.cs index babc589e0..015be948f 100644 --- a/src/Split/net7.0/CharPolyfill.cs +++ b/src/Split/net7.0/CharPolyfill.cs @@ -10,5 +10,67 @@ static partial class Polyfill /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/net7.0/Polyfill_Char.cs b/src/Split/net7.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net7.0/Polyfill_Char.cs +++ b/src/Split/net7.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net7.0/Polyfill_Memory.cs b/src/Split/net7.0/Polyfill_Memory.cs new file mode 100644 index 000000000..889e702e1 --- /dev/null +++ b/src/Split/net7.0/Polyfill_Memory.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable +#if FeatureMemory +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } +} +#endif diff --git a/src/Split/net7.0/Polyfill_Rune.cs b/src/Split/net7.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net7.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net7.0/Polyfill_String.cs b/src/Split/net7.0/Polyfill_String.cs index 99ec08a0c..7de4b00dd 100644 --- a/src/Split/net7.0/Polyfill_String.cs +++ b/src/Split/net7.0/Polyfill_String.cs @@ -49,4 +49,24 @@ public static int LastIndexOf(this string target, char value, int startIndex, St /// public static int LastIndexOf(this string target, char value, int startIndex, int count, StringComparison comparisonType) => target.LastIndexOf(value.ToString(), startIndex, count, comparisonType); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net7.0/RunePolyfill.cs b/src/Split/net7.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net7.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/net8.0/CharPolyfill.cs b/src/Split/net8.0/CharPolyfill.cs new file mode 100644 index 000000000..a4d449304 --- /dev/null +++ b/src/Split/net8.0/CharPolyfill.cs @@ -0,0 +1,71 @@ +// +#pragma warning disable +namespace Polyfills; +static partial class Polyfill +{ + extension(char) + { + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; + } +} diff --git a/src/Split/net8.0/Polyfill_Char.cs b/src/Split/net8.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net8.0/Polyfill_Char.cs +++ b/src/Split/net8.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net8.0/Polyfill_Memory.cs b/src/Split/net8.0/Polyfill_Memory.cs new file mode 100644 index 000000000..889e702e1 --- /dev/null +++ b/src/Split/net8.0/Polyfill_Memory.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable +#if FeatureMemory +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } +} +#endif diff --git a/src/Split/net8.0/Polyfill_Rune.cs b/src/Split/net8.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net8.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net8.0/Polyfill_String.cs b/src/Split/net8.0/Polyfill_String.cs index 99ec08a0c..7de4b00dd 100644 --- a/src/Split/net8.0/Polyfill_String.cs +++ b/src/Split/net8.0/Polyfill_String.cs @@ -49,4 +49,24 @@ public static int LastIndexOf(this string target, char value, int startIndex, St /// public static int LastIndexOf(this string target, char value, int startIndex, int count, StringComparison comparisonType) => target.LastIndexOf(value.ToString(), startIndex, count, comparisonType); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net8.0/RunePolyfill.cs b/src/Split/net8.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net8.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/net9.0/CharPolyfill.cs b/src/Split/net9.0/CharPolyfill.cs new file mode 100644 index 000000000..a4d449304 --- /dev/null +++ b/src/Split/net9.0/CharPolyfill.cs @@ -0,0 +1,71 @@ +// +#pragma warning disable +namespace Polyfills; +static partial class Polyfill +{ + extension(char) + { + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; + } +} diff --git a/src/Split/net9.0/Polyfill_Char.cs b/src/Split/net9.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/net9.0/Polyfill_Char.cs +++ b/src/Split/net9.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/net9.0/Polyfill_Memory.cs b/src/Split/net9.0/Polyfill_Memory.cs new file mode 100644 index 000000000..889e702e1 --- /dev/null +++ b/src/Split/net9.0/Polyfill_Memory.cs @@ -0,0 +1,47 @@ +// +#pragma warning disable +#if FeatureMemory +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } +} +#endif diff --git a/src/Split/net9.0/Polyfill_Rune.cs b/src/Split/net9.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/net9.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/net9.0/Polyfill_String.cs b/src/Split/net9.0/Polyfill_String.cs index 99ec08a0c..7de4b00dd 100644 --- a/src/Split/net9.0/Polyfill_String.cs +++ b/src/Split/net9.0/Polyfill_String.cs @@ -49,4 +49,24 @@ public static int LastIndexOf(this string target, char value, int startIndex, St /// public static int LastIndexOf(this string target, char value, int startIndex, int count, StringComparison comparisonType) => target.LastIndexOf(value.ToString(), startIndex, count, comparisonType); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/net9.0/RunePolyfill.cs b/src/Split/net9.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/net9.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/netcoreapp2.0/CharPolyfill.cs b/src/Split/netcoreapp2.0/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netcoreapp2.0/CharPolyfill.cs +++ b/src/Split/netcoreapp2.0/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netcoreapp2.0/Polyfill_Char.cs b/src/Split/netcoreapp2.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Char.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netcoreapp2.0/Polyfill_Memory.cs b/src/Split/netcoreapp2.0/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/netcoreapp2.0/Polyfill_Memory.cs +++ b/src/Split/netcoreapp2.0/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netcoreapp2.0/Polyfill_String.cs b/src/Split/netcoreapp2.0/Polyfill_String.cs index 653d44cf4..5d0809016 100644 --- a/src/Split/netcoreapp2.0/Polyfill_String.cs +++ b/src/Split/netcoreapp2.0/Polyfill_String.cs @@ -118,4 +118,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netcoreapp2.1/CharPolyfill.cs b/src/Split/netcoreapp2.1/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netcoreapp2.1/CharPolyfill.cs +++ b/src/Split/netcoreapp2.1/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netcoreapp2.1/Polyfill_Char.cs b/src/Split/netcoreapp2.1/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netcoreapp2.1/Polyfill_Char.cs +++ b/src/Split/netcoreapp2.1/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netcoreapp2.1/Polyfill_Memory.cs b/src/Split/netcoreapp2.1/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/netcoreapp2.1/Polyfill_Memory.cs +++ b/src/Split/netcoreapp2.1/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netcoreapp2.1/Polyfill_String.cs b/src/Split/netcoreapp2.1/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/netcoreapp2.1/Polyfill_String.cs +++ b/src/Split/netcoreapp2.1/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netcoreapp2.2/CharPolyfill.cs b/src/Split/netcoreapp2.2/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netcoreapp2.2/CharPolyfill.cs +++ b/src/Split/netcoreapp2.2/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netcoreapp2.2/Polyfill_Char.cs b/src/Split/netcoreapp2.2/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netcoreapp2.2/Polyfill_Char.cs +++ b/src/Split/netcoreapp2.2/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netcoreapp2.2/Polyfill_Memory.cs b/src/Split/netcoreapp2.2/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/netcoreapp2.2/Polyfill_Memory.cs +++ b/src/Split/netcoreapp2.2/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netcoreapp2.2/Polyfill_String.cs b/src/Split/netcoreapp2.2/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/netcoreapp2.2/Polyfill_String.cs +++ b/src/Split/netcoreapp2.2/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netcoreapp3.0/CharPolyfill.cs b/src/Split/netcoreapp3.0/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netcoreapp3.0/CharPolyfill.cs +++ b/src/Split/netcoreapp3.0/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netcoreapp3.0/Polyfill_Char.cs b/src/Split/netcoreapp3.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netcoreapp3.0/Polyfill_Char.cs +++ b/src/Split/netcoreapp3.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netcoreapp3.0/Polyfill_Memory.cs b/src/Split/netcoreapp3.0/Polyfill_Memory.cs index 236ca0574..9c0f5135b 100644 --- a/src/Split/netcoreapp3.0/Polyfill_Memory.cs +++ b/src/Split/netcoreapp3.0/Polyfill_Memory.cs @@ -14,5 +14,42 @@ static partial class Polyfill /// Returns an enumeration of lines over the provided span. /// public static SpanLineEnumerator EnumerateLines(this Span target) => new(target); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netcoreapp3.0/Polyfill_Rune.cs b/src/Split/netcoreapp3.0/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/netcoreapp3.0/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/netcoreapp3.0/Polyfill_String.cs b/src/Split/netcoreapp3.0/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/netcoreapp3.0/Polyfill_String.cs +++ b/src/Split/netcoreapp3.0/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netcoreapp3.0/RunePolyfill.cs b/src/Split/netcoreapp3.0/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/netcoreapp3.0/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/netcoreapp3.1/CharPolyfill.cs b/src/Split/netcoreapp3.1/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netcoreapp3.1/CharPolyfill.cs +++ b/src/Split/netcoreapp3.1/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netcoreapp3.1/Polyfill_Char.cs b/src/Split/netcoreapp3.1/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netcoreapp3.1/Polyfill_Char.cs +++ b/src/Split/netcoreapp3.1/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netcoreapp3.1/Polyfill_Memory.cs b/src/Split/netcoreapp3.1/Polyfill_Memory.cs index 236ca0574..9c0f5135b 100644 --- a/src/Split/netcoreapp3.1/Polyfill_Memory.cs +++ b/src/Split/netcoreapp3.1/Polyfill_Memory.cs @@ -14,5 +14,42 @@ static partial class Polyfill /// Returns an enumeration of lines over the provided span. /// public static SpanLineEnumerator EnumerateLines(this Span target) => new(target); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netcoreapp3.1/Polyfill_Rune.cs b/src/Split/netcoreapp3.1/Polyfill_Rune.cs new file mode 100644 index 000000000..df61638ce --- /dev/null +++ b/src/Split/netcoreapp3.1/Polyfill_Rune.cs @@ -0,0 +1,18 @@ +// +#pragma warning disable +namespace Polyfills; +using System; +using System.Text; +static partial class Polyfill +{ + /// + /// Determines whether the specified Unicode scalar value is equal to the current one, using the specified comparison type. + /// + public static bool Equals(this Rune target, Rune other, StringComparison comparisonType) => + comparisonType switch + { + StringComparison.Ordinal => target == other, + StringComparison.OrdinalIgnoreCase => ToOrdinalCase(target, toUpper: true) == ToOrdinalCase(other, toUpper: true), + _ => target.ToString().Equals(other.ToString(), comparisonType) + }; +} diff --git a/src/Split/netcoreapp3.1/Polyfill_String.cs b/src/Split/netcoreapp3.1/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/netcoreapp3.1/Polyfill_String.cs +++ b/src/Split/netcoreapp3.1/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netcoreapp3.1/RunePolyfill.cs b/src/Split/netcoreapp3.1/RunePolyfill.cs new file mode 100644 index 000000000..8d182980d --- /dev/null +++ b/src/Split/netcoreapp3.1/RunePolyfill.cs @@ -0,0 +1,30 @@ +// +#pragma warning disable +namespace Polyfills; +using System.Text; +static partial class Polyfill +{ + extension(Rune) + { + /// + /// Returns a copy of the Unicode scalar value converted to lowercase using ordinal casing rules. + /// + public static Rune ToLowerOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: false); + /// + /// Returns a copy of the Unicode scalar value converted to uppercase using ordinal casing rules. + /// + public static Rune ToUpperOrdinal(Rune value) => + ToOrdinalCase(value, toUpper: true); + } + static Rune ToOrdinalCase(Rune value, bool toUpper) + { + if (value.IsBmp) + { + var current = (char) value.Value; + return new(toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current)); + } + var cased = ToOrdinalCaseSurrogatePair(value.ToString(), toUpper); + return new(char.ConvertToUtf32(cased[0], cased[1])); + } +} diff --git a/src/Split/netstandard2.0/CharPolyfill.cs b/src/Split/netstandard2.0/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netstandard2.0/CharPolyfill.cs +++ b/src/Split/netstandard2.0/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netstandard2.0/Polyfill_Char.cs b/src/Split/netstandard2.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netstandard2.0/Polyfill_Char.cs +++ b/src/Split/netstandard2.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netstandard2.0/Polyfill_Memory.cs b/src/Split/netstandard2.0/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/netstandard2.0/Polyfill_Memory.cs +++ b/src/Split/netstandard2.0/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netstandard2.0/Polyfill_String.cs b/src/Split/netstandard2.0/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/netstandard2.0/Polyfill_String.cs +++ b/src/Split/netstandard2.0/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/netstandard2.1/CharPolyfill.cs b/src/Split/netstandard2.1/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/netstandard2.1/CharPolyfill.cs +++ b/src/Split/netstandard2.1/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/netstandard2.1/Polyfill_Char.cs b/src/Split/netstandard2.1/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/netstandard2.1/Polyfill_Char.cs +++ b/src/Split/netstandard2.1/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/netstandard2.1/Polyfill_Memory.cs b/src/Split/netstandard2.1/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/netstandard2.1/Polyfill_Memory.cs +++ b/src/Split/netstandard2.1/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/netstandard2.1/Polyfill_String.cs b/src/Split/netstandard2.1/Polyfill_String.cs index bb7f8cfd4..40224a84d 100644 --- a/src/Split/netstandard2.1/Polyfill_String.cs +++ b/src/Split/netstandard2.1/Polyfill_String.cs @@ -113,4 +113,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Split/uap10.0/CharPolyfill.cs b/src/Split/uap10.0/CharPolyfill.cs index cd890ba89..6246eafce 100644 --- a/src/Split/uap10.0/CharPolyfill.cs +++ b/src/Split/uap10.0/CharPolyfill.cs @@ -55,5 +55,67 @@ public static bool IsAsciiLetterLower(char c) => /// public static bool IsBetween(char c, char minInclusive, char maxInclusive) => (uint)(c - minInclusive) <= (uint)(maxInclusive - minInclusive); + /// + /// Converts the value of a character to its lowercase equivalent using ordinal casing rules. + /// + public static char ToLowerOrdinal(char c) => + ToLowerOrdinalChar(c); + /// + /// Converts the value of a character to its uppercase equivalent using ordinal casing rules. + /// + public static char ToUpperOrdinal(char c) => + ToUpperOrdinalChar(c); + } + internal static char ToUpperOrdinalChar(char c) => + c == 'ſ' ? c : char.ToUpperInvariant(c); + internal static char ToLowerOrdinalChar(char c) + { + var lower = char.ToLowerInvariant(c); + if (lower == c) + { + return c; + } + return ToUpperOrdinalChar(lower) == ToUpperOrdinalChar(c) ? lower : c; + } + /// + /// Applies ordinal casing to in place, treating surrogate pairs as a single scalar. + /// + internal static void ToOrdinalCase(char[] chars, bool toUpper) + { + for (var index = 0; index < chars.Length; index++) + { + var current = chars[index]; + if (char.IsHighSurrogate(current) && + index + 1 < chars.Length && + char.IsLowSurrogate(chars[index + 1])) + { + var pair = new string(chars, index, 2); + var cased = ToOrdinalCaseSurrogatePair(pair, toUpper); + chars[index] = cased[0]; + chars[index + 1] = cased[1]; + index++; + continue; + } + chars[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + } + internal static string ToOrdinalCaseSurrogatePair(string pair, bool toUpper) + { + if (toUpper) + { + return ToUpperOrdinalSurrogatePair(pair); + } + var lower = pair.ToLowerInvariant(); + if (lower.Length != 2 || + lower == pair) + { + return pair; + } + return ToUpperOrdinalSurrogatePair(lower) == ToUpperOrdinalSurrogatePair(pair) ? lower : pair; + } + static string ToUpperOrdinalSurrogatePair(string pair) + { + var upper = pair.ToUpperInvariant(); + return upper.Length == 2 ? upper : pair; } } diff --git a/src/Split/uap10.0/Polyfill_Char.cs b/src/Split/uap10.0/Polyfill_Char.cs index ff49813b9..ec82141d8 100644 --- a/src/Split/uap10.0/Polyfill_Char.cs +++ b/src/Split/uap10.0/Polyfill_Char.cs @@ -11,7 +11,7 @@ public static bool Equals(this char target, char other, StringComparison compari comparisonType switch { StringComparison.Ordinal => target == other, - StringComparison.OrdinalIgnoreCase => char.ToUpperInvariant(target) == char.ToUpperInvariant(other), + StringComparison.OrdinalIgnoreCase => ToUpperOrdinalChar(target) == ToUpperOrdinalChar(other), _ => target.ToString().Equals(other.ToString(), comparisonType) }; } diff --git a/src/Split/uap10.0/Polyfill_Memory.cs b/src/Split/uap10.0/Polyfill_Memory.cs index 8a1d62dfe..734b6da47 100644 --- a/src/Split/uap10.0/Polyfill_Memory.cs +++ b/src/Split/uap10.0/Polyfill_Memory.cs @@ -60,5 +60,42 @@ public static bool SequenceEqual( this Span target, string other) => target.SequenceEqual(other.AsSpan()); + /// + /// Copies the source span to the destination span, converting it to lowercase using ordinal casing rules. + /// + public static int ToLowerOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: false); + /// + /// Copies the source span to the destination span, converting it to uppercase using ordinal casing rules. + /// + public static int ToUpperOrdinal(this ReadOnlySpan source, Span destination) => + ToOrdinalCase(source, destination, toUpper: true); + static int ToOrdinalCase(ReadOnlySpan source, Span destination, bool toUpper) + { + if (source.Overlaps(destination)) + { + throw new InvalidOperationException("The source and destination buffers overlap."); + } + if (destination.Length < source.Length) + { + return -1; + } + for (var index = 0; index < source.Length; index++) + { + var current = source[index]; + if (char.IsHighSurrogate(current) && + index + 1 < source.Length && + char.IsLowSurrogate(source[index + 1])) + { + var cased = ToOrdinalCaseSurrogatePair(source.Slice(index, 2).ToString(), toUpper); + destination[index] = cased[0]; + destination[index + 1] = cased[1]; + index++; + continue; + } + destination[index] = toUpper ? ToUpperOrdinalChar(current) : ToLowerOrdinalChar(current); + } + return source.Length; + } } #endif diff --git a/src/Split/uap10.0/Polyfill_String.cs b/src/Split/uap10.0/Polyfill_String.cs index 254f4be0a..a4c04f9ca 100644 --- a/src/Split/uap10.0/Polyfill_String.cs +++ b/src/Split/uap10.0/Polyfill_String.cs @@ -194,4 +194,24 @@ static int IndexOfNewlineChar(string text, int startIndex, out int stride) /// public static string ReplaceLineEndings(this string target) => ReplaceLineEndings(target, Environment.NewLine); + /// + /// Returns a copy of this string converted to lowercase using ordinal casing rules. + /// + public static string ToLowerOrdinal(this string target) => + ToOrdinalCase(target, toUpper: false); + /// + /// Returns a copy of this string converted to uppercase using ordinal casing rules. + /// + public static string ToUpperOrdinal(this string target) => + ToOrdinalCase(target, toUpper: true); + static string ToOrdinalCase(string target, bool toUpper) + { + if (target.Length == 0) + { + return target; + } + var chars = target.ToCharArray(); + ToOrdinalCase(chars, toUpper); + return new(chars); + } } diff --git a/src/Tests/PolyfillTests_OrdinalCasing.cs b/src/Tests/PolyfillTests_OrdinalCasing.cs new file mode 100644 index 000000000..df1db39dc --- /dev/null +++ b/src/Tests/PolyfillTests_OrdinalCasing.cs @@ -0,0 +1,160 @@ +using System.Text; + +partial class PolyfillTests +{ + // ordinal casing deliberately leaves these uncased, where invariant casing maps them, + // so that it stays consistent with OrdinalIgnoreCase + const char kelvin = 'K'; + const char ohm = 'Ω'; + const char angstrom = 'Å'; + const char capitalSharpS = 'ẞ'; + const char capitalThetaSymbol = 'ϴ'; + const char longS = 'ſ'; + + // U+10400 DESERET CAPITAL LETTER LONG I, lowercases to U+10428 + const string deseretCapital = "\U00010400"; + const string deseretSmall = "\U00010428"; + + [Test] + public async Task Char_ToLowerOrdinal() + { + await Assert.That(char.ToLowerOrdinal('A')).IsEqualTo('a'); + await Assert.That(char.ToLowerOrdinal('a')).IsEqualTo('a'); + await Assert.That(char.ToLowerOrdinal('1')).IsEqualTo('1'); + await Assert.That(char.ToLowerOrdinal('Å')).IsEqualTo('å'); + } + + [Test] + [Arguments(kelvin)] + [Arguments(ohm)] + [Arguments(angstrom)] + [Arguments(capitalSharpS)] + [Arguments(capitalThetaSymbol)] + public async Task Char_ToLowerOrdinal_PreservesOrdinalClass(char value) => + await Assert.That(char.ToLowerOrdinal(value)).IsEqualTo(value); + + [Test] + public async Task Char_ToUpperOrdinal() + { + await Assert.That(char.ToUpperOrdinal('a')).IsEqualTo('A'); + await Assert.That(char.ToUpperOrdinal('A')).IsEqualTo('A'); + await Assert.That(char.ToUpperOrdinal('1')).IsEqualTo('1'); + await Assert.That(char.ToUpperOrdinal('å')).IsEqualTo('Å'); + // invariant casing would map the long s to S + await Assert.That(char.ToUpperOrdinal(longS)).IsEqualTo(longS); + } + + [Test] + public async Task Char_Equals_OrdinalIgnoreCase_UsesOrdinalCasing() + { + await Assert.That('A'.Equals('a', StringComparison.OrdinalIgnoreCase)).IsTrue(); + await Assert.That('A'.Equals('a', StringComparison.Ordinal)).IsFalse(); + await Assert.That('A'.Equals('a', StringComparison.InvariantCultureIgnoreCase)).IsTrue(); + // ordinal casing keeps these out of each other's casing class, unlike invariant casing + await Assert.That(kelvin.Equals('k', StringComparison.OrdinalIgnoreCase)).IsFalse(); + await Assert.That(longS.Equals('s', StringComparison.OrdinalIgnoreCase)).IsFalse(); + } + + [Test] + public async Task String_ToLowerOrdinal() + { + await Assert.That("ABC".ToLowerOrdinal()).IsEqualTo("abc"); + await Assert.That(string.Empty.ToLowerOrdinal()).IsEqualTo(string.Empty); + await Assert.That("Straße".ToLowerOrdinal()).IsEqualTo("straße"); + await Assert.That($"a{kelvin}b".ToLowerOrdinal()).IsEqualTo($"a{kelvin}b"); + // casing applies per scalar, so surrogate pairs are mapped + await Assert.That(deseretCapital.ToLowerOrdinal()).IsEqualTo(deseretSmall); + } + + [Test] + public async Task String_ToUpperOrdinal() + { + await Assert.That("abc".ToUpperOrdinal()).IsEqualTo("ABC"); + await Assert.That(string.Empty.ToUpperOrdinal()).IsEqualTo(string.Empty); + await Assert.That($"a{longS}b".ToUpperOrdinal()).IsEqualTo($"A{longS}B"); + await Assert.That(deseretSmall.ToUpperOrdinal()).IsEqualTo(deseretCapital); + } + +#if FeatureMemory + [Test] + public async Task Span_ToLowerOrdinal() + { + var destination = new char[3]; + var written = "ABC".AsSpan().ToLowerOrdinal(destination); + var result = new string(destination); + await Assert.That(written).IsEqualTo(3); + await Assert.That(result).IsEqualTo("abc"); + } + + [Test] + public async Task Span_ToUpperOrdinal() + { + var destination = new char[3]; + var written = "abc".AsSpan().ToUpperOrdinal(destination); + var result = new string(destination); + await Assert.That(written).IsEqualTo(3); + await Assert.That(result).IsEqualTo("ABC"); + } + + [Test] + public async Task Span_ToLowerOrdinal_SurrogatePair() + { + var destination = new char[2]; + var written = deseretCapital.AsSpan().ToLowerOrdinal(destination); + var result = new string(destination); + await Assert.That(written).IsEqualTo(2); + await Assert.That(result).IsEqualTo(deseretSmall); + } + + [Test] + public async Task Span_ToLowerOrdinal_DestinationTooSmall() + { + var destination = new char[2]; + var written = "ABC".AsSpan().ToLowerOrdinal(destination); + await Assert.That(written).IsEqualTo(-1); + } + + [Test] + public async Task Span_ToLowerOrdinal_Empty() + { + var written = ReadOnlySpan.Empty.ToLowerOrdinal([]); + await Assert.That(written).IsEqualTo(0); + } + + [Test] + public async Task Span_ToLowerOrdinal_Overlapping() + { + var buffer = "ABCD".ToCharArray(); + await Assert.That(() => { buffer.AsSpan().ToLowerOrdinal(buffer.AsSpan()); }) + .Throws(); + } +#endif + +#if NETCOREAPP3_0_OR_GREATER + [Test] + public async Task Rune_ToLowerOrdinal() + { + await Assert.That(Rune.ToLowerOrdinal(new('A'))).IsEqualTo(new Rune('a')); + await Assert.That(Rune.ToLowerOrdinal(new(kelvin))).IsEqualTo(new Rune(kelvin)); + await Assert.That(Rune.ToLowerOrdinal(Rune.GetRuneAt(deseretCapital, 0))).IsEqualTo(Rune.GetRuneAt(deseretSmall, 0)); + } + + [Test] + public async Task Rune_ToUpperOrdinal() + { + await Assert.That(Rune.ToUpperOrdinal(new('a'))).IsEqualTo(new Rune('A')); + await Assert.That(Rune.ToUpperOrdinal(new(longS))).IsEqualTo(new Rune(longS)); + await Assert.That(Rune.ToUpperOrdinal(Rune.GetRuneAt(deseretSmall, 0))).IsEqualTo(Rune.GetRuneAt(deseretCapital, 0)); + } + + [Test] + public async Task Rune_Equals_StringComparison() + { + await Assert.That(new Rune('A').Equals(new('a'), StringComparison.OrdinalIgnoreCase)).IsTrue(); + await Assert.That(new Rune('A').Equals(new('a'), StringComparison.Ordinal)).IsFalse(); + await Assert.That(new Rune('A').Equals(new('a'), StringComparison.InvariantCultureIgnoreCase)).IsTrue(); + await Assert.That(new Rune(kelvin).Equals(new('k'), StringComparison.OrdinalIgnoreCase)).IsFalse(); + await Assert.That(new Rune(longS).Equals(new('s'), StringComparison.OrdinalIgnoreCase)).IsFalse(); + } +#endif +}