Clarify the observable contract
The word character is ambiguous in a .NET interview. A string length counts UTF-16 code units; Rune enumeration counts Unicode scalar values; StringInfo identifies text elements. This task truncates by the latter unit so it does not split the sample emoji surrogate pair or the combining sequence. It preserves the original spelling and does not normalize equivalent sequences.
Ask whether the product measures code units, scalar values, grapheme-like text elements or rendered columns. These quantities can disagree even for a short valid string. The task deliberately chooses text elements for truncation and preserves original spelling, leaving normalization and display layout as independent policies.
Walk through the verified program
The sample contains A, one supplementary emoji, e and a combining acute accent. Those occupy five UTF-16 code units, four scalars and three text elements. Taking the first two text elements returns A followed by the intact emoji. The prefix function uses element start indices instead of slicing at the requested count of code units.
The element-start array identifies A at zero, the emoji at one and the combining sequence at three. Selecting boundary index two therefore slices before the e, retaining the emoji's complete surrogate pair. The four scalar count includes the combining accent separately, which explains why scalar-aware truncation alone is not the same requirement.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
string text="A\U0001F600e\u0301";
Console.WriteLine("UTF16:"+text.Length); Console.WriteLine("scalars:"+text.EnumerateRunes().Count());
int[] starts=StringInfo.ParseCombiningCharacters(text); Console.WriteLine("text elements:"+starts.Length);
Console.WriteLine("first two:"+Prefix(text,2));
static string Prefix(string text,int count) { if(count<0) throw new ArgumentOutOfRangeException(nameof(count)); int[] starts=StringInfo.ParseCombiningCharacters(text); return count>=starts.Length?text:text[..starts[count]]; }
Account for time, storage and allocations
Parsing all element boundaries takes O(n) work and O(g) index storage for g text elements. The substring result can allocate O(n) code units. This implementation prioritizes a clear boundary contract rather than a minimal-allocation prefix scan. Unicode segmentation behavior follows the runtime implementation.
The boundary-array approach allocates proportional to the number of elements even when only a short prefix is requested. A streaming text-element enumerator could reduce retained boundary storage, but the simpler implementation is easier to audit. Choose the alternative only after preserving the same semantics and verifying the required runtime behavior.
Review assumptions and likely defects
Rune is not the same as a user-perceived grapheme, and string.Length is not a display width. Equal-looking strings can still differ ordinally without normalization. Input containing unpaired surrogates or requirements for locale-specific presentation need an explicit policy beyond this fixed valid-Unicode fixture.
Visually identical forms should not be assumed ordinally equal. A separate normalization step can change code-unit representation and therefore keys or stored offsets. If offsets refer back to original text, normalizing first may invalidate those coordinates unless the API explicitly defines the coordinate system.
Practice one: discriminate edge cases
Test count zero, a count beyond the element count and precomposed versus decomposed accented text. Hint: compare both preserved code units and element counts rather than assuming normalization occurred.
Compare a precomposed accented letter and its decomposed spelling before and after a deliberate normalization step. Hint: both may have one text element without being the same ordinal string. Keep that experiment separate from the emoji truncation check so representation equality is not confused with boundary preservation.
Practice two: defend an extension
Design a username-length rule that also handles normalization, case comparison and security-sensitive lookalikes. State which rules are product decisions and which Unicode units you measure; do not claim grapheme-aware truncation solves spoofing or terminal column width.
A defensible username contract can normalize to a chosen form, apply a documented comparer and enforce a text-element limit, while separately validating allowed characters and security concerns. It should preserve or derive display spelling intentionally. No general Unicode API can decide all product-specific identity and spoofing policies without those requirements.
Evaluate correctness and interview reasoning
A good interview explanation derives five code units, four scalars and three text elements from the actual sample rather than memorizing that Unicode is complicated. It shows why slicing at two code units would be unsafe and why a two-element prefix is different. It also reports the boundary-array allocation honestly. A senior answer separates normalization, case matching, display width and security from segmentation. The runnable reference card supplies an additional representation-and-segmentation counterexample, reinforcing that a correct truncation boundary does not automatically establish a complete text-identity policy. Empty input and a zero-element prefix need direct tests too. The function returns an empty string without indexing beyond the empty boundary array, preserving the declared count semantics.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
string text="A\U0001F600e\u0301";
Console.WriteLine("UTF16:"+text.Length); Console.WriteLine("scalars:"+text.EnumerateRunes().Count());
int[] starts=StringInfo.ParseCombiningCharacters(text); Console.WriteLine("text elements:"+starts.Length);
Console.WriteLine("first two:"+Prefix(text,2));
static string Prefix(string text,int count) { if(count<0) throw new ArgumentOutOfRangeException(nameof(count)); int[] starts=StringInfo.ParseCombiningCharacters(text); return count>=starts.Length?text:text[..starts[count]]; }
Expected output
UTF16:5 scalars:4 text elements:3 first two:A😀
Test count zero, a count beyond the element count and precomposed versus decomposed accented text. Hint: compare both preserved code units and element counts rather than assuming normalization occurred.
Further reading
Interview questions for this lesson
- Explain: Why do UTF-16 length, Rune count and text-element count disagree for this sample, and what unit does truncation preserve?
- Trace: Trace the complete program for clarify unicode units before truncating. State the exact stdout, then justify the state transitions and relevant edge conditions.
- Design: Design a username-length rule that also handles normalization, case comparison and security-sensitive lookalikes. State which rules are product decisions and which Unicode units you measure; do not claim grapheme-aware truncation solves spoofing or terminal column width.