Isso tem a ver com a forma como o String
tipo funciona no Swift e como o contains(_:)
método funciona.
O '👩👩👧👦' é conhecido como uma sequência de emoji, que é renderizada como um caractere visível em uma string. A sequência é composta de Character
objetos e, ao mesmo tempo, é composta de UnicodeScalar
objetos.
Se você verificar a contagem de caracteres da sequência, verá que ela é composta de quatro caracteres, enquanto que, se você verificar a contagem escalar unicode, mostrará um resultado diferente:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Agora, se você analisar os caracteres e imprimi-los, verá o que parece ser caracteres normais, mas, na verdade, os três primeiros caracteres contêm um emoji e um marceneiro de largura zero 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 você pode ver, apenas o último caractere não contém um marceneiro de largura zero; portanto, ao usar o contains(_:)
método, ele funciona conforme o esperado. Como você não está comparando com emoji contendo marcadores de largura zero, o método não encontrará uma correspondência para nenhum, exceto o último caractere.
Para expandir isso, se você criar um String
que é composto de um caractere emoji que termina com um marceneiro de largura zero e passá-lo para o contains(_:)
método, ele também avaliará false
. Isso tem a ver com contains(_:)
ser exatamente o mesmo que range(of:) != nil
, que tenta encontrar uma correspondência exata para o argumento fornecido. Como os caracteres que terminam com um marceneiro de largura zero formam uma sequência incompleta, o método tenta encontrar uma correspondência para o argumento enquanto combina os caracteres que terminam com marceneiros de largura zero em uma sequência completa. Isso significa que o método nunca encontrará uma correspondência se:
- o argumento termina com um marceneiro de largura zero e
- a sequência a ser analisada não contém uma sequência incompleta (ou seja, termina com um marceneiro de largura zero e não é seguida por um caractere compatível).
Para demonstrar:
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
No entanto, como a comparação apenas olha para o futuro, é possível encontrar várias outras sequências completas na sequência trabalhando de trás para frente:
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
A solução mais fácil seria fornecer uma opção de comparação específica para o range(of:options:range:locale:)
método. A opção String.CompareOptions.literal
executa a comparação com uma equivalência exata de caractere por caractere . Como observação, o significado de caractere aqui não é o Swift Character
, mas a representação UTF-16 da instância e da string de comparação - no entanto, como String
não permite UTF-16 malformado, isso é essencialmente equivalente à comparação do escalar Unicode. representação.
Aqui eu sobrecarreguei o Foundation
método, portanto, se você precisar do original, renomeie este ou algo assim:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Agora, o método funciona como "deveria" com cada caractere, mesmo com sequências incompletas:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true