web-dev-qa-db-fra.com

Pourquoi les caractères emoji comme ????‍????‍????‍???? sont-ils traités si étrangement dans les chaînes Swift?

Le personnage???? (famille avec deux femmes, une fille et un garçon) est codé comme suit:

U+1F469WOMAN ,
‍U+200DZWJ ,
U+1F469WOMAN,
U+200DZWJ,
U+1F467GIRL ,
U+200DZWJ,
U+1F466BOY

Donc, il est très intéressant d'encoder; la cible idéale pour un test unitaire. Cependant, Swift ne semble pas savoir comment le traiter. Voici ce que je veux dire:

"????‍????‍????‍????".contains("????‍????‍????‍????") // true
"????‍????‍????‍????".contains("????") // false
"????‍????‍????‍????".contains("\u{200D}") // false
"????‍????‍????‍????".contains("????") // false
"????‍????‍????‍????".contains("????") // true

Donc, Swift dit qu'il se contient (bon) et un garçon (bon!). Mais il dit ensuite qu'il ne contient pas de femme, de fille ou de menuisier de largeur nulle. Que se passe-t-il ici? Pourquoi Swift sait-il qu'il contient un garçon mais pas une femme ni une fille? Je pourrais comprendre s'il le traitait comme un seul personnage et ne le reconnaissait le fait qu’il ait un sous-composant et aucun autre ne me dérange.

Cela ne change pas si j'utilise quelque chose comme "????".characters.first!.


Encore plus déconcertant est le suivant:

let manual = "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
Array(manual.characters) // ["????‍", "????‍", "????‍", "????"]

Même si j'ai placé les ZWJ dedans, ils ne sont pas reflétés dans le tableau de caractères. Ce qui suivit fut un peu révélateur:

manual.contains("????") // false
manual.contains("????") // false
manual.contains("????") // true

Donc, je rencontre le même comportement avec le tableau de caractères ... ce qui est extrêmement agaçant, car je sais à quoi ressemble le tableau.

Cela ne change pas non plus si j'utilise quelque chose comme "????".characters.first!.

519
Ben Leggiero

Cela a à voir avec le fonctionnement du type String dans Swift et du fonctionnement de la méthode contains(_:).

Le???? 'est ce qu'on appelle une séquence emoji, qui est restituée sous la forme d'un caractère visible dans une chaîne. La séquence est composée d'objets Character et, en même temps, d'objets UnicodeScalar.

Si vous vérifiez le nombre de caractères de la chaîne, vous verrez qu'elle est composée de quatre caractères, tandis que si vous vérifiez le nombre scalaire unicode, le résultat obtenu sera différent:

print("????‍????‍????‍????".characters.count)     // 4
print("????‍????‍????‍????".unicodeScalars.count) // 7

Maintenant, si vous analysez et imprimez les caractères, vous verrez ce qui semble être des caractères normaux, mais en fait, les trois premiers caractères contiennent à la fois un emoji et un menuisier de largeur nulle dans leur UnicodeScalarView:

for char in "????‍????‍????‍????".characters {
    print(char)

    let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) })
    print(scalars)
}

// ????‍
// ["1f469", "200d"]
// ????‍
// ["1f469", "200d"]
// ????‍
// ["1f467", "200d"]
// ????
// ["1f466"]

Comme vous pouvez le constater, seul le dernier caractère ne contient pas de jointure de largeur nulle. Par conséquent, lorsque vous utilisez la méthode contains(_:), il fonctionne comme prévu. Etant donné que vous ne comparez pas avec des jointures emoji contenant une largeur nulle, la méthode ne trouve de correspondance que pour le dernier caractère.

Pour développer ceci, si vous créez un String qui est composé d'un caractère emoji se terminant par un jointeur de largeur nulle, et le transmettez à la méthode contains(_:), il sera également évalué à false. Ceci est dû au fait que contains(_:) est exactement identique à range(of:) != nil, qui tente de trouver une correspondance exacte avec l'argument donné. Comme les caractères se terminant par un jointeur de largeur nulle forment une séquence incomplète, la méthode tente de trouver une correspondance pour l'argument tout en combinant des caractères se terminant par des jointeurs de largeur nulle en une séquence complète. Cela signifie que la méthode ne trouvera jamais de correspondance si:

  1. l'argument se termine par un menuisier de largeur nulle et
  2. la chaîne à analyser ne contient pas une séquence incomplète (c'est-à-dire se terminant par un menuisier de largeur nulle et non suivie d'un caractère compatible).

Démontrer:

let s = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // ????‍????‍????‍????

s.range(of: "\u{1f469}\u{200d}") != nil                            // false
s.range(of: "\u{1f469}\u{200d}\u{1f469}") != nil                   // false

Cependant, étant donné que la comparaison ne regarde que vers l'avenir, vous pouvez trouver plusieurs autres séquences complètes dans la chaîne en procédant à l'envers:

s.range(of: "\u{1f466}") != nil                                    // true
s.range(of: "\u{1f467}\u{200d}\u{1f466}") != nil                   // true
s.range(of: "\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil  // true

// Same as the above:
s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}")          // true

La solution la plus simple serait de fournir une option de comparaison spécifique à la méthode range(of:options:range:locale:) . L'option String.CompareOptions.literal effectue la comparaison sur une équivalence exacte . En note de côté, ce que l’on entend ici par caractère est et non pas le Swift Character, mais la représentation UTF-16 de l’instance. et chaîne de comparaison - toutefois, étant donné que String n'autorise pas le format UTF-16 mal formé, cela revient essentiellement à comparer la représentation scalaire Unicode.

Ici, j'ai surchargé la méthode Foundation, donc si vous avez besoin de l'originale, renommez celle-ci ou quelque chose comme ça:

extension String {
    func contains(_ string: String) -> Bool {
        return self.range(of: string, options: String.CompareOptions.literal) != nil
    }
}

Maintenant, la méthode fonctionne comme il se doit avec chaque caractère, même avec des séquences incomplètes:

s.contains("????")          // true
s.contains("????\u{200d}")  // true
s.contains("\u{200d}")    // true
390
xoudini

Le premier problème est que vous établissez un lien avec Foundation avec contains (le nom de Swift String n’est pas un Collection), il s’agit donc du comportement NSString, dont je ne crois pas qu’il gère Emoji aussi puissamment que Swift. Cela dit, Swift je crois est en train d'implémenter Unicode 8 à l'heure actuelle, ce qui nécessitait également une révision de cette situation dans Unicode 10 (afin que tout puisse changer quand ils implémentent Unicode 10; je ne me suis pas demandé si cela allait ou pas).

Pour simplifier les choses, supprimons Foundation et utilisons Swift, qui fournit des vues plus explicites. Nous allons commencer avec les personnages:

"????‍????‍????‍????".characters.forEach { print($0) }
????‍
????‍
????‍
????

D'ACCORD. C'est ce à quoi nous nous attendions. Mais c'est un mensonge. Voyons ce que sont vraiment ces personnages.

"????‍????‍????‍????".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
["\u{0001F469}", "\u{200D}"]
["\u{0001F469}", "\u{200D}"]
["\u{0001F467}", "\u{200D}"]
["\u{0001F466}"]

Ah… Alors c'est ["????ZWJ", "????ZWJ", "????ZWJ", "????"]. Cela rend tout un peu plus clair. ???? n'est pas un membre de cette liste (c'est "???? ZWJ"), mais ???? est un membre.

Le problème est que Character est un "cluster de graphèmes", qui compose des éléments (comme l’attachement du ZWJ). Ce que vous recherchez vraiment, c'est un scalaire unicode. Et cela fonctionne exactement comme vous le souhaitiez:

"????‍????‍????‍????".unicodeScalars.contains("????") // true
"????‍????‍????‍????".unicodeScalars.contains("\u{200D}") // true
"????‍????‍????‍????".unicodeScalars.contains("????") // true
"????‍????‍????‍????".unicodeScalars.contains("????") // true

Et bien sûr, nous pouvons également rechercher le personnage réel qui y figure:

"????‍????‍????‍????".characters.contains("????\u{200D}") // true

(Cela duplique énormément les points de Ben Leggiero. Je l'ai posté avant de remarquer qu'il avait répondu. Partir au cas où ce serait plus clair pour quiconque.)

106
Rob Napier

Il semble que Swift considère qu'un ZWJ soit un cluster de graphèmes étendu dont le caractère le précède immédiatement. Nous pouvons le voir en mappant le tableau de caractères sur leur unicodeScalars:

Array(manual.characters).map { $0.description.unicodeScalars }

Cela imprime ce qui suit à partir de LLDB:

▿ 4 elements
  ▿ 0 : StringUnicodeScalarView("????‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"
  ▿ 1 : StringUnicodeScalarView("????‍")
    - 0 : "\u{0001F469}"
    - 1 : "\u{200D}"
  ▿ 2 : StringUnicodeScalarView("????‍")
    - 0 : "\u{0001F467}"
    - 1 : "\u{200D}"
  ▿ 3 : StringUnicodeScalarView("????")
    - 0 : "\u{0001F466}"

De plus, .contains regroupe les grappes de graphèmes étendues en un seul caractère. Par exemple, en prenant les caractères hangul , et (qui se combinent pour créer le mot coréen pour "un": 한):

"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false

Cela n'a pas permis de trouver parce que les trois points de code sont regroupés dans un cluster qui agit comme un seul caractère. De même, \u{1F469}\u{200D} (WOMANZWJ) est un cluster qui agit comme un caractère.

74
Ben Leggiero

Mise à jour de Swift 4.0

String reçoit de nombreuses révisions dans la mise à jour Swift 4, comme indiqué dans la section SE-016 . Deux emoji sont utilisés pour cette démonstration représentant deux structures différentes. Les deux sont combinés avec une séquence d'emoji.

???????? est la combinaison de deux emoji, ???? et ????

????‍????‍????‍???? est la combinaison de quatre emoji, avec un menuisier de largeur zéro connecté. Le format est ????‍joiner????‍joiner????‍joiner????

1. Compte

Dans Swift 4.0. emoji est compté comme grappe de graphèmes. Chaque emoji est compté pour 1. La propriété count est également directement disponible pour string. Donc, vous pouvez l'appeler directement comme ça.

"????????".count  // 1. Not available on Swift 3
"????‍????‍????‍????".count // 1. Not available on Swift 3

Le tableau de caractères d'une chaîne est également compté en tant que grappes de graphèmes dans Swift 4.0; les deux codes suivants sont donc imprimés 1. Ces deux emoji sont des exemples de séquences emoji, dans lesquelles plusieurs emoji sont combinés avec ou sans largeur nulle. menuisier \u{200d} entre eux. Dans Swift 3.0, un tableau de caractères d'une telle chaîne sépare chaque emoji et génère un tableau avec plusieurs éléments (emoji). Le menuisier est ignoré dans ce processus. Cependant, dans Swift 4.0, le tableau de caractères voit tous les emoji comme une seule pièce. Donc, celui de n'importe quel emoji sera toujours 1.

"????????".characters.count  // 1. In Swift 3, this prints 2
"????‍????‍????‍????".characters.count // 1. In Swift 3, this prints 4

unicodeScalars reste inchangé dans Swift 4. Il fournit les caractères Unicode uniques dans la chaîne donnée.

"????????".unicodeScalars.count  // 2. Combination of two emoji
"????‍????‍????‍????".unicodeScalars.count // 7. Combination of four emoji with joiner between them

2. Contient

Dans Swift 4.0, la méthode contains ignore le menuisier de largeur nulle dans emoji. Donc, il retourne vrai pour l’un des quatre composants emoji de "????‍????‍????‍????", et retourne faux si vous recherchez le participant. Cependant, dans Swift 3.0, le menuisier n'est pas ignoré et est combiné à l'emoji qui le précède. Ainsi, lorsque vous vérifiez si "????‍????‍????‍????" contient les trois premiers composants emoji, le résultat sera faux

"????????".contains("????")       // true
"????????".contains("????")       // true
"????‍????‍????‍????".contains("????‍????‍????‍????")      // true
"????‍????‍????‍????".contains("????")      // true. In Swift 3, this prints false
"????‍????‍????‍????".contains("\u{200D}") // false
"????‍????‍????‍????".contains("????")      // true. In Swift 3, this prints false
"????‍????‍????‍????".contains("????")      // true
18
Fangming

Les autres réponses traitent de ce que Swift fait, mais n'entrez pas dans les détails pour savoir pourquoi.

Vous attendez-vous à ce que "Å" soit égal à "Å"? Je pense que tu le ferais.

L'une d'elles est une lettre avec un combinateur, l'autre est un caractère composé unique. Vous pouvez ajouter de nombreux combineurs différents à un caractère de base, et un humain considérerait toujours qu'il s'agit d'un caractère unique. Pour traiter ce type de divergence, le concept de graphème a été créé pour représenter ce que l’être humain considérerait comme un personnage, quels que soient les codes utilisés.

Maintenant, les services de messagerie texte combinent des caractères dans un emoji graphique depuis des années :)????. Donc, divers emoji ont été ajoutés à Unicode.
Ces services ont également commencé à combiner des emoji en un emoji composite.
Il n’existe bien entendu aucun moyen raisonnable de coder toutes les combinaisons possibles en points de code individuels. Le Consortium Unicode a donc décidé de développer le concept de graphèmes pour englober ces caractères composites.

Cela revient à "????‍????‍????‍????" devrait être considéré comme un seul "cluster de graphèmes" si vous essayez de le manipuler au niveau du graphème, comme le fait Swift par défaut.

Si vous voulez vérifier s'il contient "????", vous devriez alors descendre à un niveau inférieur.


Je ne connais pas la syntaxe Swift, voici donc quelques Perl 6 qui ont un niveau de support similaire pour Unicode.
(Perl 6 prend en charge la version 9 de Unicode, ce qui peut entraîner des divergences)

say "\c[family: woman woman girl boy]" eq "????‍????‍????‍????"; # True

# .contains is a Str method only, in Perl 6
say "????‍????‍????‍????".contains("????‍????‍????‍????")    # True
say "????‍????‍????‍????".contains("????");        # False
say "????‍????‍????‍????".contains("\x[200D]");  # False

# comb with no arguments splits a Str into graphemes
my @graphemes = "????‍????‍????‍????".comb;
say @graphemes.elems;                # 1

Descendons d'un niveau

# look at it as a list of NFC codepoints
my @components := "????‍????‍????‍????".NFC;
say @components.elems;                     # 7

say @components.grep("????".ord).Bool;       # True
say @components.grep("\x[200D]".ord).Bool; # True
say @components.grep(0x200D).Bool;         # True

Descendre à ce niveau peut cependant rendre certaines choses plus difficiles.

my @match = "????‍????‍????‍????".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True

Je suppose que .contains dans Swift facilite les choses, mais cela ne signifie pas qu'il n'y a pas d'autres choses qui deviennent plus difficiles.

Travailler à ce niveau facilite beaucoup le fractionnement accidentel d'une chaîne au milieu d'un caractère composite, par exemple.


Ce que vous demandez par inadvertance est de savoir pourquoi cette représentation de niveau supérieur ne fonctionne pas comme une représentation de niveau inférieur. La réponse est bien sûr, ce n'est pas censé.

Si vous vous demandez "pourquoi cela doit-il être si compliqué", la réponse est bien sûr "humain".

18
Brad Gilbert

Les Emojis, un peu comme le standard Unicode, sont trompeusement compliqués. Les tonalités de peau, les genres, les tâches, les groupes de personnes, les séquences de jointure de largeur nulle, les drapeaux (unicode à 2 caractères) et d'autres complications peuvent compliquer l'analyse syntaxique d'emoji. Un arbre de Noël, une part de pizza ou une pile de caca peuvent tous être représentés avec un seul point de code Unicode. Sans oublier que lorsque de nouveaux émojis sont introduits, il existe un délai entre le support iOS et la version emoji. Cela et le fait que différentes versions d'iOS prennent en charge différentes versions du standard Unicode.

TL; DR. J'ai travaillé sur ces fonctionnalités et ouvert la bibliothèque d'une source dont je suis l'auteur pour JKEmoji afin d'aider à l'analyse des chaînes. avec des emojis. Cela rend l'analyse aussi simple que:

print("I love these emojis ????‍????‍????‍????????????????????????????".emojiCount)

5

Pour ce faire, il actualise régulièrement une base de données locale de tous les émojis reconnus à partir de la dernière version unicode ( 12. récemment) et en les comparant avec ce qui est reconnu comme un emoji valide dans la version en cours d'exécution du système d'exploitation. en regardant la représentation bitmap d'un caractère emoji non reconnu.

NOTE

Une réponse précédente a été supprimée pour la publicité de ma bibliothèque sans indiquer clairement que je suis l'auteur. Je le reconnais encore.

1
Joe