Skip to content

Add ToUpperOrdinal and ToLowerOrdinal casing APIs#130140

Open
tarekgh wants to merge 9 commits into
dotnet:mainfrom
tarekgh:ordinal-casing-apis
Open

Add ToUpperOrdinal and ToLowerOrdinal casing APIs#130140
tarekgh wants to merge 9 commits into
dotnet:mainfrom
tarekgh:ordinal-casing-apis

Conversation

@tarekgh

@tarekgh tarekgh commented Jul 2, 2026

Copy link
Copy Markdown
Member

Implements the ordinal casing APIs approved in #90999.

APIs

  • char.ToUpperOrdinal(char) / char.ToLowerOrdinal(char)
  • Rune.ToUpperOrdinal(Rune) / Rune.ToLowerOrdinal(Rune)
  • string.ToUpperOrdinal() / string.ToLowerOrdinal()
  • MemoryExtensions.ToUpperOrdinal(ReadOnlySpan<char>, Span<char>) / ToLowerOrdinal(ReadOnlySpan<char>, Span<char>)

Notes

  • Ordinal casing uses the same simple, one-to-one mapping as OrdinalIgnoreCase comparisons. Two strings are OrdinalIgnoreCase-equal iff their ToUpperOrdinal results are ordinally equal.
  • Reuses the existing ordinal upper-casing tables and adds a mirrored lower-casing path. Six characters (U+0130, U+03F4, U+1E9E, U+2126, U+212A, U+212B) stay unchanged under lower casing to remain consistent with OrdinalIgnoreCase.
  • string.ToUpperOrdinal() / string.ToLowerOrdinal() return the same instance when an all-ASCII string is already in the requested case, avoiding an allocation.

Tests under System.Globalization.Tests validate the mappings against OrdinalIgnoreCase across the entire BMP.

Implement ordinal (simple, one-to-one) casing on Char, Rune, String, and MemoryExtensions, matching the mappings used by OrdinalIgnoreCase comparisons. String.ToUpperOrdinal/ToLowerOrdinal return the same instance when an all-ASCII string is already in the requested case.
@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-globalization
See info in area-owners.md if you want to be subscribed.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds new ordinal casing APIs across char, Rune, string, and span-based MemoryExtensions, implemented to match the StringComparison.OrdinalIgnoreCase folding behavior (one-to-one mapping, length-preserving), including a new ordinal lower-casing table path alongside the existing upper-casing tables.

Changes:

  • Introduces ToUpperOrdinal / ToLowerOrdinal APIs on char, Rune, string, and ReadOnlySpan<char> -> Span<char> transforms.
  • Implements ICU-backed ordinal lower-casing table initialization (native + managed), mirroring the existing upper-casing infrastructure and special-case handling.
  • Adds comprehensive System.Globalization.Tests coverage validating BMP-wide behavior and overload consistency.

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/native/libs/System.Globalization.Native/System.Globalization.Native.def Exports the new native entrypoint for ordinal lower-casing page initialization.
src/native/libs/System.Globalization.Native/pal_common.c Implements GlobalizationNative_InitOrdinalLowerCasingPage using ICU simple lower casing + special cases.
src/native/libs/System.Globalization.Native/pal_casing.h Declares the new native PAL export for ordinal lower-casing page initialization.
src/native/libs/System.Globalization.Native/entrypoints.c Registers the new entrypoint for DllImport resolution.
src/libraries/Common/src/Interop/Interop.Casing.cs Adds managed LibraryImport for GlobalizationNative_InitOrdinalLowerCasingPage.
src/libraries/System.Private.CoreLib/src/System/Globalization/OrdinalCasing.Icu.cs Adds a mirrored lower-casing table + lazy per-page initialization and span-based lower casing.
src/libraries/System.Private.CoreLib/src/System/Globalization/TextInfo.cs Adds TextInfo.ToLowerOrdinal(char) routing based on globalization mode (Invariant / NLS / ICU).
src/libraries/System.Private.CoreLib/src/System/Globalization/Ordinal.cs Adds span-based Ordinal.ToLowerOrdinal entrypoint consistent with existing ToUpperOrdinal.
src/libraries/System.Private.CoreLib/src/System/MemoryExtensions.Globalization.cs Adds public span extension methods ToUpperOrdinal / ToLowerOrdinal.
src/libraries/System.Private.CoreLib/src/System/String.Manipulation.cs Adds string.ToUpperOrdinal() / string.ToLowerOrdinal() with ASCII fast-path returning same instance when unchanged.
src/libraries/System.Private.CoreLib/src/System/Char.cs Adds public char.ToUpperOrdinal(char) / char.ToLowerOrdinal(char).
src/libraries/System.Private.CoreLib/src/System/Text/Rune.cs Adds public Rune.ToUpperOrdinal(Rune) / Rune.ToLowerOrdinal(Rune) implementations.
src/libraries/System.Runtime/ref/System.Runtime.cs Adds the new public APIs to the System.Runtime ref surface (char, string, Rune).
src/libraries/System.Memory/ref/System.Memory.cs Adds the new public APIs to the System.Memory ref surface (MemoryExtensions).
src/libraries/System.Runtime/tests/System.Globalization.Tests/System/Globalization/OrdinalCasingTests.cs New tests validating ordinal casing invariants and special-case behavior across the BMP and overloads.
src/libraries/System.Runtime/tests/System.Globalization.Tests/System.Globalization.Tests.csproj Includes the new test file in the test project.
src/coreclr/vm/wasm/wasi/callhelpers-pinvoke.cpp Adds the new native export for WASM/WASI callhelper pinvoke tables.
src/coreclr/vm/wasm/browser/callhelpers-pinvoke.cpp Adds the new native export for WASM/browser callhelper pinvoke tables.

Comment thread src/native/libs/System.Globalization.Native/entrypoints.c Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 2, 2026 21:23
@tarekgh tarekgh added this to the 11.0.0 milestone Jul 2, 2026
@tarekgh

tarekgh commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

CC @lewing to advise if we need to do anything more for WASM with this change?

CC @jkotas & @GrabYourPitchforks for awareness.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Tarek Mahmoud Sayed added 2 commits July 2, 2026 14:40
… modes

Invariant and NLS simple lowering could move characters such as the Kelvin, Ohm and Angstrom signs out of their ordinal upper-casing class, so ToLowerOrdinal stopped being consistent with OrdinalIgnoreCase in those modes. Keep the original character when its simple lower mapping would change its ordinal upper-casing form, and extend the invariant-mode test to cover ToLowerOrdinal across the BMP.
…ecutor

Guard the MICRO SIGN and GREEK SMALL FINAL SIGMA fold cases so they only run under ICU and invariant casing, since NLS does not treat them as OrdinalIgnoreCase-equal. Gate OrdinalIgnoreCase_Equivalence_HoldsInInvariantMode on RemoteExecutor.IsSupported so it is skipped on wasm and android instead of throwing PlatformNotSupportedException.
Copilot AI review requested due to automatic review settings July 6, 2026 19:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 2 comments.

Comment thread src/libraries/System.Private.CoreLib/src/System/Globalization/Ordinal.cs Outdated
Comment thread src/coreclr/vm/wasm/browser/callhelpers-pinvoke.cpp
Copilot AI review requested due to automatic review settings July 6, 2026 19:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Tarek Mahmoud Sayed added 2 commits July 6, 2026 13:46
Upper-case the source and lowered spans once each instead of calling
TextInfo.ToUpperOrdinal per non-ASCII character, which is expensive under NLS.
Copilot AI review requested due to automatic review settings July 6, 2026 21:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 1 comment.

Comment thread src/libraries/System.Private.CoreLib/src/System/Globalization/Ordinal.cs Outdated
Only a fixed set of five BMP scalars (Greek capital theta symbol, Latin
capital sharp S, Ohm, Kelvin and Angstrom signs) fall out of their ordinal
upper-casing class under simple invariant or NLS lowering. Scan the source
once with SearchValues and skip the pass entirely when none are present,
instead of upper-casing the whole span on every lowering.
for (int i = 0; i < source.Length; i++)
{
char c = source[i];
if (c <= '\u00FF') // optimize ASCII/Latin

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we call Ascii.ToLower first to quickly scan through any leading ASCII?

Comment on lines +2621 to +2668
int consumed;

// Fast path: scan the leading run of ASCII characters that is already in the
// requested case. If the entire string is ASCII and needs no change, return the
// same instance to avoid an allocation. This mirrors the behavior of
// TextInfo.ChangeCaseCommon used by ToUpper(Invariant)/ToLower.
fixed (char* pSource = this)
{
nuint currIdx = 0; // in chars

if (Length >= 2)
{
nuint lastIndexWhereCanReadTwoChars = (uint)Length - 2;
do
{
// Read 2 chars (one 32-bit integer) at a time.
uint tempValue = Unsafe.ReadUnaligned<uint>(pSource + currIdx);
if (!Utf16Utility.AllCharsInUInt32AreAscii(tempValue) ||
(toUpper
? Utf16Utility.UInt32ContainsAnyLowercaseAsciiChar(tempValue)
: Utf16Utility.UInt32ContainsAnyUppercaseAsciiChar(tempValue)))
{
goto MustChange;
}

currIdx += 2;
} while (currIdx <= lastIndexWhereCanReadTwoChars);
}

// If there's a single character left, check it now.
if ((Length & 1) != 0)
{
uint tempValue = pSource[currIdx];
if (tempValue > 0x7Fu ||
(toUpper
? ((tempValue - 'a') <= (uint)('z' - 'a'))
: ((tempValue - 'A') <= (uint)('Z' - 'A'))))
{
goto MustChange;
}
}

// The whole string is ASCII and already in the requested case.
return this;

MustChange:
consumed = (int)currIdx;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems like it could be written as something like this instead:

int skippedAscii = toUpper
    ? this.AsSpan().IndexOfAnyExcept(s_asciiExceptLower)
    : this.AsSpan().IndexOfAnyExcept(s_asciiExceptUpper);

if (skippedAscii < 0)
{
    return this;
}

private const string AsciiExceptLetters = "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\u000A\u000B\u000C\u000D\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F !\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`{|}~\u007F";
private static readonly SearchValues<char> s_asciiExceptLower = SearchValues.Create(AsciiExceptLetters + "ABCDEFGHIJKLMNOPQRSTUVWXYZ");
private static readonly SearchValues<char> s_asciiExceptUpper = SearchValues.Create(AsciiExceptLetters + "abcdefghijklmnopqrstuvwxyz");

And avoid the unsafe code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants