Text Reversal and Bidirectional Unicode
Reversing a string sounds trivial — swap characters from both ends until you reach the middle. But the moment your text contains emojis, combining marks, or right-to-left scripts, the simple approach breaks. What you see as a single character on screen might be multiple code points under the hood, and naïve reversal scrambles them into gibberish.
The Problem with Naïve Reversal
Consider this string: "noe\u0308l" — the word "noël" written with a combining diaeresis on the e. It contains five code points:
| Index | Code Point | Character |
|---|---|---|
| 0 | U+006E | n |
| 1 | U+006F | o |
| 2 | U+0065 | e |
| 3 | U+0308 | ◌̈ (combining diaeresis) |
| 4 | U+006C | l |
A naïve reversal that operates on code points produces "l\u0308eon" — the combining mark now attaches to the l instead of the e, rendering as "l̈eon". The diaeresis hopped to the wrong letter because the algorithm did not respect grapheme boundaries.
Grapheme Clusters
A grapheme cluster is the smallest unit of text that a user perceives as a single character. It may consist of:
- A base character (e.g.,
e) - Zero or more combining marks (e.g., diaeresis, accent, cedilla)
- Zero or more variation selectors or joiners
The family emoji 👨👩👧👦 illustrates this dramatically. It is a single grapheme cluster composed of seven code points:
U+1F468 👨 Man
U+200D Zero-Width Joiner
U+1F469 👩 Woman
U+200D Zero-Width Joiner
U+1F467 👧 Girl
U+200D Zero-Width Joiner
U+1F466 👦 Boy
Reversing these code points individually splits the joiner sequences, producing a string of disconnected emoji that no longer renders as a family.
Proper Reversal with Grapheme Clusters
To reverse text correctly, you must segment it into grapheme clusters first, then reverse the cluster order:
function reverseGraphemes(str) {
const segmenter = new Intl.Segmenter('en', { granularity: 'grapheme' });
const clusters = [...segmenter.segment(str)].map(s => s.segment);
return clusters.reverse().join('');
}
import unicodedata
def reverse_graphemes(s):
clusters = []
current = ''
for char in s:
if unicodedata.combining(char) and current:
current += char
else:
if current:
clusters.append(current)
current = char
if current:
clusters.append(current)
return ''.join(reversed(clusters))
Note that the Python version above is a simplified heuristic. Full grapheme segmentation requires following the Unicode Text Segmentation algorithm (UAX #29), which handles emoji ZWJ sequences, regional indicators, and Hangul jamo. Libraries like grapheme (Python) or graphemer (JavaScript) implement this correctly.
Bidirectional Text and Reversal
Right-to-left (RTL) scripts like Arabic and Hebrew add another layer of complexity. The Unicode Bidirectional Algorithm (UAX #9) determines the display order of mixed-direction text.
Consider: "Hello و globalization". The word و (Arabic "and") is RTL. The visual display order differs from the logical storage order:
Logical order: H e l l o و g l o b a l i z a t i o n
Display order: H e l l o و n o i t a z i l a b o l g
Reversing the logical code points reverses the storage order, but the bidirectional algorithm then re-applies, producing unexpected visual output. The relationship between logical reversal and visual reversal is not obvious and depends on the surrounding text direction.
Surrogate Pairs in UTF-16
JavaScript and Java store strings as UTF-16 code units. Characters outside the BMP (like most emoji) are encoded as surrogate pairs — two code units representing one code point.
const emoji = '😊';
console.log(emoji.length); // 2 (surrogate pair)
console.log([...emoji].length); // 1 (code point via spread)
Reversing at the code unit level splits surrogate pairs, producing invalid UTF-16 and rendering as replacement characters (�). Always iterate by code point or grapheme cluster, never by code unit.
Comparison of Reversal Granularities
| Level | Preserves | Breaks |
|---|---|---|
| Code unit (UTF-16) | ASCII | Surrogate pairs, combining marks, emoji |
| Code point | BMP characters | Combining marks, ZWJ sequences |
| Grapheme cluster | Combining marks, emoji, ZWJ sequences | RTL visual order, edge cases |
For most practical purposes, grapheme-cluster reversal is the correct granularity.
Key Takeaways
- Naïve code-point reversal breaks combining marks, separating diaereses from their base letters.
- Emoji with ZWJ sequences (family, skin tones) are multi-code-point grapheme clusters that must not be split.
- Use
Intl.Segmenter(JavaScript) or a grapheme library (Python) to segment text before reversing. - RTL scripts interact with reversal in non-obvious ways because the bidirectional algorithm re-applies to the reversed logical order.
- UTF-16 code-unit reversal is always wrong for text containing emoji or other supplementary characters.
Try It Yourself
Curious how reversal handles emoji and accented characters? Test any string in the Text Reverser and see the difference proper Unicode handling makes.