Grapheme Clusters
Text Encoding and Unicode
Course 1 ยท Chapter 6 ยท Grapheme Clusters
Text Encoding and Unicode 1 showed three ways to measure a string: bytes, code points, and what a reader sees. Normalization (the previous chapter) cannot give you the third one. A family emoji, a flag, or a letter with two accents is still several code points after any normalization, yet a reader sees one thing. This chapter defines that unit, the grapheme cluster, shows how emoji and Hangul are built from pieces, and uses it to count, cut and reverse text without breaking it.
regex module (install with pip install regex), whose \X pattern matches one extended grapheme cluster.
Facts come from Unicode Standard Annex #29 (UAX #29) and the Wikipedia article on emoji, both read through a summarising tool (see "Not Yet Verified" at the end). Details not checked against a source are marked "(check)".
What Is a Character, to a Reader?
UAX #29 calls what a person thinks of as one character a user-perceived character, and approximates it with the grapheme cluster. Its own example is the letter G with a grave accent: two code points, one perceived character. Grapheme clusters are the units for moving a cursor, selecting, deleting and counting "characters", and a word, line or sentence break should never fall inside one. The standard defines extended grapheme clusters, the recommended default, and older legacy ones kept for compatibility.
Many things that count as a single cluster are built from several code points, each with a job:
| Piece | Code points | Effect |
|---|---|---|
| Combining mark | e.g. U+0301 | Attaches to the previous letter |
| Skin tone modifier | U+1F3FB to U+1F3FF | Follows a human emoji and sets its tone (added in Unicode 8.0, Wikipedia) |
| Zero-width joiner (ZWJ) | U+200D | Glues emoji into one picture, e.g. a family; unsupported systems show the parts side by side |
| Regional indicators | U+1F1E6 to U+1F1FF | 26 letters; a pair spells a flag |
| Variation selector 16 | U+FE0F | Asks for the colourful emoji look (VS15, U+FE0E, asks for plain text look) |
| Keycap sequence | digit or # or *, U+FE0F, U+20E3 | Makes a keycap picture (the exact recipe is from general knowledge: check) |
One Cluster, Many Code Points
Every sample is one thing to a reader and one cluster to the library, while the code point and byte counts range up to 5 and 18. Even CR LF (a Windows-style line break) is one cluster, because rule GB3 forbids a break between them. Zoom in on emoji parts:
The third line is a tone-modified person joined by ZWJ to another emoji: four code points, one picture. The heart shows that U+2764 alone and followed by U+FE0F are both one cluster, differing in how they should be drawn.
The Rules Behind the Clusters
UAX #29 gives every code point a Grapheme_Cluster_Break property (CR, LF, Control, Extend, ZWJ, Regional_Indicator, Prepend, SpacingMark, and the Hangul types L, V, T, LV and LVT) and then a list of rules. In plain terms:
- GB3: do not break between CR and LF. GB4, GB5: otherwise always break around controls.
- GB6 to GB8: keep Hangul jamo (and syllable) sequences together.
- GB9: do not break before Extend or ZWJ characters (this keeps a letter with its marks). GB9a: do not break before a SpacingMark. GB9b: do not break after a Prepend character.
- GB11: do not break inside an emoji ZWJ sequence.
- GB12, GB13: regional indicators pair up into flags; an odd one out is left alone.
- GB999: anywhere else, break.
Hangul shows why composition does not matter to the rules: a syllable written as one code point and the same syllable as three conjoining jamo are both single clusters. Flags show the odd-one-out rule:
Four regional indicators make two flags; a fifth makes a third cluster that is not a valid flag by itself, but is still a cluster. The Hangul syllable ๊ฐ (U+AC01) is the example UAX #29 itself uses.
A Small Segmenter
You do not need to write one in practice, but building a simplified one makes the rules concrete. This version treats marks, joiners, variation selectors and skin tones as "extend", glues whatever follows a ZWJ, pairs regional indicators, and keeps CR LF together. It leaves out Hangul, Prepend and SpacingMark, so it is a teaching sketch and not UAX #29:
On this sample, 24 code points make 13 clusters (seven things plus six spaces), and the sketch agrees with the library. It would not on Hangul jamo or Indic scripts, and the rule list above is the reason: the real rules have more cases than a page of code. Use a maintained library.
Using Clusters: Counting, Cutting, Reversing
Cutting a string at a code point index can split a cluster, leaving a stray accent, half a family, or a dangling joiner. Cut at cluster boundaries instead:
(ascii() shows the escapes so the invisible parts can be seen.) A four-code-point slice lost the accent from "cafรฉ", and a seven-code-point slice kept only the man of the family. Reversing by code points left the string starting with a stray girl and a joiner,
while reversing the list of clusters kept each cluster intact. The same logic applies to character limits in forms: count clusters if you mean what a reader sees, but remember that storage limits are still in bytes or code points (a database may count differently: next chapters).
unicodedata reports its Unicode version in unicodedata.unidata_version, and a library's data may be older or newer than that.
Also, a cluster is a good default for "one character", not a perfect one: what a reader sees depends on the font and language.
Exercises
Write report(s) printing the number of code points, the number of clusters and each code point in hex. Use it on a thumbs-up, the same thumbs-up with a skin tone, a woman with a tone joined to a laptop (use ZWJ), a flag, a plain heart and a heart with U+FE0F. Predict the cluster counts first.
Show that composition does not change cluster counts. Take three Hangul syllables, count code points and clusters under NFC and under NFD. Then show that "a", CR LF, "b" is 3 clusters but 4 code points, and compare a letter followed by two combining marks with a combining mark followed by a letter.
๐ View solutionWrite truncate(s, limit) that shortens text to at most limit clusters, ending with an ellipsis when it cuts, and never splitting a cluster. Test it on plain text, text with a combining accent, and text containing a family emoji. Compare with a naive slice.
Chapter 6 Quick Reference
- A grapheme cluster approximates a user-perceived character; use it for counting, cursor movement, selection, truncation and reversal
- Emoji parts: skin tone modifiers (U+1F3FB-1F3FF), ZWJ (U+200D), regional indicators (U+1F1E6-1F1FF, pairs make flags), variation selector U+FE0F
- Rules (UAX #29): GB3 keeps CR LF together; GB9 keeps marks and ZWJ with their base; GB11 keeps emoji ZWJ sequences; GB12/13 pair regional indicators
- Hangul syllables and conjoining jamo are single clusters either way, so normalization does not change cluster counts
- Python's standard library has no grapheme function (check); the
regexmodule's\Xmatches one extended grapheme cluster - Never slice or reverse by code point when the output is for people; cut and reverse lists of clusters
- Segmentation rules grow with each Unicode version; keep libraries current