Bug #21559
closedUnicode normalization nfd -> nfc -> nfd is not reversible
Updated by nobu (Nobuyoshi Nakada) about 1 year ago
Updated by ima1zumi (Mari Imaizumi) about 1 year ago
- Assignee set to ima1zumi (Mari Imaizumi)
This looks like a bug. Per Unicode TR15, the identity toNFD(x) == toNFD(toNFC(x)) must be maintained. https://unicode.org/reports/tr15/#Design_Goals
It seems the NFC process is combining characters across U+11930, even though its CCC is 0.
Updated by duerst (Martin Dürst) about 1 year ago
- Assignee changed from ima1zumi (Mari Imaizumi) to duerst (Martin Dürst)
@ima1zumi (Mari Imaizumi) Not sure this is even allowed, but I'm sure I'm responsible for this behavior, and want to fix it myself, so I change the Assignee to myself.
Updated by ima1zumi (Mari Imaizumi) about 1 year ago
@duerst (Martin Dürst) Thank you, I appreciate you taking care of it.
Updated by duerst (Martin Dürst) 12 months ago
- Status changed from Open to Closed
Added regression test at https://github.com/ruby/ruby/commit/a122d7a58e91ed6cd531e906cb398688d7cc8b17
and fix at https://github.com/ruby/ruby/commit/e4c8e3544237b8c0efba6b945173dc66552d641c.
Many thanks to Tomoya Ishida for finding this bug.
Updated by duerst (Martin Dürst) 12 months ago
- Backport changed from 3.2: UNKNOWN, 3.3: UNKNOWN, 3.4: UNKNOWN to 3.2: DONTNEED, 3.3: DONTNEED, 3.4: DONTNEED
Backport would only be needed if the upgrade to Unicode 16.0.0 (see https://bugs.ruby-lang.org/issues/20724) is backported.
Updated by duerst (Martin Dürst) 12 months ago
Note to potential backporters: https://github.com/ruby/ruby/commit/bd51b20c50 should also be backported.
Updated by camertron (Cameron Dutro) 5 days ago
Hi there @duerst (Martin Dürst) 👋
I found this ticket after running into a normalization problem while working on an implementation of the Unicode Collation Algorithm. It seems there is at least one other case where the identity toNFD(x) == toNFD(toNFC(x)) is not maintained by the current Ruby normalization implementation:
# ruby 4.1 (master)
str = "\u0FB2\u0591\u0F81\u0061"
p str.unicode_normalize(:nfd) == str.unicode_normalize(:nfc).unicode_normalize(:nfd)
#=> false
In this example, the 0F81 decomposes into 0F71 0F80, which have combining classes of 129 and 130 respectively. The 0591 that comes logically earlier in the codepoint sequence has a combining class of 220. Because Ruby's NFD normalization algorithm decomposes subsections of the string rather than the entire string at once, the 0FB2 and 0591 are processed in isolation, which generates a string with combining classes 0 220 129 130 0, which is not the correct canonical order; 0591 (combining class of 220) should be ordered after 0F80 (combining class of 130).
One possible solution is to augment the NFD regex to include characters that decompose into entirely "accent" characters (i.e. characters that all have non-zero combining classes). That way, such characters are decomposed and reordered along with any accent characters that precede them.
I've created a PR demonstrating this solution: https://github.com/ruby/ruby/pull/19393
Curious to hear your thoughts.