diff --git a/apiCount.include.md b/apiCount.include.md index 05c596a5..3aa32697 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 7ba88a03..52855e7a 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 e6082d74..644fb47e 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 c970e656..2406c98c 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 f26c62db..dca6aca4 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 dbdebc19..71407c65 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 c16c5a31..12d579f1 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 23cdb24d..51280d15 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 00000000..c6e83bd5 --- /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 0e16714a..2a68aa76 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 00000000..b50b6383 --- /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 00000000..a4d44930 --- /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 ff49813b..ec82141d 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 00000000..889e702e --- /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 00000000..df61638c --- /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 99ec08a0..7de4b00d 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 00000000..8d182980 --- /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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 236ca057..9c0f5135 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 00000000..df61638c --- /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 bb7f8cfd..40224a84 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 00000000..8d182980 --- /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 6499f425..5b4ae0cb 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 ff49813b..ec82141d 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 00000000..889e702e --- /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 00000000..df61638c --- /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 99ec08a0..7de4b00d 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 00000000..8d182980 --- /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 babc589e..015be948 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 ff49813b..ec82141d 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 00000000..889e702e --- /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 00000000..df61638c --- /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 99ec08a0..7de4b00d 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 00000000..8d182980 --- /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 00000000..a4d44930 --- /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 ff49813b..ec82141d 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 00000000..889e702e --- /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 00000000..df61638c --- /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 99ec08a0..7de4b00d 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 00000000..8d182980 --- /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 00000000..a4d44930 --- /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 ff49813b..ec82141d 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 00000000..889e702e --- /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 00000000..df61638c --- /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 99ec08a0..7de4b00d 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 00000000..8d182980 --- /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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 653d44cf..5d080901 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 bb7f8cfd..40224a84 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 bb7f8cfd..40224a84 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 236ca057..9c0f5135 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 00000000..df61638c --- /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 bb7f8cfd..40224a84 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 00000000..8d182980 --- /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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 236ca057..9c0f5135 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 00000000..df61638c --- /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 bb7f8cfd..40224a84 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 00000000..8d182980 --- /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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 bb7f8cfd..40224a84 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 cd890ba8..6246eafc 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 ff49813b..ec82141d 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 8a1d62df..734b6da4 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 254f4be0..a4c04f9c 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 00000000..df1db39d --- /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 +}