Это связано с тем, как 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(_:)
метода он работает так, как вы ожидаете. Так как вы не сравниваете смайлики, содержащие соединения нулевой ширины, метод не найдет совпадения ни для одного, кроме последнего символа.
Более подробно, если вы создадите a, 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