GH-51225: [C++] utf8_normalize: compose for NFC and NFKC - #51237
Conversation
utf8proc_decompose() only decomposes; call utf8proc_normalize_utf32() on the scratch buffer when the form asks for composition. Fix the json_composed test fixture, whose bytes were the decomposed form, and add composed/decomposed pairs to the pyarrow test.
|
|
|
|
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WQYE6hSNYZur5fRZC3mqGT
raulcd
left a comment
There was a problem hiding this comment.
This is not a critical fix, can you update the description.
|
|
|
Checked the diff. All three parts land, and the kernel change is in the right place — after the regrow retry and the It also matches the reference path exactly: The composed fixture is now One small suggestion. The pyarrow case includes Hangul, which is the more interesting input — Hangul composition in utf8proc is algorithmic rather than table-driven, so it exercises a different path. The C++ suite still only covers the Latin pair. A Hangul pair there too would be worth having, since the C++ kernel is what the other bindings sit on. Happy to look again once CI is green. |
|
Added the Hangul pair to the C++ test: U+1112 U+1161 U+11AB against U+D55C, both directions for all |
| &options); | ||
| } | ||
|
|
||
| // Hangul composes algorithmically in utf8proc, not through the composition table. |
There was a problem hiding this comment.
Is this comment useful? This is mentioning an implementation detail of utf8proc, which doesn't seem relevant for our purposes.
(I've checked that Python agreed with this behavior)
There was a problem hiding this comment.
Removed, the code point lines stay.
| // utf8proc_decompose() only decomposes; the canonical composition step for | ||
| // NFC and NFKC is done in-place by utf8proc_normalize_utf32(). |
There was a problem hiding this comment.
Is it still useful that we call utf8proc_decompose first? Or should we just decode to UTF32 codepoints ourselves?
There was a problem hiding this comment.
Yes, it is still needed. utf8proc_normalize_utf32() composes pairs (through the composition
table, Hangul by arithmetic) and applies the newline and control-character options; it does no
decomposition and no reordering of combining marks, both of which happen in
utf8proc_decompose(). NFC and NFKC are the full decomposition followed by canonical
composition. Skipping utf8proc_decompose() would leave singletons alone (U+212B ANGSTROM SIGN
normalizes to U+00C5, there is nothing to compose), would not reorder marks (U+00E1 U+0323 must
become U+1EA1 U+0301), and would drop all of NFKC's compatibility mappings, which happen in the
decompose call. utf8proc's own utf8proc_map() is the same sequence, utf8proc_decompose() then
utf8proc_reencode(), which calls utf8proc_normalize_utf32() before encoding; the kernel does
that into its scratch buffer instead of the malloc in utf8proc_map(). I added the U+212B
singleton to the test, since it is the case that only passes with both steps.
There was a problem hiding this comment.
Oh, that's interesting indeed. Thanks for the example.
>>> s = "\u212B"
>>> unicodedata.normalize('NFC', s) == s
False
>>> unicodedata.normalize('NFKC', s) == s
False
>>> unicodedata.normalize('NFD', s) == s
False
>>> unicodedata.normalize('NFKD', s) == s
False
pitrou
left a comment
There was a problem hiding this comment.
Thanks for this PR, and see already posted comments.
…he Hangul comment, add a singleton case
…ul and singleton test cases added on request Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HPt1VJ8C7JGeLy7bsoExZN
|
Nit: I've literal-quoted @Santoshkumarpuppala in the PR description to avoid spurious GH notifications when commits propagate (merges, etc.). |
|
|
pitrou
left a comment
There was a problem hiding this comment.
Thanks for the update and the explanations @singhpratech
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HPt1VJ8C7JGeLy7bsoExZN
…erged into 26.0.0 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HPt1VJ8C7JGeLy7bsoExZN
|
Thanks for the review and the merge. |
Rationale for this change
utf8_normalizewithform=NFCorNFKCreturned the decomposed forms (NFD, NFKD).Utf8NormalizeBasebuilds the right
utf8procoptions for each form but only callsutf8proc_decompose(), whichdecomposes regardless of
UTF8PROC_COMPOSE; the composition step lives inutf8proc_normalize_utf32(), which the kernel never called. See #51225.What changes are included in this PR?
utf8proc_decompose(), when the options includeUTF8PROC_COMPOSE, callutf8proc_normalize_utf32()on the scratch buffer. It composes in place and returns the new codepoint count; the existing UTF-8 encode loop is unchanged. NFD and NFKD take the same path as before.
json_composedfixture inscalar_string_test.cc: its bytes were the decomposed form(
61 CC 81), the same string asjson_decomposed, so the compose assertions were comparing a valuewith itself and passed with the bug. Thanks to
@Santoshkumarpuppalafor spotting that on the issue.test_utf8_normalizein pyarrow; the existing input, U+00B2, is its own NFC.Are these changes tested?
Yes. With the corrected fixture,
TestStringKernels.Utf8Normalizefails on the unpatched kernel andpasses with this change; the pyarrow test covers the composed forms from Python.
Are there any user-facing changes?
Yes:
utf8_normalizewithNFCandNFKCnow returns composed output. Callers that depended on theprevious (decomposed) result for those forms will see different bytes.
formoption: NFC and NFKC output is decomposed #51225