Skip to content

Case-map İ and ı, which the invariant mapping leaves alone [patch] - #101

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/97-dotted-dotless-i
Sep 29, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/97-dotted-dotless-i

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #97

What changed

.NET's invariant casing deliberately leaves U+0130 İ and U+0131 ı unmapped. As a result, snake, kebab and camel output kept an uppercase İ, and MACRO and Pascal output kept a lowercase ı.

  • Case-mapping helpers. ToUpperInvariantFull and ToLowerInvariantFull apply the invariant mapping, then patch those two letters to their Unicode simple mappings (ı → I, İ → i). A culture TextInfo is not used, because its result depends on the installed ICU data. Every casing call goes through the helpers:

    • snake/macro conversion
    • the first-char helpers
    • the possessive-suffix uppercasing
    • IsLowercaseWithNoUppercase. Before this change ı counted as "no uppercase form", which also made "ı".IsAllCaps() return true.
  • Title case. TitleCaseKeepingTypographicApostrophes swaps İ/ı for I/i before the invariant TextInfo.ToTitleCase runs. It then reads each result back:

    • an I means the letter starts a word, so İ is kept and ı becomes I
    • an i means it does not, so İ becomes i and ı is kept

    This is what makes "ılık su".ToPascalCase() give IlıkSu. "İZMİR" normalizes to İzmir, the same as "İzmir" does.

Call Before After
"İzmir city".ToSnakeCase() İzmir_city izmir_city
"İzmir city".ToCamelCase() İzmirCity izmirCity
"İstanbul".ToLowercaseFirstChar() İstanbul istanbul
"kırmızı".ToMacroCase() KıRMıZı KIRMIZI
"ılık su".ToPascalCase() ılıkSu IlıkSu
"ılık su".ToUppercaseFirstChar() ılık su Ilık su

Out of scope

ToSnakeCase still lowercases the ToMacroCase output, so a lowercase ı round-trips to i in snake/kebab case ("kırmızı".ToSnakeCase() → kirmizi). The camel path has the same problem for a leading ı. That upper→lower round trip is #84, so these tests don't assert those outputs.

Testing

  • New tests: data rows for İ across all five converters, ı rows for Pascal and Macro, the first-char helpers and ToTitleCase, ToMacroCase idempotence on kırmızı / İzmir city / ılık su, and IsAllCaps on ı.
  • With the source change reverted, 6 tests fail. With it, all 94 pass on net10.0.
  • dotnet build CaseConverter.sln builds every target (net10.0/9.0/8.0, netstandard2.0/2.1) with 0 warnings.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AtF6Ynq3K6Km6HSRHaKuX8


Generated by Claude Code

.NET's invariant casing deliberately leaves U+0130 "İ" and U+0131 "ı"
unmapped, so snake, kebab and camel output kept an uppercase "İ" and
MACRO and Pascal output kept a lowercase "ı". Route the casing calls
through helpers that apply Unicode's simple mappings for those two
letters, and fix them up around TextInfo.ToTitleCase so a word can
start with "I" rather than "ı". A culture's TextInfo is not used,
because its result depends on the installed ICU data.

Fixes #97

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AtF6Ynq3K6Km6HSRHaKuX8
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit 591fbbe into main Sep 29, 2026
14 checks passed
@matt-edmondson
matt-edmondson deleted the fix/97-dotted-dotless-i branch September 29, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

İ and ı are never case-mapped: "İzmir city".ToSnakeCase() returns "İzmir_city", "kırmızı".ToMacroCase() returns "KıRMıZı"

2 participants