Add net11 ordinal casing APIs - #585
Merged
Merged
Conversation
Adds the ordinal casing family from net11: * char.ToLowerOrdinal(char) and char.ToUpperOrdinal(char) * string.ToLowerOrdinal() and string.ToUpperOrdinal() * ReadOnlySpan<char>.ToLowerOrdinal(Span<char>) and ToUpperOrdinal * Rune.ToLowerOrdinal(Rune), Rune.ToUpperOrdinal(Rune) and Rune.Equals(Rune, StringComparison) Ordinal casing is not invariant casing. It deliberately declines any mapping that would move a character out of its ordinal upper-casing class, so that it stays consistent with OrdinalIgnoreCase. The Kelvin, Ohm and Angstrom signs, the capital sharp s, the Greek capital theta symbol and the long s are all left uncased where invariant casing would map them. The implementation follows the algorithm the runtime itself uses when the ICU ordinal table is unavailable: invariant casing, with the lower case mapping discarded when it would change the character's ordinal upper case form. Verified against net11 over the whole BMP, where it now agrees on every code point. Casing is applied per scalar rather than per char, so surrogate pairs are mapped, matching net11. The Rune members are only available from netcoreapp3.0, since Rune does not exist earlier and is not part of netstandard2.1. Also fixes char.Equals(char, StringComparison), which used invariant casing for OrdinalIgnoreCase and so reported the long s as equal to s where net11 reports it as not equal.
This was referenced Sep 10, 2026
This was referenced Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds the ordinal casing family from net11:
char.ToLowerOrdinal(char)andchar.ToUpperOrdinal(char)string.ToLowerOrdinal()andstring.ToUpperOrdinal()ReadOnlySpan<char>.ToLowerOrdinal(Span<char>)andToUpperOrdinalRune.ToLowerOrdinal(Rune),Rune.ToUpperOrdinal(Rune)andRune.Equals(Rune, StringComparison)API count 1038 to 1047.
Ordinal casing is not invariant casing
Worth stating up front, because the obvious implementation is wrong. Ordinal casing declines any mapping that would move a character out of its ordinal upper-casing class, so that it stays consistent with
OrdinalIgnoreCase. Six BMP code points differ from invariant casing:kSThe implementation follows the algorithm the runtime itself uses when the ICU ordinal table is not available (
TextInfo.PreserveOrdinalLowerCasingClass): invariant casing, with the lower case mapping discarded when it would change the character's ordinal upper case form, plus the long s exception on the upper case side.Verified against net11 over the whole BMP: it agrees on every code point, for both directions.
Two further behaviours were pinned against the runtime rather than assumed:
"\U00010400".ToLowerOrdinal()returns U+10428.-1when the destination is too small, and throwInvalidOperationExceptionwhen the buffers overlap. The overlap check runs first.Divergence
The mapping is derived from the running framework's invariant casing table, so it follows that framework's Unicode version rather than the one net11 is built against. Scripts added in a newer Unicode version than the host will not be cased. Recorded as a
//Note:on each member.Also fixed
char.Equals(char, StringComparison)was already polyfilled and usedToUpperInvariantforOrdinalIgnoreCase, so it reported the long s as equal tos, where net11 reports them as not equal. Now routed through ordinal casing.Scope
Runedoes not exist before netcoreapp3.0, and is not part of netstandard2.1, so those three members are gated toNETCOREAPP3_0_OR_GREATER. The char, string and span members are available on every target framework.TextInfo.ToLower(Rune)andToUpper(Rune)are left out. They are culture aware casing rather than ordinal, so they do not belong with this set.Verification
Consumebuilds clean in Debug across all 22 target frameworks, which covers the non-FeatureMemoryand pre-Rune paths.PublicTests,EmbeddedTests,UnsafeTests,NoRefsTestsandNoExtrasTests.