Це пов'язано з тим, як String
працює тип Swift та як contains(_:)
працює метод.
'👩👩👧👦' називається послідовністю смайликів, яка відображається як один видимий символ у рядку. Послідовність складається з Character
предметів, і в той же час вона складається з UnicodeScalar
предметів.
Якщо ви перевірите кількість символів рядка, ви побачите, що він складається з чотирьох символів, тоді як якщо ви перевірите скалярний підрахунок унікоду, він покаже вам інший результат:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
Тепер, якщо проаналізувати символи та роздрукувати їх, ви побачите те, що здається нормальним символом, але насправді три перші символи містять як емоцій, так і столяр нульової ширини у своїх 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"]
Як бачимо, тільки останній символ не містить столярної ширини нульової ширини, тому при використанні contains(_:)
методу він працює так, як ви очікували. Оскільки ви не порівнюєте з емоджи, що містить столяри нульової ширини, метод не знайде відповідності для будь-якого, крім останнього символу.
Щоб розширити цю проблему, якщо ви створите String
елемент, який складається з символу смайлів, що закінчується приєднувачем нульової ширини, і передати його contains(_:)
методу, він також оцінить false
. Це стосуєтьсяcontains(_:)
тим, що точно такий же, як і range(of:) != nil
, який намагається знайти точну відповідність даному аргументу. Оскільки символи, що закінчуються приєднувачем нульової ширини, утворюють неповну послідовність, метод намагається знайти відповідність аргументу, поєднуючи символи, що закінчуються нульовою шириною приєднувачів, у повну послідовність. Це означає, що метод ніколи не знайде відповідність, якщо:
- аргумент закінчується приєднувачем нульової ширини та
- рядок для розбору не містить неповної послідовності (тобто закінчується столером нульової ширини і не супроводжується символом, сумісним).
Демонструвати:
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
Однак, оскільки порівняння дивиться лише вперед, ви можете знайти кілька інших повних послідовностей всередині рядка, працюючи назад:
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
Найпростішим рішенням буде надання конкретного варіанту порівняння з range(of:options:range:locale:)
методом. Параметр String.CompareOptions.literal
виконує порівняння за точною еквівалентністю символів за символами . Як бічне зауваження, що тут мається на увазі під символом не Swift Character
, а представлення UTF-16 як рядка екземпляра, так і порівняння, однак, оскільки String
не допускає неправильно сформованого UTF-16, це по суті еквівалентно порівнянню скаляра Unicode представництво.
Тут я перевантажив Foundation
метод, тому якщо вам потрібен оригінальний, перейменуйте цей або щось таке:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
Тепер метод працює так, як "повинен" з кожним символом, навіть із неповними послідовностями:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true