Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to write a Swift function that replaces only part of an extended grapheme cluster like ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง?

I want to write a function that could be used like this:

let ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ = "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ง".replacingFirstOccurrence(of: "๐Ÿ‘ง", with: "๐Ÿ‘ฆ")

Given how odd both this string and Swift's String library are, is this possible in Swift?

like image 674
Ky. Avatar asked Apr 25 '17 19:04

Ky.


People also ask

What is extended grapheme cluster?

An extended grapheme cluster is a group of one or more Unicode scalar values that approximates a single user-perceived character. Many individual characters, such as โ€œรฉโ€, โ€œ๊น€โ€, and โ€œ๐Ÿ‡ฎ๐Ÿ‡ณโ€, can be made up of multiple Unicode scalar values.

What is a grapheme character in Swift?

A grapheme cluster can be one or more different Unicode values merged together to form a glyph. A character in Swift is a grapheme cluster, not a Unicode value. And the same cluster can be represented in different ways. This is called canonical equivalence.


1 Answers

Based on the insights gained at Why are emoji characters like ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ treated so strangely in Swift strings?, a sensible approach might be to replace Unicode scalars:

extension String {
    func replacingFirstOccurrence(of target: UnicodeScalar, with replacement: UnicodeScalar) -> String {

        let uc = self.unicodeScalars
        guard let idx = uc.index(of: target) else { return self }
        let prefix = uc[uc.startIndex..<idx]
        let suffix = uc[uc.index(after: idx) ..< uc.endIndex]
        return "\(prefix)\(replacement)\(suffix)"
    }
}

Example:

let family1 = "๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆ"
print(family1.characters.map { Array(String($0).unicodeScalars) })
// [["\u{0001F469}", "\u{200D}"], ["\u{0001F469}", "\u{200D}"], ["\u{0001F467}", "\u{200D}"], ["\u{0001F466}"]]

let family2 = family1.replacingFirstOccurrence(of: "๐Ÿ‘ง", with: "๐Ÿ‘ฆ")
print(family2) // ๐Ÿ‘ฉโ€๐Ÿ‘ฉโ€๐Ÿ‘ฆโ€๐Ÿ‘ฆ
print(family2.characters.map { Array(String($0).unicodeScalars) })
// [["\u{0001F469}", "\u{200D}"], ["\u{0001F469}", "\u{200D}"], ["\u{0001F466}", "\u{200D}"], ["\u{0001F466}"]]

And here is a possible version which locates and replaces the Unicode scalars of an arbitrary string:

extension String {
    func replacingFirstOccurrence(of target: String, with replacement: String) -> String {
        let uc = self.unicodeScalars
        let tuc = target.unicodeScalars

        // Target empty or too long:
        if tuc.count == 0 || tuc.count > uc.count {
            return self
        }

        // Current search position:
        var pos = uc.startIndex
        // Last possible position of `tuc` within `uc`:
        let end = uc.index(uc.endIndex, offsetBy: tuc.count - 1)

        // Locate first Unicode scalar
        while let from = uc[pos..<end].index(of: tuc.first!) {
            // Compare all Unicode scalars:
            let to = uc.index(from, offsetBy: tuc.count)
            if !zip(uc[from..<to], tuc).contains(where: { $0 != $1 }) {
                let prefix = uc[uc.startIndex..<from]
                let suffix = uc[to ..< uc.endIndex]
                return "\(prefix)\(replacement)\(suffix)"
            }
            // Next search position:
            uc.formIndex(after: &pos)
        }

        // Target not found.
        return self
    }
}
like image 89
Martin R Avatar answered Oct 24 '22 22:10

Martin R