Swift countElements() возвращает неверное значение, когда флаг count emoji

let str1 = "🇩🇪🇩🇪🇩🇪🇩🇪🇩🇪"
let str2 = "🇩🇪.🇩🇪.🇩🇪.🇩🇪.🇩🇪."

println("\(countElements(str1)), \(countElements(str2))")

Результат: 1, 10

Но не должно быть str1 иметь 5 элементов?

Ошибка появляется, только когда я использую флаг emoji.

Ответ 1

Обновление для Swift 4 (Xcode 9)

По состоянию на Swift 4 (протестирован с Xcode 9 beta) кластеры grapheme ломаются после каждого второго регионального символа индикатора, как это предусмотрено Unicode 9 стандарт:

let str1 = "🇩🇪🇩🇪🇩🇪🇩🇪🇩🇪"
print(str1.count) // 5
print(Array(str1)) // ["🇩🇪", "🇩🇪", "🇩🇪", "🇩🇪", "🇩🇪"]

Также String представляет собой набор его символов (снова), поэтому можно получить число символов с помощью str1.count.


(Старый ответ для Swift 3 и старше:)

Из "3 Графические границы кластера" в "Стандартном приложении № 29 UNICODE TEXT SEGMENTATION": (выделено мной):

Унаследованный кластер grapheme определяется как база (например, A или カ) за которыми следуют ноль или более продолжающихся символов. Один из способов думать это как последовательность символов, которые образуют "стек".

База может быть одиночным символом или быть любой последовательностью Хангыл-Джамо символы, которые образуют хангыльский слог, как определено D133 в The Unicode Standard, или любую последовательность символов Regional_Indicator (RI). Символы RI используются парами, чтобы обозначить Emoji национальные символы флагов, соответствующие кодам стран ISO. Последовательности более двух символов RI должны быть разделены другими символами, таких как U + 200B ZWSP.

(Спасибо @rintaro за ссылку).

Свифт-символ представляет собой расширенный кластер графем, поэтому он (согласно к этой ссылке) исправить, что любая последовательность региональных символов индикатора считается единственным символом.

Вы можете отделить "флаги" от ZERO WIDTH NON-JOINER:

let str1 = "🇩🇪\u{200C}🇩🇪"
print(str1.characters.count) // 2

или вставьте пробел ZERO WIDTH SPACE:

let str2 = "🇩🇪\u{200B}🇩🇪"
print(str2.characters.count) // 3

Это решает также возможные двусмысленности, например. должен "🇫 🇷 🇺 🇸" "🇫 🇷🇺 🇸" или "🇫🇷 🇺🇸"?

См. также Как узнать, будут ли отображаться две emojis как один emoji? о возможном методе для подсчета количества "скомпонованных символов" в строке Swift, который вернет 5 для вашего let str1 = "🇩🇪🇩🇪🇩🇪🇩🇪🇩🇪".

Ответ 2

Вот как я решил эту проблему, для Swift 3:

let str = "🇩🇪🇩🇪🇩🇪🇩🇪🇩🇪" //or whatever the string of emojis is
let range = str.startIndex..<str.endIndex
var length = 0
str.enumerateSubstrings(in: range, options: NSString.EnumerationOptions.byComposedCharacterSequences) { (substring, substringRange, enclosingRange, stop) -> () in
        length = length + 1
    }
print("Character Count: \(length)")

Это устраняет все проблемы с количеством символов и emojis, и это самый простой метод, который я нашел.

Ответ 3

Как упоминается в Documentation:

Обратите также внимание, что число символов, возвращаемое countElements, не всегда совпадает с значением длины NSString, содержащим одни и те же символы. Длина NSString основана на числе 16-разрядных кодовых блоков в представлении строк UTF-16, а не на количестве Unicode расширенных кластеров графем в строке. Чтобы отразить этот факт, свойство length из NSString называется utf16Count, когда к нему обращается значение Swift String.

То, что вы используете с помощью CountElements, - это количество расширенных кластеров графема Unicode. Поскольку str1 имеет все одинаковые символы эможи, функция CountElements возвращает 1. Если вы хотите, чтобы фактическая длина строки пыталась выполнить utf16Count.

println ( "(str1.utf16Count), (str2.utf16Count)" )

EDIT:

Обратите внимание, что вычисление длины строки требует итерации по всем символам и, следовательно, является операцией O (N). Причина этого в том, что для разных символов требуются переменные объемы памяти для хранения. Хотя наиболее часто используемые символы вписываются в 16 или даже 8 бит, другим, например, emoji, требуется 32 бита2, а хранилище, требуемое для кластера grapheme, теоретически неограниченно, поскольку базовый символ может иметь неограниченное сочетание меток.

Итак, чтобы рассчитать фактическую длину:

var length = 0
for char in str1 {
    length = length + 1
}
println(length)