Преобразовать суррогатную пару Юникода в литеральную строку

Я пытаюсь прочитать высокий символ Юникода из одной строки в другую. Для краткости я упрощу свой код, как показано ниже:

public static void UnicodeTest()
{
    var highUnicodeChar = "𝐀"; //Not the standard A

    var result1 = highUnicodeChar; //this works
    var result2 = highUnicodeChar[0].ToString(); // returns \ud835
}

Когда я назначаю highUnicodeChar непосредственно result1, он сохраняет свое буквальное значение 𝐀. Когда я пытаюсь получить доступ к нему по индексу, он возвращает \ud835. Насколько я понимаю, это суррогатная пара символов UTF-16, используемых для представления символа UTF-32. Я уверен, что эта проблема связана с попыткой неявно преобразовать char в string.

В конце концов, я хочу, чтобы result2 дал то же значение, что и result1. Как я могу это сделать?

Ответ 1

В Unicode у вас есть кодовые точки. Это 21 бит длиной. Ваш персонаж 𝐀, Mathematical Bold Capital A жирный Mathematical Bold Capital A, имеет кодовую точку U + 1D400.

В кодировках Unicode у вас есть кодовые единицы. Это естественная единица кодирования: 8-битная для UTF-8, 16-битная для UTF-16 и так далее. Одна или несколько кодовых единиц кодируют одну кодовую точку.

В UTF-16 две кодовые единицы, которые образуют одну кодовую точку, называются суррогатной парой. Суррогатные пары используются для кодирования любой кодовой точки больше 16 бит, то есть U + 10000 и выше.

Это становится немного сложнее в .NET, так как .NET Char представляет собой единицу кода UTF-16, а .NET String представляет собой набор единиц кода.

Таким образом, ваша кодовая точка 𝐀 (U + 1D400) не может уместиться в 16 бит и нуждается в суррогатной паре, что означает, что ваша строка содержит две кодовые единицы:

var highUnicodeChar = "𝐀";
char a = highUnicodeChar[0]; // code unit 0xD835
char b = highUnicodeChar[1]; // code unit 0xDC00

То есть, когда вы индексируете строку таким образом, вы фактически получаете только половину суррогатной пары.

Вы можете использовать IsSurrogatePair для проверки суррогатной пары. Например:

string GetFullCodePointAtIndex(string s, int idx) =>
    s.Substring(idx, char.IsSurrogatePair(s, idx) ? 2 : 1);

Важно отметить, что кроличья нора переменной кодировки в Unicode не заканчивается в кодовой точке. Кластер графем - это "видимая вещь", которую большинство людей, когда ее спрашивают, в конечном итоге называют "персонажем". Кластер графемы состоит из одной или нескольких кодовых точек: базовый символ и ноль или более комбинирующих символов. Примером комбинирующего символа является умлаут или различные другие украшения/модификаторы, которые вы можете добавить. Посмотрите этот ответ для ужасающего примера того, что могут сделать комбинирующие персонажи.

Чтобы проверить объединяющий символ, вы можете использовать GetUnicodeCategory, чтобы проверить наличие метки вложения, непространственного или межстрочного знака.

Ответ 2

Похоже, что вы хотите извлечь первый "атомный" символ из пользовательской точки зрения (т.е. первого кластера графем Unicode) из строки highUnicodeChar, где "атомный" символ включает обе половины суррогатной пары.

Вы можете использовать StringInfo.GetTextElementEnumerator() чтобы сделать это, разбивая string на атомные куски, а затем беря первый.

Сначала определите следующий метод расширения:

public static class TextExtensions
{
    public static IEnumerable<string> TextElements(this string s)
    {
        // StringInfo.GetTextElementEnumerator is a .Net 1.1 class that doesn't implement IEnumerable<string>, so convert
        if (s == null)
            yield break;
        var enumerator = StringInfo.GetTextElementEnumerator(s);
        while (enumerator.MoveNext())
            yield return enumerator.GetTextElement();
    }
}

Теперь вы можете сделать:

var result2 = highUnicodeChar.TextElements().FirstOrDefault() ?? "";

Обратите внимание, что StringInfo.GetTextElementEnumerator() будет также группа Unicode, комбинированных символов, так что первый графема кластера строки Ĥ=T̂+V̂ будет не H.

Пример скрипки здесь.