Question Pourquoi les personnages d'emoji comme 👩👩👧👦 sont-ils si étrangement traités dans les cordes Swift?


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

U+1F469  WOMAN,
‍U+200D  ZWJ,
U+1F469  WOMAN,
U+200D  ZWJ,
U+1F467  GIRL,
U+200D  ZWJ,
U+1F466  BOY

Donc, il est très intéressant de coder; la cible parfaite 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

Ainsi, Swift dit qu'il se contient (bon) et un garçon (bon!). Mais il dit alors qu'il ne contient pas une femme, une fille, ou un menuisier de largeur nulle. Qu'est-ce qu'il se passe ici? Pourquoi Swift sait-il qu'il contient un garçon mais pas une femme ou une fille? Je pourrais comprendre si elle le traitait comme un seul caractère et ne le reconnaissait que par elle-même, mais le fait qu'il y ait une sous-composante et pas d'autres me déroute.

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


La confusion est encore plus grande:

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 ZWJs là-dedans, ils ne sont pas reflétés dans le tableau de caractères. Ce qui a suivi était un peu révélateur:

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

Donc, je reçois le même comportement avec le tableau de caractères ... ce qui est suprêmement ennuyeux, puisque je sais à quoi ressemble le tableau.

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


473
2018-04-25 18:36


origine


Réponses:


Cela a à voir avec la façon dont String type fonctionne dans Swift, et comment le contains(_:) méthode fonctionne.

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

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

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

Maintenant, si vous parcourez les caractères et les imprimez, vous verrez ce qui ressemble à 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 voir, seul le dernier caractère ne contient pas de joint de largeur zéro, donc lorsque vous utilisez le contains(_:) méthode, cela fonctionne comme vous l'attendez. Comme vous ne comparez pas avec les emoji contenant des jointures de largeur nulle, la méthode ne trouvera pas de correspondance avec le dernier caractère.

Pour développer cela, si vous créez un String qui est composé d'un caractère emoji se terminant par un menuisier de largeur zéro, et le passe au contains(_:) méthode, il évaluera également false. Cela a à voir avec contains(_:) être le même que range(of:) != nil, qui essaie de trouver une correspondance exacte avec l'argument donné. Puisque les caractères se terminant par un menuisier de largeur nulle forment une séquence incomplète, la méthode essaie de trouver une correspondance pour l'argument tout en combinant des caractères se terminant par des menuisiers 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 zéro, et
  2. la chaîne à analyser ne contient pas une séquence incomplète (c'est-à-dire se terminant par un jointeur de largeur zéro 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, puisque la comparaison ne regarde que vers l'avant, vous pouvez trouver plusieurs autres séquences complètes dans la chaîne en retraversant:

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 consisterait à fournir une option de comparaison spécifique range(of:options:range:locale:) méthode. L'option String.CompareOptions.literal effectue la comparaison sur un Équivalence exacte caractère par caractère. En guise de note, que signifie le caractère ici est ne pas le Swift Character, mais la représentation UTF-16 de l'instance et de la chaîne de comparaison - cependant, depuis String ne permet pas UTF-16 mal formé, cela équivaut essentiellement à comparer la représentation scalaire Unicode.

Ici j'ai surchargé le Foundation méthode, donc si vous avez besoin de l'original, renommer celui-ci ou quelque chose:

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

Maintenant, la méthode fonctionne comme "devrait" 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

361
2018-04-25 19:12



Le premier problème est que vous établissez un lien avec Foundation avec contains (Swift's String n'est pas un Collection), Alors ceci est NSString comportement, que je ne crois pas manipule Emoji composé aussi puissamment que Swift. Cela dit, je crois que Swift est en train d'implémenter Unicode 8, ce qui nécessitait également une révision autour de cette situation dans Unicode 10 (cela peut donc changer quand ils implémentent Unicode 10;

Pour simplifier, débarrassons-nous de Foundation, et utilisons Swift, qui fournit des vues plus explicites. Nous allons commencer par les caractères:

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

D'ACCORD. C'est ce que nous attendions. Mais c'est un mensonge. Voyons voir 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 ... Donc 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 membre.

Le problème est que Character est un "cluster de graphèmes", qui compose les choses ensemble (comme attacher le ZWJ). Ce que vous cherchez vraiment, c'est un scalaire unicode. Et cela fonctionne exactement comme vous l'attendez:

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

Et bien sûr, nous pouvons également rechercher le caractère réel qui s'y trouve:

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

(Cela fait largement double emploi avec les points de Ben Leggiero. J'ai posté ceci avant de me rendre compte qu'il aurait répondu.


100
2018-04-25 19:24



Il semble que Swift considère un ZWJ être un graphe étendu de graphèmes avec le caractère qui le précède immédiatement. Nous pouvons le voir en mappant le tableau de caractères à leur unicodeScalars:

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

Ceci imprime ce qui suit 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}"

Aditionellement, .contains les groupes ont étendu les grappes de graphèmes en un seul caractère. Par exemple, en prenant les caractères hangul , , et  (qui se combinent pour faire le mot coréen pour "un": 한):

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

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


69
2018-04-25 19:10



Les autres réponses parlent de ce que fait Swift, mais ne détaillent pas pourquoi.

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

L'un d'eux est une lettre avec un combineur, l'autre est un caractère composé unique. Vous pouvez ajouter de nombreux combiners différents à un personnage de base, et un humain considérerait toujours qu'il s'agit d'un seul caractère. Pour faire face à ce type de divergence, le concept de graphème a été créé pour représenter ce qu'un humain considère comme un caractère, quels que soient les points de code utilisés.

Depuis des années, les services de messagerie texte combinent les caractères en emoji graphique. :). Donc, divers emoji ont été ajoutés à Unicode.
Ces services ont également commencé à combiner emoji ensemble en emoji composite.
Il n'y a bien sûr aucune manière raisonnable d'encoder toutes les combinaisons possibles dans des points de code individuels. Le Consortium Unicode a donc décidé d'étendre le concept de graphèmes pour englober ces caractères composites.

Ce qui se résume à est "‍‍‍" devrait être considéré comme un seul "cluster de graphèmes" si vous essayez de travailler avec lui au niveau du graphème, comme le fait par défaut Swift.

Si vous voulez vérifier s'il contient "" dans le cadre de cela, alors vous devriez descendre à un niveau inférieur.


Je ne connais pas la syntaxe de Swift donc voici Perl 6 qui a un niveau de support similaire pour Unicode.
(Perl 6 prend en charge la version Unicode 9, il peut donc y avoir 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 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 Swift rend cela plus facile, mais cela ne signifie pas qu'il n'y a pas d'autres choses qui deviennent plus difficiles.

Travailler à ce niveau rend beaucoup plus facile la division accidentelle d'une chaîne au milieu d'un caractère composite, par exemple.


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

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


15
2018-04-27 19:45



Mise à jour Swift 4.0

La chaîne reçoit beaucoup de révisions dans la mise à jour 4 rapide, comme documenté dans SE-0163. Deux emoji sont utilisés pour cette démo 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 jointeur de largeur zéro connecté. Le format est ‍joiner‍joiner‍joiner

1. Compte

En swift 4.0. emoji est compté comme grappe de graphèmes. Chaque emoji unique compte pour 1. La propriété count est également directement disponible pour string. Vous pouvez donc 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 comptabilisé comme des grappes de graphèmes dans swift 4.0, et les deux codes suivants impriment 1. Ces deux emoji sont des exemples de séquences emoji, où plusieurs emoji sont combinés avec ou sans jointeur de largeur zéro. \u{200d} entre eux. Dans swift 3.0, le tableau de caractères d'une telle chaîne sépare chaque emoji et donne un tableau avec plusieurs éléments (emoji). Le menuisier est ignoré dans ce processus. Cependant, dans swift 4.0, le tableau de caractères considère 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

En 4.0 rapide, contains La méthode ignore le jointeur de largeur zéro dans emoji. Donc, il retourne vrai pour l'un des quatre composants emoji de "‍‍‍"et renvoyez false si vous recherchez le menuisier. Cependant, dans swift 3.0, le menuisier n'est pas ignoré et est combiné avec l'emoji devant lui. Donc, quand vous vérifiez si "‍‍‍" contient les trois premiers emoji composant, 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

10
2017-07-13 01:51