Articles
Unicode String Interview Questions: Code Units, Graphemes, and Broken Reversal
Practise Unicode string interview questions with UTF-16 units, code points, grapheme reversal, safe truncation, and normalization counterexamples.
“Reverse this string” is incomplete when the input can contain Unicode. Reversing UTF-16 code units can split an emoji's surrogate pair. Reversing code points preserves that pair but can separate an accent from its base letter or reorder a joined emoji sequence. The correct implementation follows from the unit the problem asks you to preserve.
This guide uses JavaScript to make those distinctions observable. Standards and API facts are linked to Unicode specifications and JavaScript reference documentation. Original exercises and deductions use explicit fixtures and contracts. Candidate-report boundary: these are independent interview preparation cases, not a claim that a named employer asks these exact questions.
An adjacent starting exercise is Reverse a Character Array In Place. Its array elements already define a unit. A JavaScript string requires you to make that decision before applying the same two-pointer idea.
Question 1: What does string length actually count?
Documented JavaScript fact: String.length counts UTF-16 code units. A code point outside the Basic Multilingual Plane uses two units, so a length of two does not necessarily mean two user-visible characters.
Standards fact: Unicode Text Segmentation, UAX #29, defines default extended grapheme-cluster boundaries. Grapheme clusters approximate user-perceived characters; they are not a promise that every cluster equals one glyph or has one fixed display width.
The layers answer different questions:
- A code unit is an encoding unit. JavaScript indexing and many string offsets use UTF-16 units.
- A code point identifies a Unicode value. A combining accent and its base letter can be separate code points.
- A grapheme cluster groups text according to segmentation rules. A letter plus accent or a supported joined emoji sequence can form one cluster.
Original fixture: these five strings each segment into one grapheme in the tested runtime:
| String | UTF-16 units | Code points | Grapheme clusters |
|---|---|---|---|
A | 1 | 1 | 1 |
😀 | 2 | 1 | 1 |
e\u0301 — e plus combining acute | 2 | 2 | 1 |
🇺🇸 — two regional indicators | 4 | 2 | 1 |
👩💻 — woman, joiner, laptop | 5 | 3 | 1 |
Do not use this table to infer byte length. Encoding a string as UTF-8 produces a different measurement. Similarly, five graphemes need not occupy five terminal columns or five equally sized boxes in a browser.
An interview clarification can be short: “Should length and reversal operate on UTF-16 units, Unicode code points, or user-facing grapheme clusters? Are inputs restricted to ASCII?” If ASCII is explicitly the contract, a simple implementation may be sufficient. If it is not, state your chosen behavior before coding.
Question 2: Why does split-and-reverse break emoji?
Consider this familiar expression:
const reverseUnits = s => s.split("").reverse().join("");
Worked deduction: with A😀B, splitting on the empty string treats the two UTF-16 units of the emoji separately. Reversal changes their order. The result contains malformed surrogate structure rather than the intact emoji between B and A.
Rendering may display replacement symbols, making two distinct malformed outputs look alike. Inspect escaped units or well-formedness instead of trusting what the terminal happens to draw. The precise defect is an encoding-boundary violation.
A candidate might repair it with spread syntax:
const reversePoints = s => [...s].reverse().join("");
Documented JavaScript fact: the string iterator yields code points, preserving surrogate pairs while still splitting grapheme clusters. Spread and for...of use that iterator.
Therefore the second implementation repairs A😀B, but it does not meet every user-facing reversal contract. If the input is e\u0301x, code-point reversal produces x, then the combining acute accent, then e. The accent can attach to the wrong base. Reversing the code points of 👩💻 also changes the joined sequence rather than moving it intact.
The lesson is to identify which assumption failed. “Unicode-safe” is too vague to describe either function. Name the preserved unit and the input domain.
Question 3: How do you reverse grapheme segments?
Documented API fact: Intl.Segmenter supports locale-sensitive segmentation with grapheme, word, or sentence granularity. Choose grapheme granularity for this exercise, and check availability in the target environment.
Original contract: given a well-formed JavaScript string, obtain its grapheme segments using the runtime's segmenter, reverse the resulting segment list, and concatenate each segment unchanged. Do not normalize, case-fold, or reverse its internal code points.
const graphemes = new Intl.Segmenter("en", {
granularity: "grapheme"
});
function reverseGraphemeSegments(s) {
const chunks = Array.from(
graphemes.segment(s), item => item.segment
);
return chunks.reverse().join("");
}
const input = "Ae\u0301👩💻🇺🇸Z";
const expected = "Z🇺🇸👩💻e\u0301A";
console.assert(reverseGraphemeSegments(input) === expected);
This function preserves the code-point sequence inside every segment it originally obtained. It allocates a segment array and a result string; it is not an in-place constant-space reversal of immutable JavaScript text. Under a linear segmentation model, it requires O(n) work and O(n) additional storage relative to input size.
There is a subtle boundary: concatenating moved segments can change how the result segments if you run segmentation again. Regional-indicator pairing or a leading combining mark can depend on neighboring text. Consequently, do not claim that segment-then-reverse is universally an involution over all strings. Reversing a stored list twice restores that list; resegmenting each intermediate string is a different operation.
For a UI requirement, ask whether this contract is the intended behavior. Visual reversal of bidirectional text, cursor movement, and linguistic transformations involve additional rules. Moving grapheme blocks is a useful explicit exercise, not a complete text-layout engine.
Question 4: How do you truncate without cutting a cluster?
A preview requirement says “keep the first three characters.” If characters means graphemes, slice(0,3) is the wrong boundary for a string containing supplementary characters or combining sequences.
Documented API fact: a segment's index from Intl.Segmenter.segment is an offset into the original string in code units. It is not a grapheme ordinal.
Original implementation: accept a nonnegative integer limit, walk whole segments, and slice only at the start of the first excluded segment:
function takeGraphemes(s, limit) {
if (!Number.isInteger(limit) || limit < 0) {
throw new RangeError("limit must be a nonnegative integer");
}
let count = 0;
for (const item of graphemes.segment(s)) {
if (count === limit) return s.slice(0, item.index);
count += 1;
}
return s;
}
console.assert(takeGraphemes("Ae\u0301😀Z", 3) === "Ae\u0301😀");
console.assert(takeGraphemes("Ae\u0301😀Z", 0) === "");
The example's first three graphemes occupy five UTF-16 units. Keep the unit conversion visible. Returning a grapheme count to a caller that expects a slice offset creates an integration bug even when the segmenter itself is correct.
An ellipsis introduces another contract choice: does the limit include the ellipsis? Is it added only when truncation occurred? If the limit is zero, should the output be empty or an ellipsis? Do not bury those decisions in the loop.
For an editor, maintain source offsets for selections and highlights. If you normalize or otherwise transform the text first, an offset in that transformed string may no longer identify the same span in the original. Either keep the coordinate space consistent or preserve an explicit mapping.
Question 5: Does normalization solve the problem?
It solves a different problem. Two visually similar strings can have different code-point sequences, and some sequences are canonically equivalent.
Standards fact: Unicode Normalization Forms, UAX #15, distinguishes canonical forms from compatibility forms. NFC performs canonical composition; NFD performs canonical decomposition. NFKC and NFKD also apply compatibility equivalences, which may remove distinctions your application cares about.
Documented JavaScript fact: String.normalize provides those forms. For this original equality fixture:
const composed = "\u00E9";
const decomposed = "e\u0301";
console.assert(composed !== decomposed);
console.assert(composed.normalize("NFC") === decomposed.normalize("NFC"));
Normalization does not turn every emoji into one code point, define display width, or replace grapheme segmentation. It also does not mean lowercasing, accent removal, or universal visual equivalence.
Design advice: choose normalization at a documented boundary when the application intends canonical equivalence. Preserve raw input when exact spelling or source offsets matter. Do not silently apply compatibility normalization to identifiers, passwords, or signed data without an explicit policy.
For an anagram question, ask whether two canonically equivalent representations should compare equal and what units form the frequency map. For a palindrome question, ask whether punctuation, case, and accents are significant. “Normalize everything” is not a defensible substitute for answering those questions.
Question 6: How do you test this without trusting the font?
Use fixtures that separate the three implementations rather than many ordinary English words. In this original test ledger, the grapheme contract is segment-list reversal in a runtime supporting Intl.Segmenter:
| Fixture | What to inspect |
|---|---|
| Empty string and one ASCII letter | Boundary handling without special cases |
A😀B | Surrogate pair remains intact |
e\u0301x | Accent stays inside its original segment |
A👩💻B | Joined sequence moves as one block |
A🇺🇸B | Regional-indicator pair stays together in this fixture |
| NFC and NFD spellings of é | Equality policy and preservation of original representation |
| Leading combining marks and odd regional-indicator runs | Context-sensitive resegmentation after movement |
| A lone surrogate | Reject, preserve, or repair according to an explicit input policy |
For the reference functions here, well-formed input is a precondition; they do not validate or repair lone surrogates. A production boundary that accepts arbitrary JavaScript strings must decide that policy separately. Do not imply that a segmenter guarantees well-formed input.
Record the runtime and its Unicode/ICU data when comparing results across environments. A newly defined sequence may segment differently in an older implementation. The JavaScript checks accompanying this guide ran in Node.js 26.9.0 with ICU 78.3 and Unicode 17.0 data. The current UAX #29 page describes Unicode 18.0; citing that specification does not mean this runtime implements its newest data. These fixtures passed locally, without establishing identical behavior in every deployed browser.
In an interview, explain a failing result at the right layer: broken surrogate structure, reordered combining code point, changed grapheme boundary, or unexpected normalization policy. That explanation is more useful than saying the emoji “looks wrong.”
Sources and Further Reading
- Unicode UAX #29: Text Segmentation — default grapheme boundaries and tailoring.
- Unicode UAX #15: Normalization Forms — canonical and compatibility normalization.
- MDN: String.length — UTF-16 length.
- MDN: String iterator — code-point iteration and its limits.
- MDN: Intl.Segmenter and segment — segmentation and original-string offsets.
- MDN: String.normalize — normalization API behavior.