Вырезать строку, содержащую символы Unicode

У меня есть фрагмент текста с символами разных байтов.

let text = "Hello привет";

Мне нужно взять фрагмент строки с указанием начальных (включенных) и конечных (исключенных) символов. Я попробовал это

let slice = &text[start..end];

и получил следующую ошибку

thread 'main' panicked at 'byte index 7 is not a char boundary; it is inside 'п' (bytes 6..8) of 'Hello привет''

Я предполагаю, что это происходит, поскольку кириллические буквы являются многобайтовыми, а нотация [..] принимает символы, использующие байт-индексы. Что я могу использовать, если я хочу срезать с использованием символьных индексов, например, я делаю в Python:

slice = text[start:end]?

Я знаю, что могу использовать итератор chars() и вручную пройти через нужную подстроку, но есть ли более сжатый способ?

Ответы

Ответ 1

Возможные решения для нарезки кода

Я знаю, что могу использовать итератор chars() и вручную пройти через нужную подстроку, но есть ли более сжатый способ?

Если вы знаете точные байтовые индексы, вы можете нарезать строку:

let text = "Hello привет";
println!("{}", &text[2..10]);

Это печатает "llo pr". Поэтому проблема заключается в том, чтобы узнать точное положение байта. Вы можете сделать это довольно легко с char_indices() итератора char_indices() (char_indices() вы можете использовать char::len_utf8() chars() с char::len_utf8()):

let text = "Hello привет";
let end = text.char_indices().map(|(i, _)| i).nth(8).unwrap();
println!("{}", &text[2..idx]);

В качестве альтернативы вы можете сначала собрать строку в Vec<char>. Затем индексирование прост, но чтобы напечатать его как строку, вам нужно снова собрать его или написать свою собственную функцию, чтобы сделать это.

let text = "Hello привет";
let text_vec = text.chars().collect::<Vec<_>>();
println!("{}", text_vec[2..8].iter().cloned().collect::<String>());

Почему это не так просто?

Как вы можете видеть, ни одно из этих решений не очень велико. Это преднамеренно по двум причинам:

Поскольку str является просто буфером UTF8, индексирование по кодовым точкам unicode является операцией O (n). Обычно люди ожидают, что оператор [] будет выполнять операцию O (1). Rust делает эту сложность выполнения явной и не пытается скрыть ее. В обоих вышеизложенных решениях вы можете ясно видеть, что это не O (1).

Но более важная причина:

Кодовые обозначения Unicode обычно не являются полезной единицей

Что делает Python (и то, что вы считаете нужным) не так уж и полезно. Все сводится к сложности языка и, следовательно, сложности юникода. Питонные фрагменты Unicode. Это то, что Руст char представляет. Это 32-битный бит (было немного меньше бит, но мы округлились до мощности 2).

Но то, что вы на самом деле хотите сделать, - это срезать воспринимаемые пользователем персонажи. Но это явно явно определенный термин. Различные культуры и языки рассматривают разные вещи как "один персонаж". Ближайшим приближением является "кластер графем". Такой кластер может состоять из одного или нескольких кодовых точек Unicode. Рассмотрим этот код Python 3:

>>> s = "Jürgen"
>>> s[0:2]
'Ju'

Удивительно, правда? Это связано с тем, что строка выше:

  • 0x004A ПИСЬМО LATIN CAPITAL J
  • 0x0075 LATIN SMALL ПИСЬМО U
  • 0x0308 КОМБИНИРОВАННАЯ ДИАЕРЕЗИЯ
  • ...

Это пример комбинирующего символа, который отображается как часть предыдущего символа. Нарезка Python делает здесь "неправильную" вещь.

Другой пример:

>>> s = "fire"
>>> s[0:2]
'fir'

Также не то, что вы ожидаете. На этот раз fi фактически является лигатурой , которая является одной кодовой точкой.

Есть гораздо больше примеров, когда Unicode ведет себя удивительным образом. См. Ссылки внизу для получения дополнительной информации и примеров.

Поэтому, если вы хотите работать с международными строками, которые должны работать повсюду, не делайте codepoint slicing! Если вам действительно нужно семантически рассматривать строку как последовательность символов, используйте кластеры grapheme. Для этого очень удобна unicode-segmentation ящика.


Дополнительные ресурсы по этой теме:

Ответ 2

Закодированная строка UTF-8 может содержать символы, состоящие из нескольких байтов. В вашем случае п начинается с индекса 6 (включительно) и заканчивается в позиции 8 (исключая), поэтому индексирование 7 не является началом символа. Вот почему произошла ваша ошибка.

Вы можете использовать str::char_indices для решения этого вопроса (помните, что получение позиции в UTF-8 равно O(n)):

fn get_utf8_slice(string: &str, start: usize, end: usize) -> Option<&str> {
    assert!(end >= start);
    string.char_indices().nth(start).and_then(|(start_pos, _)| {
        string[start_pos..]
            .char_indices()
            .nth(end - start + 1)
            .map(|(end_pos, _)| &string[start_pos..end_pos])
    })
}

детская площадка

Вы можете использовать str::chars() если у вас все получится с помощью String:

let string: String = text.chars().take(end).skip(start).collect();