Skip to main content

Code points and graphemes

The library includes the Unicode data that it uses for layout. The namespace PinataLabs.Typography.Unicode makes some of it public, so you can use the same rules in your own code. This is useful, for example, to compute grapheme indexes for a TextRun, or to move a caret one character at a time.

Three ways to count​

A .NET string is a sequence of UTF-16 code units (char). A code point outside the Basic Multilingual Plane, such as most emoji, uses two of them. A grapheme, what a reader sees as one character, can be several code points.

using PinataLabs.Typography.Unicode;

// e + combining acute accent, x, thumbs up + medium skin tone
string text = "e\u0301x\U0001F44D\U0001F3FD";

int chars = text.Length; // 7
int codePoints = text.GetCodePointCount(); // 5
int graphemes = text.GetGraphemeCount(); // 3

The counting methods are extension methods for string, Span<char> and ReadOnlySpan<char>.

Enumerate graphemes and code points​

EnumerateGraphemes returns each grapheme as a ReadOnlySpan<char>. EnumerateCodePoints returns each CodePoint. Neither allocates.

foreach (ReadOnlySpan<char> grapheme in text.AsSpan().EnumerateGraphemes())
{
Console.WriteLine(grapheme.ToString());
}

foreach (CodePoint codePoint in text.AsSpan().EnumerateCodePoints())
{
Console.WriteLine($"U+{codePoint.Value:X4}");
}

Graphemes follow the extended grapheme cluster rules of UAX #29.

Find the grapheme index of a char index​

TextRun.Start and TextRun.End count graphemes. To convert from a char index, count the graphemes before it:

static int GraphemeIndex(string text, int charIndex)
=> text.AsSpan(0, charIndex).GetGraphemeCount();

string text = "Café 👍🏽 ok";
int start = GraphemeIndex(text, text.IndexOf("ok", StringComparison.Ordinal)); // 7

This works when charIndex is at the start of a grapheme.

The CodePoint type​

CodePoint is a value type for one Unicode scalar value. Make one from a char, a surrogate pair or an int:

CodePoint a = new('A');
CodePoint smile = new(0x1F600);
CodePoint fromPair = new('\uD83D', '\uDE00'); // Also U+1F600.

Console.WriteLine(smile.IsBmp); // False
Console.WriteLine(smile.Utf16SequenceLength); // 2
Console.WriteLine(CodePoint.IsValid(0xD800)); // False: a lone surrogate is not a scalar value.

CodePoint.ReplacementChar is U+FFFD.

Unicode properties​

Static methods on CodePoint read the Unicode properties of a code point:

MethodReturns
GetGeneralCategoryThe general category, as System.Globalization.UnicodeCategory.
GetBidiClassThe bidirectional class and paired-bracket data, as a BidiClass.
GetLineBreakClassThe line-break class from UAX #14, as a LineBreakClass.
GetGraphemeClusterClassThe grapheme-break property from UAX #29, as a GraphemeClusterClass.
GetVerticalOrientationTypeThe vertical orientation from UAX #50, as a VerticalOrientationType.
TryGetBidiMirrorThe mirrored character, for example ) for (.
TryGetVerticalMirrorThe vertical form of a character, if it has one.

And tests:

IsLetter, IsDigit, IsLetterOrDigit, IsNumber, IsLower, IsUpper, IsMark, IsPunctuation, IsSeparator, IsSymbol, IsControl, IsWhiteSpace, IsNonBreakingSpace, IsNewLine, IsTabulation, IsVariationSelector, IsZeroWidthJoiner, IsZeroWidthNonJoiner.

CodePoint hyphen = new('-');
LineBreakClass lineBreak = CodePoint.GetLineBreakClass(hyphen); // LineBreakClass.HY

BidiClass bidi = CodePoint.GetBidiClass(new CodePoint('('));
if (bidi.TryGetPairedBracket(out CodePoint closing))
{
Console.WriteLine(closing); // )
}

new ArabicJoiningClass(codePoint) gives the Arabic joining type and joining group of a code point. The namespace also has the ScriptClass, IndicSyllabicCategory and IndicPositionalCategory enumerations. The shapers use them inside the library; there is no public method that returns them for a code point. See Unicode types.