\X — one character as a person counts it¶
One line: \X matches an extended grapheme cluster — a base character with its combining marks, an emoji with its skin tone and joiners, a flag made of two regional indicators — in Perl, PCRE2, Ruby and Java; . matches one code point everywhere and so splits all three; and Python, Go and Rust have no \X, while JavaScript reads it as the letter X.
Level: reference · one construct, eight engines
What it means¶
é can be one code point (U+00E9) or two (e U+0065 followed by U+0301 COMBINING ACUTE ACCENT). A family emoji is up to seven code points glued with U+200D ZERO WIDTH JOINER. A flag is two regional-indicator symbols. To a person each of those is one character; to . each is one code point, and . on the second é matches the e and leaves the accent behind.
\X is the escape that matches the whole cluster, using the boundary rules in UAX #29 ↗.
Measured¶
Verified output of kw_grapheme_cluster_sh.sh — regenerated by tools/run_examples.py, never hand-typed.
construct python perl node ruby java go rust fancy
. on e + combining accent e e e e e e e e
\X on e + accent - e\u{301} no e\u{301} e\u{301} - - -
\X on a ZWJ sequence - \u{1F468}\u{200D}\u{1F469} no \u{1F468}\u{200D}\u{1F469} \u{1F468}\u{200D}\u{1F469} - - -
\X on a flag pair - \u{1F1EB}\u{1F1F7} no \u{1F1EB}\u{1F1F7} \u{1F1EB}\u{1F1F7} - - -
\X in JavaScript - X X X X - - -
Reading the table¶
- Row 1. All eight engines match
ealone —.stops before the combining accent. This is not an engine defect; it is what.is defined to do. - Row 2,
\Xon the same subject. Perl, Ruby and Java take both code points as one match. Python, Go and Rust's two crates refuse the escape. JavaScript'snois the identity escape:\XisX, and there is noXin the subject — row 5 confirms it by matching one. - Rows 3–4. The ZWJ family and the flag pair come back whole from the same three engines. Java's
\Xhas understood emoji sequences since Java 9; older Java matched only base-plus-marks.
The trap¶
Truncating text with a regex. ^.{10} to take "the first ten characters" of a string can cut a family into a man and a joiner, or a flag into half a flag, in every engine on this page — and in JavaScript without u it can cut a single emoji in half. Where \X exists, ^\X{10} is the honest version; where it does not, the host language has a grapheme iterator (Rust's unicode-segmentation, Go's x/text, Python's third-party regex or grapheme) and the regex is the wrong tool.
Where it is measured in depth¶
- Naming a Unicode property —
\Xbeside the properties, per engine. - What
\wand\bmatch — combining marks and whether they count as\w.
See also¶
.— what\Xcorrects.\p{…}.- Unicode text · the Encodings learning library ↗ for what a character is in the first place.