ΒΏPor quΓ© los caracteres emoji como π©βπ©βπ§βπ¦ son tratados de manera tan extraΓ±a en las cadenas Swift?
string unicode (6)
El primer problema es que estás conectando a Foundation con
contains
(la
String
de Swift no es una
Collection
), por lo que este es el comportamiento de
NSString
, que no creo que maneje Emoji compuesto tan poderosamente como Swift.
Dicho esto, Swift creo que está implementando Unicode 8 en este momento, que también necesitaba revisión en torno a esta situación en Unicode 10 (por lo que todo esto puede cambiar cuando implementan Unicode 10; no he investigado si lo hará o no).
Para simplificar, eliminemos Foundation y usemos Swift, que proporciona vistas más explícitas. Comenzaremos con los personajes:
"π©π©π§π¦".characters.forEach { print($0) }
π©
π©
π§
π¦
OKAY. Eso es lo que esperábamos. Pero es mentira. Veamos cuáles son esos personajes realmente.
"π©π©π§π¦".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
["/u{0001F469}", "/u{200D}"]
["/u{0001F469}", "/u{200D}"]
["/u{0001F467}", "/u{200D}"]
["/u{0001F466}"]
Ah ... Entonces es
["π©ZWJ", "π©ZWJ", "π§ZWJ", "π¦"]
.
Eso deja todo un poco más claro.
π© no es miembro de esta lista (es "π©ZWJ"), pero π¦ es miembro.
El problema es que
Character
es un "grupo de grafemas" que compone cosas juntas (como adjuntar el ZWJ).
Lo que realmente estás buscando es un escalar unicode.
Y eso funciona exactamente como esperabas:
"π©π©π§π¦".unicodeScalars.contains("π©") // true
"π©π©π§π¦".unicodeScalars.contains("/u{200D}") // true
"π©π©π§π¦".unicodeScalars.contains("π§") // true
"π©π©π§π¦".unicodeScalars.contains("π¦") // true
Y, por supuesto, también podemos buscar el personaje real que está allí:
"π©π©π§π¦".characters.contains("π©/u{200D}") // true
(Esto duplica en gran medida los puntos de Ben Leggiero. Publiqué esto antes de notar que había respondido. Partir en caso de que sea más claro para alguien).
El carácter π©π©π§π¦ (familia con dos mujeres, una niña y un niño) está codificado como tal:
U+1F469
WOMAN
,
U+200D
ZWJ
,
U+1F469
WOMAN
,
U+200D
ZWJ
,
U+1F467
GIRL
,
U+200D
ZWJ
,
U+1F466
BOY
Entonces está muy interesantemente codificado; El blanco perfecto para una prueba unitaria. Sin embargo, Swift no parece saber cómo tratarlo. Esto es lo que quiero decir:
"π©π©π§π¦".contains("π©π©π§π¦") // true
"π©π©π§π¦".contains("π©") // false
"π©π©π§π¦".contains("/u{200D}") // false
"π©π©π§π¦".contains("π§") // false
"π©π©π§π¦".contains("π¦") // true
Entonces, Swift dice que se contiene a sí mismo (bueno) y a un niño (¡bueno!). Pero luego dice que no contiene una mujer, una niña o un carpintero de ancho cero. ¿Que esta pasando aqui? ¿Por qué Swift sabe que contiene un niño pero no una mujer o una niña? Podía entender si lo trataba como un solo personaje y solo reconocía que se contenía a sí mismo, pero el hecho de que tuviera un subcomponente y ningún otro me desconcierta.
Esto no cambia si uso algo como
"π©".characters.first!
.
Aún más confuso es esto:
let manual = "/u{1F469}/u{200D}/u{1F469}/u{200D}/u{1F467}/u{200D}/u{1F466}"
Array(manual.characters) // ["π©", "π©", "π§", "π¦"]
Aunque coloqué los ZWJ allí, no se reflejan en la matriz de caracteres. Lo que siguió fue un poco revelador:
manual.contains("π©") // false
manual.contains("π§") // false
manual.contains("π¦") // true
Entonces obtengo el mismo comportamiento con la matriz de caracteres ... lo cual es sumamente molesto, ya que sé cómo se ve la matriz.
Esto tampoco cambia si uso algo como
"π©".characters.first!
.
Esto tiene que ver con cómo funciona el tipo
String
en Swift y cómo funciona el método
contains(_:)
.
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 por objetos
Character
y, al mismo tiempo, está compuesta por objetos
UnicodeScalar
.
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 un carpintero de ancho cero, por lo que cuando usa el método
contains(_:)
, 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 una
String
que se compone de un carácter emoji que termina con una unión de ancho cero y la pasa al método
contains(_:)
, también se evaluará como
false
.
Esto tiene que ver con
contains(_:)
es exactamente el mismo que el
range(of:) != nil
, que intenta 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 al
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 el
Character
Swift, sino la representación UTF-16 tanto de la instancia como de la cadena de comparación; sin embargo, dado que
String
no permite UTF-16 con formato incorrecto, esto es esencialmente equivalente a comparar el Representación escalar Unicode.
Aquí he sobrecargado el método
Foundation
, 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
Las otras respuestas discuten lo que hace Swift, pero no entran en muchos detalles sobre por qué.
¿Espera que "Å" sea igual a "Å"? Espero que lo hagas.
Una de ellas es una letra con un combinador, la otra es un único personaje compuesto. Puedes agregar muchos combinadores diferentes a un personaje base, y un humano aún lo consideraría como un personaje único. Para lidiar con este tipo de discrepancia, se creó el concepto de grafema para representar lo que un humano consideraría un personaje, independientemente de los puntos de código utilizados.
Ahora los servicios de mensajes de texto han estado combinando caracteres en emoji gráficos durante años
:)
→
π
.
Así que se agregaron varios emoji a Unicode.
Estos servicios también comenzaron a combinar emoji en emoji compuesto.
Por supuesto, no hay una forma razonable de codificar todas las combinaciones posibles en puntos de código individuales, por lo que El Consorcio Unicode decidió ampliar el concepto de grafemas para abarcar estos caracteres compuestos.
Lo que se reduce a
"π©π©π§π¦"
debe considerarse como un solo "grupo de grafemas" si intenta trabajar con él a nivel de grafema, como lo hace Swift de manera predeterminada.
Si desea verificar si contiene
"π¦"
como parte de eso, entonces debe bajar a un nivel inferior.
No conozco la sintaxis de Swift, así que aquí hay un Perl 6 que tiene un nivel similar de soporte para Unicode.
(Perl 6 admite la versión 9 de Unicode, por lo que puede haber discrepancias)
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
Bajemos un nivel
# 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
Sin embargo, bajar a este nivel puede hacer que algunas cosas sean más difíciles.
my @match = "π©π©π§π¦".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True
Supongo que
.contains
en Swift lo hace más fácil, pero eso no significa que no haya otras cosas que se vuelvan más difíciles.
Trabajar en este nivel hace que sea mucho más fácil dividir accidentalmente una cadena en el medio de un carácter compuesto, por ejemplo.
Lo que inadvertidamente pregunta es por qué esta representación de nivel superior no funciona como lo haría una representación de nivel inferior. La respuesta es, por supuesto, no se supone que lo haga.
Si se pregunta " por qué esto tiene que ser tan complicado ", la respuesta es, por supuesto, " humanos ".
Los emojis, al igual que el estándar Unicode, son engañosamente complicados. Los tonos de piel, los géneros, los trabajos, los grupos de personas, las secuencias de carpintería de ancho cero, las banderas (2 caracteres unicode) y otras complicaciones pueden hacer que el análisis de emojis sea desordenado. Un árbol de Navidad, una rebanada de pizza o una pila de caca se pueden representar con un único punto de código Unicode. Sin mencionar que cuando se introducen nuevos emojis, hay un retraso entre el soporte de iOS y el lanzamiento de emoji. Eso y el hecho de que diferentes versiones de iOS admiten diferentes versiones del estándar Unicode.
TL; DR. Trabajé en estas características y abrí una biblioteca. Soy el autor de JKEmoji para ayudar a analizar cadenas con emojis. Hace que el análisis sea tan fácil como:
print("I love these emojis π©π©π§π¦πͺπΎπ§₯π§πΏπ".emojiCount)
5 5
Lo hace al actualizar rutinariamente una base de datos local de todos los emojis reconocidos a partir de la última versión Unicode ( 12.0 fecha reciente) y hacer una referencia cruzada de ellos con lo que se reconoce como un emoji válido en la versión del sistema operativo en ejecución mirando la representación de mapa de bits de Un personaje emoji no reconocido.
NOTA
Se eliminó una respuesta anterior por anunciar mi biblioteca sin indicar claramente que yo soy el autor. Estoy reconociendo esto nuevamente.
Parece que Swift considera que un
ZWJ
es un grupo de grafemas extendido con el personaje inmediatamente anterior.
Podemos ver esto al asignar la matriz de caracteres a sus
unicodeScalars
:
Array(manual.characters).map { $0.description.unicodeScalars }
Esto imprime lo siguiente 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}"
Además,
.contains
agrupa grupos de grafemas extendidos en un solo carácter.
Por ejemplo, tomando los caracteres hangul
α
,
α
‘
y
α«
(que se combinan para formar la palabra coreana para "uno":
αα
‘α«
):
"/u{1112}/u{1161}/u{11AB}".contains("/u{1112}") // false
Esto no pudo encontrar
α
porque los tres puntos de código se agrupan en un grupo que actúa como un solo carácter.
Del mismo modo,
/u{1F469}/u{200D}
(
WOMAN
ZWJ
) es un clúster, que actúa como un carácter.
Actualización de Swift 4.0
La cadena recibe muchas revisiones en la actualización rápida 4, como se documenta en SE-0163 . Se utilizan dos emoji para esta demostración que representan dos estructuras diferentes. Ambos se combinan con una secuencia de emoji.
ππ½
es la combinación de dos emoji,
π
y
π½
π©π©π§π¦
es la combinación de cuatro emoji, con carpintero de ancho cero conectado.
El formato es
π©joinerπ©joinerπ§joinerπ¦
1. Cuenta
En swift 4.0. emoji se cuenta como grupo de grafemas. Cada emoji se cuenta como 1. La propiedad de conteo también está directamente disponible para la cadena. Entonces puedes llamarlo directamente así.
"ππ½".count // 1. Not available on swift 3
"π©π©π§π¦".count // 1. Not available on swift 3
La matriz de caracteres de una cadena también se cuenta como grupos de grafemas en swift 4.0, por lo que se imprimen los dos códigos siguientes 1. Estos dos emoji son ejemplos de secuencias de emoji, donde varios emoji se combinan con o sin unión de ancho cero
/u{200d}
entre ellos.
En swift 3.0, la matriz de caracteres de dicha cadena separa cada emoji y da como resultado una matriz con múltiples elementos (emoji).
El carpintero se ignora en este proceso.
Sin embargo, en swift 4.0, la matriz de caracteres ve todos los emoji como una sola pieza.
Entonces, el de cualquier emoji siempre será 1.
"ππ½".characters.count // 1. In swift 3, this prints 2
"π©π©π§π¦".characters.count // 1. In swift 3, this prints 4
unicodeScalars
permanece sin cambios en swift 4. Proporciona los caracteres únicos Unicode en la cadena dada.
"ππ½".unicodeScalars.count // 2. Combination of two emoji
"π©π©π§π¦".unicodeScalars.count // 7. Combination of four emoji with joiner between them
2. Contiene
En swift 4.0, el método
contains
ignora la unión de ancho cero en emoji.
Por lo tanto, devuelve verdadero para cualquiera de los cuatro componentes emoji de
"π©π©π§π¦"
, y devuelve falso si verifica la unión.
Sin embargo, en swift 3.0, el carpintero no se ignora y se combina con el emoji frente a él.
Entonces, cuando verifique si
"π©π©π§π¦"
contiene los primeros tres componentes emoji, el resultado será falso
"ππ½".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