Разделить строку со смайликами

У меня есть строка: "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???". Мне нужно, чтобы у меня разделилось абсолютно всё, все буквы, пробелы, смайлы и так далее. Дальше мне нужно всё это внести в массив:

List<string> array = new List<string>();

чтобы я мог обратится к каждому символу отдельно.

Какой должен быть параметр в Regex.Split, чтобы он мог распознать смайлы, отделить их и при этом отделить остальные символы?

Данный параметр не работает как мне нужно:

Regex.Split(text, @"\p{So}");

Ответы (5 шт):

Автор решения: InterceptorTSK

Смайлики - это не всегда чары. Это последовательность чаров, может быть и так. Это всё упирается в Юникод, и его кодирование. Причём utf-8/utf16 здесь ни при чём.

Формализуем проблему.

  1. Существует Юникод. Именно в нём и нужно разбираться. Потому что именно в Юникоде все символы, визуальные представления и существуют, и больше нигде.

  2. Юникод строится не на чарах, а на Кодовых Точках (Code Points). Чар - его значение может совпадать со значением Кодовой Точки, и тут проблем нет.

    Пробел: Char 0x0020 = U+00000020 (Code Point - Space) в Юникоде.

  3. Чар имеет границы, максимальное/минимальное значение. Это диапазон 0x0000 - 0xFFFF. А символов гораздо больше. Чары и Кодовые Точки совпадают в Basic Multilingual Plane (BMP). Т.е. сложное перекодирование не нужно, значения совпадают.

  4. Два чара могут кодировать Кодовую Точку, это называется Surrogate Pair.

    Some Code Point: Pair {Char 0x0045, Char 0x0067} = U+00567834 (Some Code Point) в Юникоде. Внимание, последовательность чаров нужно всегда проверять - является ли чар и чар+1 - на то, является ли эта последовательность Surrogate Pair, и эта Surrogate Pair (два чара) образует одну Кодовую Точку Юникода.

  5. Всё становится сильно хуже. В Юникоде есть последовательность Кодовых Точек, именно не чаров, а Кодовых Точек, и уже эта последовательность Кодовых Точек может образовывать один визуальный символ.

    Code Point 1, Code Point 2: Sequense {Code Point 1, Code Point 2} = U+00567834, U+00783467 Образует смайлик в шляпке с пером.

Исходя из этого следует парсер. Берём строку, строка - это последовательность чаров. Чары парсим в Кодовые Точки. Кодовые точки (одиночные, и разрешённые последовательности) парсим в визуальные представления. А визуальные представления строятся на разрешённых таблицах.

У вас задача чуть другая. Но суть не меняется. Вам нужно заиметь таблицы Юникода в Кодовых Точках и всевозможных их комбинациях. На сайте Юникода это всё есть. Там же есть алгоритм, но он строится (опирается) строго на таблицы - таблицы разрешённых (допустимых) Кодовых Точек, и разрешённых (допустимых) последовательностях Кодовых Точек.

Т.е. алгоритма без таблиц - не существует. Алгоритмы без таблиц - это псевдо-алгоритмы, забагованы ошибочны итд итп.

→ Ссылка
Автор решения: EvgeniyZ

В C# для этого существует StringInfo.GetTextElementEnumerator из System.Globalization.

Код будет примерно таким:

var input = "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???";
var charEnum = StringInfo.GetTextElementEnumerator(input);

var list = new List<string>();
while (charEnum.MoveNext())
{
    list.Add(charEnum.GetTextElement());
}   

В list будут все символы и эмодзи (пример).

→ Ссылка
Автор решения: InterceptorTSK

Не ответ. Специально для @EvgeniyZ. Ответ уже написан словами, в комментарии.

Набор Кодовых Точек - это всегда набор чаров, представимых в определённой кодировке. Таблица наборов чаров - это и есть то что нужно, под конкретный шрифт.

Таблица последовательностей чаров, на самом деле код-пойнтов, но кодированных utf-16, например. Таблица как минимум имеет веса, и собственно последовательность на каждый смайлик, всё это есть на сайте Юникода.

Вес   Последовательность чаров
1234; {0x0056, 0x0067, 0x3456, 0x895a} - смайлик с пером в шляпке
1567; {0x348a, 0x0034} - грустный смайлик
1689; {0x5678, 0x5734, 0x2332, 0x7894, 0x3434, 0x1245} - смайлик с синей шапкой
И так далее

Из входящей строки [последовательность чаров], нужно искать последовательности чаров из таблицы чаров. ВСЁ. Я не понимаю какой код здесь нужен, и зачем его пейсать. Тривиальный код не требует описания, в виде кода. Бессмысленно. Ну потому что поиск под-последовательности в последовательности чаров - это тривиальная операция. Но требующая таблицы. Зачем тут код, тривиальный - понятия не имею. Всё это безумие происходит ровно из тотального непонимания того, как утроен и работает Юникод. Читайте стандарт, и соответствующие таблицы, таблицы приложены к стандарту, полностью.

Какие смайлики поддерживаются шрифтом - те и впишите в вашу таблицу. А зачем ещё? Что бы что? Все остальные смайлики, по определению - не валидны. Потому что не могут быть отображены шрифтом. Полная поддержка Юникода никогда не нужна. Когда нужен банан - он и нужен, а не абизьяна с бананом, и всей Африкой впридачу.

Консорциум Юникода настаивает на собственных таблицах последовательностей, это всё описано в тривиальных рекомендациях и алгоритмах. Но последовательности должны совпадать с последовательностями Юникода. Консорциум настаивает на собственных обобщённых реализациях кодирования/раскодирования. Стандарт устроен ровно так, что эти все процессы кодирования/раскодирования - простейшие, и сразу строятся на предрасчитанных [детерминированных] таблицах последовательностей.

→ Ссылка
Автор решения: Alexander Petrov

Юникод - тема сложная и многогранная. Его поддержка практически во всех языках и платформах оставляет желать лучшего. Но у нас в дотнете всё не так уж плохо.

Если просто брать символы из строки, то мы не учтём суррогатные пары.

Метод StringInfo.GetTextElementEnumerator получает из строки корректные символы юникода, состоящие в том числе из суррогатных пар. Однако эмодзи это не просто суррогатные пары.

Можно попробовать использовать руны.

var input = "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???";

var charEnum = StringInfo.GetTextElementEnumerator(input);
var list = new List<string>();
while (charEnum.MoveNext())
    list.Add(charEnum.GetTextElement());

var list2 = input.EnumerateRunes().Select(r => r.ToString()).ToList();

Console.WriteLine(list.Count);  // 52
Console.WriteLine(list2.Count); // 66

На входной строке из вопроса это даёт разные длины результирующих списков.
Несколько странно, что рун получается больше.


Дополнение. Важно!
Почитал внимательней документацию. Если нужно получить именно графемы - то есть отображаемые символы, которые должен видеть пользователь, - то следует использовать StringInfo.GetTextElementEnumerator. А руны дадут разложенную на кирпичики последовательность символов эмодзи.

→ Ссылка
Автор решения: Dev18

Предложу вариант с использованием StringInfo.ParseCombiningCharacters - способ работать с графемами.

Each index in the resulting array is the beginning of a text element, that is, the index of the base character or the high surrogate.

The length of each element is easily computed as the difference between successive indexes. The length of the array will always be less than or equal to the length of the string. For example, given the string "\u4f00\u302a\ud800\udc00\u4f01", this method returns the indexes 0, 2, and 4.

Каждый индекс в результирующем массиве является началом текстового элемента, то есть индексом базового символа или старшего суррогата.

Длина каждого элемента легко вычисляется как разность между последовательными индексами. Длина массива всегда будет меньше или равна длине строки. Например, если задана строка "\u4f00\u302a\ud800\udc00\u4f01", этот метод возвращает индексы 0, 2 и 4.

код

using System.Globalization;

public class Program
{
    public static List<string> ParseCombining(string input)
    {
        var idx = StringInfo.ParseCombiningCharacters(input);
        var list = new List<string>(idx.Length);
        for (int i = 0; i < idx.Length; i++)
        {
            int start = idx[i];
            int len = (i + 1 < idx.Length ? idx[i + 1] : input.Length) - start;
            list.Add(input.Substring(start, len));
        }

        return list;
    }

    public static void Main()
    {
        var input = "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???";
        var result = ParseCombining(input);

        for (int i = 0; i < result.Count; i++)
        {
            Console.WriteLine($"{result[i]}");
        }
    }
}

результат

С
т
р
о
к
а
,
 
м
о
я
 
с
т
р
о
к
а
 
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?
?‍?
?‍?
?‍❤‍?‍?
??
??
✋?
??

посмотрела решения и собрала их для сравнения

не точность (она одинаковая Разница только в «обёртке»), а именно накладные расходы разных API и способы материализации результата

и вот замеры на бенчмарке

я беру эталон ParseCombining и проверяю, что остальные решения дают такие же списки

проверяю длину списков

ParseCombining = 52
Enumerator     = 52
Yield          = 52
SpanImmediate  = 52

код

using System.Globalization;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

public class Program
{
    public static void Main()
    {
        //var input = "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???";

        //var pc = Helpers.ParseCombining_Core(input);
        //var en = Helpers.Enumerator_Core(input);
        //var yi = Helpers.Graphemes(input).ToList();
        //var sp = Helpers.SpanImmediate_Core(input);

        //Console.WriteLine($"ParseCombining = {pc.Count}");
        //Console.WriteLine($"Enumerator     = {en.Count}");
        //Console.WriteLine($"Yield          = {yi.Count}");
        //Console.WriteLine($"SpanImmediate  = {sp.Count}");

         BenchmarkRunner.Run<GraphemeBench>();
    }
}

public static class Helpers
{
    public static List<string> Enumerator_Core(string s)
    {
        var e = StringInfo.GetTextElementEnumerator(s);
        var list = new List<string>();
        while (e.MoveNext())
            list.Add(e.GetTextElement());
        return list;
    }

    public static IEnumerable<string> Graphemes(string s)
    {
        for (int i = 0; i < s.Length; i += StringInfo.GetNextTextElementLength(s, i))
            yield return StringInfo.GetNextTextElement(s, i);
    }

    public static List<string> ParseCombining_Core(string s)
    {
        var idx = StringInfo.ParseCombiningCharacters(s);
        var list = new List<string>(idx.Length);
        for (int i = 0; i < idx.Length; i++)
        {
            int start = idx[i];
            int len = (i + 1 < idx.Length ? idx[i + 1] : s.Length) - start;
            list.Add(s.Substring(start, len));
        }
        return list;
    }

    public static List<string> SpanImmediate_Core(string s)
    {
        var list = new List<string>(128);
        ReadOnlySpan<char> span = s.AsSpan();
        int offset = 0;
        while (!span.IsEmpty)
        {
            int len = StringInfo.GetNextTextElementLength(span);
            list.Add(s.Substring(offset, len));
            span = span[len..];
            offset += len;
        }
        return list;
    }
}

[MemoryDiagnoser]
public class GraphemeBench
{
    private string input = default!;
    private List<string> reference = default!;

    [GlobalSetup]
    public void Setup()
    {
        input = "Строка, моя строка ???????????????????????????‍??‍??‍❤‍?‍?????✋???";

        reference = Helpers.ParseCombining_Core(input);

        var en = Helpers.Enumerator_Core(input);
        var yi = Helpers.Graphemes(input).ToList();
        var sp = Helpers.SpanImmediate_Core(input);

        if (!reference.SequenceEqual(en))
            throw new InvalidOperationException("Enumerator != ParseCombining.");
        if (!reference.SequenceEqual(yi))
            throw new InvalidOperationException("Yield != ParseCombining.");
        if (!reference.SequenceEqual(sp))
            throw new InvalidOperationException("SpanImmediate != ParseCombining.");
    }

    [Benchmark] public List<string> Enumerator() => Helpers.Enumerator_Core(input);
    [Benchmark] public List<string> Yield() => Helpers.Graphemes(input).ToList();
    [Benchmark] public List<string> ParseCombining() => Helpers.ParseCombining_Core(input);
    [Benchmark] public List<string> SpanImmediate() => Helpers.SpanImmediate_Core(input);
}

результат

Method Mean Error StdDev Gen0 Allocated
Enumerator 2.009 us 0.0402 us 0.0395 us 0.8659 2.66 KB
Yield 3.504 us 0.0371 us 0.0347 us 0.8659 2.66 KB
ParseCombining 1.855 us 0.0364 us 0.0419 us 0.7114 2.18 KB
SpanImmediate 1.773 us 0.0198 us 0.0176 us 0.8297 2.55 KB
→ Ссылка