web-dev-qa-db-fra.com

Trancher une chaîne contenant des caractères Unicode

J'ai un morceau de texte avec des caractères de longueur différente.

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

J'ai besoin de prendre une tranche des indices de caractères de début (inclus) et de fin (exclus) donnés. J'ai essayé

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

et a obtenu l'erreur suivante

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

Je suppose que cela se produit car les lettres cyrilliques sont multi-octets et la notation [..] Prend des caractères en utilisant les index byte. Que puis-je utiliser si je souhaite découper en utilisant des index caractère, comme je le fais en Python:

slice = text[start:end]?

Je sais que je peux utiliser l'itérateur chars() et parcourir manuellement la sous-chaîne souhaitée, mais existe-t-il un moyen plus concis?

20
Sasha Tsukanov

Solutions possibles au découpage de point de code

Je sais que je peux utiliser l'itérateur chars() et parcourir manuellement la sous-chaîne souhaitée, mais existe-t-il un moyen plus concis?

Si vous connaissez les indices d'octets exacts, vous pouvez découper une chaîne:

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

Ceci imprime "llo пр". Le problème est donc de trouver la position exacte des octets. Vous pouvez le faire assez facilement avec l'itérateur char_indices() (vous pouvez également utiliser chars() avec char::len_utf8()):

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

Comme autre alternative, vous pouvez d'abord collecter la chaîne dans Vec<char>. Ensuite, l'indexation est simple, mais pour l'imprimer sous forme de chaîne, vous devez la collecter à nouveau ou écrire votre propre fonction pour le faire.

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

Pourquoi n'est-ce pas plus facile?

Comme vous pouvez le voir, aucune de ces solutions n'est si bonne. Ceci est intentionnel, pour deux raisons:

Comme str est simplement un tampon UTF8, l'indexation par points de code unicode est une opération O(n). Habituellement, les gens s'attendent à ce que l'opérateur [] Soit une opération O(1). Rust rend cette complexité d'exécution explicite et n'essaie pas de la cacher. Dans les deux solutions ci-dessus, vous pouvez clairement voir que ce n'est pas O (1).

Mais la raison la plus importante:

Les points de code Unicode ne sont généralement pas une unité utile

Ce que Python fait (et ce que vous pensez que vous voulez) n'est pas du tout utile. Tout se résume à la complexité du langage et donc à la complexité de l'unicode. Python tranches Unicode codepoints. C'est ce que représente un Rust char. C'est 32 bits gros (quelques bits de moins suffiraient, mais nous arrondissons à une puissance de 2).

Mais ce que vous voulez réellement faire, c'est découper les personnages perçus par les utilisateurs. Mais c'est un terme défini de façon explicite. Différentes cultures et langues considèrent différentes choses comme "un seul personnage". L'approximation la plus proche est une "grappe de graphèmes". Un tel cluster peut être constitué d'un ou plusieurs points de code unicode. Considérez ce code Python 3:

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

Étonnant, non? En effet, la chaîne ci-dessus est:

  • 0x004A LETTRE MAJUSCULE LATINE J
  • 0x0075 LETTRE MINUSCULE LATINE U
  • 0x0308 COMBINAISON DE DIAERESIS
  • ...

Ceci est un exemple de caractère de combinaison qui est rendu dans le cadre du caractère précédent. Python le découpage fait la "mauvaise" chose ici.

Un autre exemple:

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

Ce n'est pas non plus ce à quoi vous vous attendez. Cette fois, fi est en fait la ligature , Qui est un point de code.

Il y a beaucoup plus d'exemples où Unicode se comporte de manière surprenante. Voir les liens en bas pour plus d'informations et d'exemples.

Donc, si vous voulez travailler avec des chaînes internationales qui devraient pouvoir fonctionner partout, ne faites pas de découpage de point de code! Si vous avez vraiment besoin de visualiser sémantiquement la chaîne comme une série de caractères, utilisez des grappes de graphèmes. Pour ce faire, la caisse unicode-segmentation est très utile.


Autres ressources sur ce sujet:

24
Lukas Kalbertodt

Une chaîne codée UTF-8 peut contenir des caractères, qui se composent de plusieurs octets. Dans votre cas, п Commence à l'index 6 (inclus) et se termine à la position 8 (exclusif), donc l'indexation 7 n'est pas le début du caractère. C'est pourquoi votre erreur s'est produite.

Vous pouvez utiliser str::char_indices pour résoudre ce problème (rappelez-vous que se positionner en UTF-8 est 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])
    })
}

aire de jeux

Vous pouvez utiliser str::chars() si vous êtes d'accord pour obtenir un String:

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