Cela a à voir avec le fonctionnement du String
type dans Swift et le fonctionnement de la contains(_:)
méthode.
Le «👩👩👧👦» est ce que l'on appelle une séquence d'emoji, qui est rendue sous la forme d'un caractère visible dans une chaîne. La séquence est composée d' Character
objets, et en même temps elle est constituée d' UnicodeScalar
objets.
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, cela vous montrera un résultat différent:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Maintenant, si vous analysez les caractères et les imprimez, 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 jointeur 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 jointeur de largeur nulle, donc lorsque vous utilisez la contains(_:)
méthode, cela fonctionne comme vous vous en doutez. Étant donné que vous ne comparez pas avec des emoji contenant des menuisiers de largeur nulle, la méthode ne trouvera pas de correspondance pour tout sauf 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 jointeur de largeur nulle et que vous le passez à la contains(_:)
méthode, il sera également évalué false
. Cela a à voir avec le fait d' contains(_:)
être exactement le même que range(of:) != nil
, qui essaie de trouver une correspondance exacte avec l'argument donné. Étant donné que les caractères se terminant par un jointeur de largeur nulle forment une séquence incomplète, la méthode essaie de trouver une correspondance pour l'argument tout en combinant les caractères se terminant par un jointeur de largeur nulle en une séquence complète. Cela signifie que la méthode ne trouvera jamais de correspondance si:
- l'argument se termine par un jointeur de largeur nulle, et
- la chaîne à analyser ne contient pas de séquence incomplète (c'est-à-dire se terminant par un jointeur de largeur nulle et non suivi 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'avenir, vous pouvez trouver plusieurs autres séquences complètes dans la chaîne en travaillant à 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 range(of:options:range:locale:)
méthode. L'option String.CompareOptions.literal
effectue la comparaison sur une équivalence exacte caractère par caractère . En guise de remarque, ce que l'on entend par caractère ici n'est pas le Swift Character
, mais la représentation UTF-16 de l'instance et de la chaîne de comparaison.Cependant, comme String
cela ne permet pas un UTF-16 malformé, cela revient essentiellement à comparer le scalaire Unicode représentation.
Ici, j'ai surchargé la Foundation
méthode, donc si vous avez besoin de l'original, renommez 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 elle le 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