Esto tiene que ver con cómo funciona el String
tipo en Swift y cómo funciona el contains(_:)
método.
La '👩👩👧👦' es lo que se conoce como una secuencia de emoji, que se representa como un carácter visible en una cadena. La secuencia está compuesta de Character
objetos, y al mismo tiempo está compuesta de UnicodeScalar
objetos.
Si verifica el recuento de caracteres de la cadena, verá que está formado por cuatro caracteres, mientras que si verifica el recuento escalar unicode, le mostrará un resultado diferente:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Ahora, si analiza los caracteres e los imprime, verá lo que parecen caracteres normales, pero de hecho, los tres primeros caracteres contienen tanto un emoji como un carpintero de ancho cero en su 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"]
Como puede ver, solo el último carácter no contiene una unión de ancho cero, por lo que cuando usa el contains(_:)
método, funciona como es de esperar. Como no se compara con los emoji que contienen uniones de ancho cero, el método no encontrará una coincidencia para ningún otro personaje que no sea el último.
Para ampliar esto, si crea uno String
que está compuesto por un carácter emoji que termina con una unión de ancho cero y lo pasa al contains(_:)
método, también se evaluará false
. Esto tiene que ver con contains(_:)
ser exactamente igual que range(of:) != nil
, que trata de encontrar una coincidencia exacta con el argumento dado. Dado que los caracteres que terminan con una unión de ancho cero forman una secuencia incompleta, el método intenta encontrar una coincidencia para el argumento mientras combina caracteres que terminan con uniones de ancho cero en una secuencia completa. Esto significa que el método nunca encontrará una coincidencia si:
- el argumento termina con una unión de ancho cero, y
- la cadena a analizar no contiene una secuencia incompleta (es decir, que termina con una unión de ancho cero y no seguida de un carácter compatible).
Demostrar:
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
Sin embargo, dado que la comparación solo mira hacia adelante, puede encontrar varias otras secuencias completas dentro de la cadena trabajando hacia atrás:
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 solución más fácil sería proporcionar una opción de comparación específica para el range(of:options:range:locale:)
método. La opción String.CompareOptions.literal
realiza la comparación en una equivalencia exacta de carácter por carácter . Como nota al margen, lo que se entiende por carácter aquí no es Swift Character
, sino la representación UTF-16 de la cadena de instancia y de comparación; sin embargo, dado String
que no permite UTF-16 con formato incorrecto, esto es esencialmente equivalente a comparar el escalar Unicode representación.
Aquí he sobrecargado el Foundation
método, así que si necesitas el original, renombra este o algo así:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Ahora el método funciona como "debería" con cada personaje, incluso con secuencias incompletas:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true