it-roy-ru.com

Работа с кодовыми точками Юникода в Swift

Если вы не интересуетесь подробностями монгольского языка, а просто хотите получить быстрый ответ об использовании и преобразовании значений Unicode в Swift, перейдите к первой части принятого ответа .


Фон

Я хочу сделать текст Unicode для традиционного монгольского для использования в приложениях iOS . Лучшее и долгосрочное решение состоит в том, чтобы использовать интеллектуальный шрифт AAT , который бы отображал этот сложный скрипт. ( Такие шрифты существуют но их лицензия не допускает модификации и не личного использования.) Однако, поскольку я никогда не делал шрифт, не говоря уже о всей логике рендеринга для шрифта AAT, я просто планирую сделать рендеринг себя в Swift на данный момент. Возможно, позже я смогу научиться делать умный шрифт. 

Внешне я буду использовать текст Unicode, но внутренне (для отображения в UITextView) я преобразую Unicode в отдельные глифы, которые хранятся в немом шрифте (закодированном с помощью Unicode PUA values). Таким образом, мой механизм рендеринга должен преобразовать значения монгольского Юникода (диапазон: от U + 1820 до U + 1842) в значения глифов, хранящиеся в PUA (диапазон: от U + E360 до U + E5CF). Во всяком случае, это мой план, так как это то, что я делал в Java в прошлом , но, возможно, мне нужно изменить весь мой образ мышления.

Пример

На следующем изображении su написано дважды на монгольском языке, используя две разные формы для буквы u (красным). (Монгольский написан вертикально с буквами, связанными как курсивные буквы на английском языке.) 

enter image description here

В Unicode эти две строки будут выражаться как 

var suForm1: String = "\u{1830}\u{1826}"
var suForm2: String = "\u{1830}\u{1826}\u{180B}"

Селектор свободных вариаций (U + 180B) в suForm2 распознается (правильно) Swift String как единица с u (U + 1826), которая предшествует ему. Свифт считает его одним символом, расширенным кластером графем. Однако для целей рендеринга мне нужно различать u (U + 1826) и FVS1 (U + 180B) как две разные кодовые точки UTF-16.

Для внутреннего отображения я бы преобразовал приведенные выше строки Unicode в следующие отрисованные строки глифа:

suForm1 = "\u{E46F}\u{E3BA}" 
suForm2 = "\u{E46F}\u{E3BB}"

Вопрос

Я играл с Swift String и Character. В них есть много удобных вещей, но, поскольку в моем конкретном случае я имею дело исключительно с блоками кода UTF-16, мне интересно, стоит ли мне использовать старую NSString, а не Swift String. Я понимаю, что могу использовать String.utf16 для получения кодовых точек UTF-16, но преобразование обратно в String не очень приятно .

Было бы лучше придерживаться String и Character или я должен использовать NSString и unichar ?

Что я прочитал

Обновления этого вопроса были скрыты, чтобы очистить страницу. Смотрите историю редактирования.

44
Suragch

Обновлено для Swift 3

Строка и Характер

Почти для всех, кто посетит этот вопрос в будущем, ответят String И Character

Установить значения Unicode непосредственно в коде:

var str: String = "I want to visit 北京, Москва, मुंबई, القاهرة, and 서울시. ????"
var character: Character = "????"

Используйте шестнадцатеричное значение для установки значений

var str: String = "\u{61}\u{5927}\u{1F34E}\u{3C0}" // a大????π
var character: Character = "\u{65}\u{301}" // é = "e" + accent mark

Обратите внимание, что символ Swift может состоять из нескольких кодовых точек Unicode, но представляется одним символом. Это называется расширенным кластером графем.

Смотрите этот вопрос также.

Преобразовать в значения Юникода:

str.utf8
str.utf16
str.unicodeScalars // UTF-32

String(character).utf8
String(character).utf16
String(character).unicodeScalars

Преобразовать из шестнадцатеричных значений Юникода:

let hexValue: UInt32 = 0x1F34E

// convert hex value to UnicodeScalar
guard let scalarValue = UnicodeScalar(hexValue) else {
    // early exit if hex does not form a valid unicode value
    return
}

// convert UnicodeScalar to String
let myString = String(scalarValue) // ????

Или в качестве альтернативы:

let hexValue: UInt32 = 0x1F34E
if let scalarValue = UnicodeScalar(hexValue) {
    let myString = String(scalarValue)
}

Еще несколько примеров

let value0: UInt8 = 0x61
let value1: UInt16 = 0x5927
let value2: UInt32 = 0x1F34E

let string0 = String(UnicodeScalar(value0)) // a
let string1 = String(UnicodeScalar(value1)) // 大
let string2 = String(UnicodeScalar(value2)) // ????

// convert hex array to String
let myHexArray = [0x43, 0x61, 0x74, 0x203C, 0x1F431] // an Int array
var myString = ""
for hexValue in myHexArray {
    myString.append(UnicodeScalar(hexValue))
}
print(myString) // Cat‼????

Обратите внимание, что для UTF-8 и UTF-16 преобразование не всегда так просто. (См. UTF-8 , UTF-16 , и UTF-32 вопросы.)

NSString и unichar

Также возможно работать с NSString и unichar в Swift, но вы должны понимать, что, если вы не знакомы с Objective C и не умеете преобразовывать синтаксис в Swift, будет трудно найти хорошую документацию. 

Кроме того, unichar - это массив UInt16, и, как уже упоминалось выше, преобразование из UInt16 в скалярные значения Unicode не всегда легко (т.е. преобразование суррогатных пар для таких вещей, как эмодзи и другие символы в верхних плоскостях кода).

Пользовательская структура строки

По причинам, указанным в вопросе, я не использовал ни один из перечисленных методов. Вместо этого я написал свою собственную строковую структуру, которая была в основном массивом UInt32 для хранения скалярных значений Unicode.

Опять же, это не решение для большинства людей. Сначала рассмотрите возможность использования extensions , если вам нужно только немного расширить функциональность String или Character

Но если вам действительно нужно работать исключительно со скалярными значениями Unicode, вы можете написать собственную структуру. 

Преимущества:

  • Не нужно постоянно переключаться между типами (String, Character, UnicodeScalar, UInt32 и т.д.) При выполнении операций со строками.
  • После того, как манипуляции с Юникодом завершены, окончательное преобразование в String легко.
  • Легко добавить больше методов, когда они необходимы
  • Упрощает преобразование кода из Java или других языков

Недостатки:

  • делает код менее переносимым и менее читаемым для других разработчиков Swift
  • не так хорошо протестированы и оптимизированы, как родные типы Swift
  • это еще один файл, который нужно включать в проект каждый раз, когда он вам нужен

Вы можете сделать свой собственный, но вот мой для справки. Самое сложное было сделать его Hashable .

// This struct is an array of UInt32 to hold Unicode scalar values
// Version 3.4.0 (Swift 3 update)


struct ScalarString: Sequence, Hashable, CustomStringConvertible {

    fileprivate var scalarArray: [UInt32] = []


    init() {
        // does anything need to go here?
    }

    init(_ character: UInt32) {
        self.scalarArray.append(character)
    }

    init(_ charArray: [UInt32]) {
        for c in charArray {
            self.scalarArray.append(c)
        }
    }

    init(_ string: String) {

        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // Generator in order to conform to SequenceType protocol
    // (to allow users to iterate as in `for myScalarValue in myScalarString` { ... })
    func makeIterator() -> AnyIterator<UInt32> {
        return AnyIterator(scalarArray.makeIterator())
    }

    // append
    mutating func append(_ scalar: UInt32) {
        self.scalarArray.append(scalar)
    }

    mutating func append(_ scalarString: ScalarString) {
        for scalar in scalarString {
            self.scalarArray.append(scalar)
        }
    }

    mutating func append(_ string: String) {
        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // charAt
    func charAt(_ index: Int) -> UInt32 {
        return self.scalarArray[index]
    }

    // clear
    mutating func clear() {
        self.scalarArray.removeAll(keepingCapacity: true)
    }

    // contains
    func contains(_ character: UInt32) -> Bool {
        for scalar in self.scalarArray {
            if scalar == character {
                return true
            }
        }
        return false
    }

    // description (to implement Printable protocol)
    var description: String {
        return self.toString()
    }

    // endsWith
    func endsWith() -> UInt32? {
        return self.scalarArray.last
    }

    // indexOf
    // returns first index of scalar string match
    func indexOf(_ string: ScalarString) -> Int? {

        if scalarArray.count < string.length {
            return nil
        }

        for i in 0...(scalarArray.count - string.length) {

            for j in 0..<string.length {

                if string.charAt(j) != scalarArray[i + j] {
                    break // substring mismatch
                }
                if j == string.length - 1 {
                    return i
                }
            }
        }

        return nil
    }

    // insert
    mutating func insert(_ scalar: UInt32, atIndex index: Int) {
        self.scalarArray.insert(scalar, at: index)
    }
    mutating func insert(_ string: ScalarString, atIndex index: Int) {
        var newIndex = index
        for scalar in string {
            self.scalarArray.insert(scalar, at: newIndex)
            newIndex += 1
        }
    }
    mutating func insert(_ string: String, atIndex index: Int) {
        var newIndex = index
        for scalar in string.unicodeScalars {
            self.scalarArray.insert(scalar.value, at: newIndex)
            newIndex += 1
        }
    }

    // isEmpty
    var isEmpty: Bool {
        return self.scalarArray.count == 0
    }

    // hashValue (to implement Hashable protocol)
    var hashValue: Int {

        // DJB Hash Function
        return self.scalarArray.reduce(5381) {
            ($0 << 5) &+ $0 &+ Int($1)
        }
    }

    // length
    var length: Int {
        return self.scalarArray.count
    }

    // remove character
    mutating func removeCharAt(_ index: Int) {
        self.scalarArray.remove(at: index)
    }
    func removingAllInstancesOfChar(_ character: UInt32) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar != character {
                returnString.append(scalar)
            }
        }

        return returnString
    }
    func removeRange(_ range: CountableRange<Int>) -> ScalarString? {

        if range.lowerBound < 0 || range.upperBound > scalarArray.count {
            return nil
        }

        var returnString = ScalarString()

        for i in 0..<scalarArray.count {
            if i < range.lowerBound || i >= range.upperBound {
                returnString.append(scalarArray[i])
            }
        }

        return returnString
    }


    // replace
    func replace(_ character: UInt32, withChar replacementChar: UInt32) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar == character {
                returnString.append(replacementChar)
            } else {
                returnString.append(scalar)
            }
        }
        return returnString
    }
    func replace(_ character: UInt32, withString replacementString: String) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar == character {
                returnString.append(replacementString)
            } else {
                returnString.append(scalar)
            }
        }
        return returnString
    }
    func replaceRange(_ range: CountableRange<Int>, withString replacementString: ScalarString) -> ScalarString {

        var returnString = ScalarString()

        for i in 0..<scalarArray.count {
            if i < range.lowerBound || i >= range.upperBound {
                returnString.append(scalarArray[i])
            } else if i == range.lowerBound {
                returnString.append(replacementString)
            }
        }
        return returnString
    }

    // set (an alternative to myScalarString = "some string")
    mutating func set(_ string: String) {
        self.scalarArray.removeAll(keepingCapacity: false)
        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // split
    func split(atChar splitChar: UInt32) -> [ScalarString] {
        var partsArray: [ScalarString] = []
        if self.scalarArray.count == 0 {
            return partsArray
        }
        var part: ScalarString = ScalarString()
        for scalar in self.scalarArray {
            if scalar == splitChar {
                partsArray.append(part)
                part = ScalarString()
            } else {
                part.append(scalar)
            }
        }
        partsArray.append(part)
        return partsArray
    }

    // startsWith
    func startsWith() -> UInt32? {
        return self.scalarArray.first
    }

    // substring
    func substring(_ startIndex: Int) -> ScalarString {
        // from startIndex to end of string
        var subArray: ScalarString = ScalarString()
        for i in startIndex..<self.length {
            subArray.append(self.scalarArray[i])
        }
        return subArray
    }
    func substring(_ startIndex: Int, _ endIndex: Int) -> ScalarString {
        // (startIndex is inclusive, endIndex is exclusive)
        var subArray: ScalarString = ScalarString()
        for i in startIndex..<endIndex {
            subArray.append(self.scalarArray[i])
        }
        return subArray
    }

    // toString
    func toString() -> String {
        var string: String = ""

        for scalar in self.scalarArray {
            if let validScalor = UnicodeScalar(scalar) {
                string.append(Character(validScalor))
            }
        }
        return string
    }

    // trim
    // removes leading and trailing whitespace (space, tab, newline)
    func trim() -> ScalarString {

        //var returnString = ScalarString()
        let space: UInt32 = 0x00000020
        let tab: UInt32 = 0x00000009
        let newline: UInt32 = 0x0000000A

        var startIndex = self.scalarArray.count
        var endIndex = 0

        // leading whitespace
        for i in 0..<self.scalarArray.count {
            if self.scalarArray[i] != space &&
                self.scalarArray[i] != tab &&
                self.scalarArray[i] != newline {

                startIndex = i
                break
            }
        }

        // trailing whitespace
        for i in stride(from: (self.scalarArray.count - 1), through: 0, by: -1) {
            if self.scalarArray[i] != space &&
                self.scalarArray[i] != tab &&
                self.scalarArray[i] != newline {

                endIndex = i + 1
                break
            }
        }

        if endIndex <= startIndex {
            return ScalarString()
        }

        return self.substring(startIndex, endIndex)
    }

    // values
    func values() -> [UInt32] {
        return self.scalarArray
    }

}

func ==(left: ScalarString, right: ScalarString) -> Bool {
    return left.scalarArray == right.scalarArray
}

func +(left: ScalarString, right: ScalarString) -> ScalarString {
    var returnString = ScalarString()
    for scalar in left.values() {
        returnString.append(scalar)
    }
    for scalar in right.values() {
        returnString.append(scalar)
    }
    return returnString
}
56
Suragch
//Swift 3.0  
// This struct is an array of UInt32 to hold Unicode scalar values
struct ScalarString: Sequence, Hashable, CustomStringConvertible {

    private var scalarArray: [UInt32] = []

    init() {
        // does anything need to go here?
    }

    init(_ character: UInt32) {
        self.scalarArray.append(character)
    }

    init(_ charArray: [UInt32]) {
        for c in charArray {
            self.scalarArray.append(c)
        }
    }

    init(_ string: String) {

        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // Generator in order to conform to SequenceType protocol
    // (to allow users to iterate as in `for myScalarValue in myScalarString` { ... })

    //func generate() -> AnyIterator<UInt32> {
    func makeIterator() -> AnyIterator<UInt32> {

        let nextIndex = 0

        return AnyIterator {
            if (nextIndex > self.scalarArray.count-1) {
                return nil
            }
            return self.scalarArray[nextIndex + 1]
        }
    }

    // append
    mutating func append(scalar: UInt32) {
        self.scalarArray.append(scalar)
    }

    mutating func append(scalarString: ScalarString) {
        for scalar in scalarString {
            self.scalarArray.append(scalar)
        }
    }

    mutating func append(string: String) {
        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // charAt
    func charAt(index: Int) -> UInt32 {
        return self.scalarArray[index]
    }

    // clear
    mutating func clear() {
        self.scalarArray.removeAll(keepingCapacity: true)
    }

    // contains
    func contains(character: UInt32) -> Bool {
        for scalar in self.scalarArray {
            if scalar == character {
                return true
            }
        }
        return false
    }

    // description (to implement Printable protocol)
    var description: String {

        var string: String = ""

        for scalar in scalarArray {
            string.append(String(describing: UnicodeScalar(scalar))) //.append(UnicodeScalar(scalar)!)
        }
        return string
    }

    // endsWith
    func endsWith() -> UInt32? {
        return self.scalarArray.last
    }

    // insert
    mutating func insert(scalar: UInt32, atIndex index: Int) {
        self.scalarArray.insert(scalar, at: index)
    }

    // isEmpty
    var isEmpty: Bool {
        get {
            return self.scalarArray.count == 0
        }
    }

    // hashValue (to implement Hashable protocol)
    var hashValue: Int {
        get {

            // DJB Hash Function
            var hash = 5381

            for i in 0 ..< scalarArray.count {
                hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
            }
            /*
             for i in 0..< self.scalarArray.count {
             hash = ((hash << 5) &+ hash) &+ Int(self.scalarArray[i])
             }
             */
            return hash
        }
    }

    // length
    var length: Int {
        get {
            return self.scalarArray.count
        }
    }

    // remove character
    mutating func removeCharAt(index: Int) {
        self.scalarArray.remove(at: index)
    }
    func removingAllInstancesOfChar(character: UInt32) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar != character {
                returnString.append(scalar: scalar) //.append(scalar)
            }
        }

        return returnString
    }

    // replace
    func replace(character: UInt32, withChar replacementChar: UInt32) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar == character {
                returnString.append(scalar: replacementChar) //.append(replacementChar)
            } else {
                returnString.append(scalar: scalar) //.append(scalar)
            }
        }
        return returnString
    }

    // func replace(character: UInt32, withString replacementString: String) -> ScalarString {
    func replace(character: UInt32, withString replacementString: ScalarString) -> ScalarString {

        var returnString = ScalarString()

        for scalar in self.scalarArray {
            if scalar == character {
                returnString.append(scalarString: replacementString) //.append(replacementString)
            } else {
                returnString.append(scalar: scalar) //.append(scalar)
            }
        }
        return returnString
    }

    // set (an alternative to myScalarString = "some string")
    mutating func set(string: String) {
        self.scalarArray.removeAll(keepingCapacity: false)
        for s in string.unicodeScalars {
            self.scalarArray.append(s.value)
        }
    }

    // split
    func split(atChar splitChar: UInt32) -> [ScalarString] {
        var partsArray: [ScalarString] = []
        var part: ScalarString = ScalarString()
        for scalar in self.scalarArray {
            if scalar == splitChar {
                partsArray.append(part)
                part = ScalarString()
            } else {
                part.append(scalar: scalar) //.append(scalar)
            }
        }
        partsArray.append(part)
        return partsArray
    }

    // startsWith
    func startsWith() -> UInt32? {
        return self.scalarArray.first
    }

    // substring
    func substring(startIndex: Int) -> ScalarString {
        // from startIndex to end of string
        var subArray: ScalarString = ScalarString()
        for i in startIndex ..< self.length {
            subArray.append(scalar: self.scalarArray[i]) //.append(self.scalarArray[i])
        }
        return subArray
    }
    func substring(startIndex: Int, _ endIndex: Int) -> ScalarString {
        // (startIndex is inclusive, endIndex is exclusive)
        var subArray: ScalarString = ScalarString()
        for i in startIndex ..< endIndex {
            subArray.append(scalar: self.scalarArray[i]) //.append(self.scalarArray[i])
        }
        return subArray
    }

    // toString
    func toString() -> String {
        let string: String = ""

        for scalar in self.scalarArray {
            string.appending(String(describing:UnicodeScalar(scalar))) //.append(UnicodeScalar(scalar)!)
        }
        return string
    }

    // values
    func values() -> [UInt32] {
        return self.scalarArray
    }

}

func ==(left: ScalarString, right: ScalarString) -> Bool {

    if left.length != right.length {
        return false
    }

    for i in 0 ..< left.length {
        if left.charAt(index: i) != right.charAt(index: i) {
            return false
        }
    }

    return true
}

func +(left: ScalarString, right: ScalarString) -> ScalarString {
    var returnString = ScalarString()
    for scalar in left.values() {
        returnString.append(scalar: scalar) //.append(scalar)
    }
    for scalar in right.values() {
        returnString.append(scalar: scalar) //.append(scalar)
    }
    return returnString
}
0
Marijan V.