Questo ha a che fare con il modo in cui il Stringtipo funziona in Swift e come funziona il contains(_:)metodo.
'π©βπ©βπ§βπ¦' Γ¨ ciΓ² che Γ¨ noto come una sequenza emoji, che viene visualizzato come un carattere visibile in una stringa. La sequenza Γ¨ composta da Characteroggetti e allo stesso tempo Γ¨ composta da UnicodeScalaroggetti.
Se controlli il conteggio dei caratteri della stringa, vedrai che Γ¨ composto da quattro caratteri, mentre se controlli il conteggio scalare unicode, ti mostrerΓ un risultato diverso:
print("π©βπ©βπ§βπ¦".characters.count) // 4
print("π©βπ©βπ§βπ¦".unicodeScalars.count) // 7
Ora, se analizzi i personaggi e li stampi, vedrai quelli che sembrano personaggi normali, ma in realtΓ i primi tre personaggi contengono sia un'emoji che un falegname a larghezza zero nei loro 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"]
Come puoi vedere, solo l'ultimo carattere non contiene un falegname a larghezza zero, quindi quando si utilizza il contains(_:)metodo, funziona come previsto. Dal momento che non stai confrontando con emoji contenenti joiner a larghezza zero, il metodo non troverΓ una corrispondenza per nessuno tranne l'ultimo personaggio.
Per espanderlo, se crei un carattere Stringcomposto da un personaggio emoji che termina con un joiner di larghezza zero e lo passi al contains(_:)metodo, lo valuterΓ anche a false. Questo ha a che fare con l' contains(_:)essere esattamente lo stesso range(of:) != nil, che cerca di trovare una corrispondenza esatta con l'argomento dato. PoichΓ© i caratteri che terminano con un falegname a larghezza zero formano una sequenza incompleta, il metodo tenta di trovare una corrispondenza per l'argomento combinando i caratteri che terminano con un falegname a larghezza zero in una sequenza completa. CiΓ² significa che il metodo non troverΓ mai una corrispondenza se:
- l'argomento termina con un joiner di larghezza zero e
- la stringa da analizzare non contiene una sequenza incompleta (cioè termina con un joiner di larghezza zero e non seguita da un carattere compatibile).
Dimostrare:
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
Tuttavia, poichΓ© il confronto guarda solo avanti, puoi trovare diverse altre sequenze complete all'interno della stringa lavorando all'indietro:
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 soluzione piΓΉ semplice sarebbe quella di fornire un'opzione di confronto specifica al range(of:options:range:locale:)metodo. L'opzione String.CompareOptions.literalesegue il confronto su un'equivalenza esatta carattere per carattere . Come nota a margine, ciΓ² che qui si intende per carattere non Γ¨ Swift Character, ma la rappresentazione UTF-16 sia dell'istanza che della stringa di confronto - tuttavia, poichΓ© Stringnon consente UTF-16 non valido, ciΓ² equivale essenzialmente al confronto dello scalare Unicode rappresentazione.
Qui ho sovraccaricato il Foundationmetodo, quindi se hai bisogno di quello originale, rinomina questo o qualcosa del genere:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Ora il metodo funziona come "dovrebbe" con ogni personaggio, anche con sequenze incomplete:
s.contains("π©") // true
s.contains("π©\u{200d}") // true
s.contains("\u{200d}") // true