Swift 5:最大UTF-8长度的字符串前缀

t5zmwmid  于 2022-11-21  发布在  Swift
关注(0)|答案(4)|浏览(178)

我有一个可以包含任意Unicode字符的字符串,我想获得该字符串的前缀,其UTF-8编码长度尽可能接近32字节,同时仍然是有效的UTF-8,并且不改变字符的含义(即不切断扩展的字形簇)。
请考虑以下正确的示例:

let string = "\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}\u{1F1EA}\u{1F1FA}"
print(string)                    // 🏴󠁧󠁢󠁳󠁣󠁴󠁿🇪🇺
print(string.count)              // 2
print(string.utf8.count)         // 36

let prefix = string.utf8Prefix(32)  // <-- function I want to implement 
print(prefix)                    // 🏴󠁧󠁢󠁳󠁣󠁴󠁿
print(prefix.count)              // 1
print(prefix.utf8.count)         // 28

print(string.hasPrefix(prefix))  // true

下面是一个错误实现的示例:

let string = "ar\u{1F3F4}\u{200D}\u{2620}\u{FE0F}\u{1F3F4}\u{200D}\u{2620}\u{FE0F}\u{1F3F4}\u{200D}\u{2620}\u{FE0F}"
print(string)                    // ar🏴‍☠️🏴‍☠️🏴‍☠️
print(string.count)              // 5
print(string.utf8.count)         // 41

let prefix = string.wrongUTF8Prefix(32)  // <-- wrong implementation 
print(prefix)                    // ar🏴‍☠️🏴‍☠️🏴
print(prefix.count)              // 5
print(prefix.utf8.count)         // 32

print(string.hasPrefix(prefix))  // false

有什么优雅的方法可以做到这一点?(除了试错法之外)

qnakjoqk

qnakjoqk1#

你没有尝试解决问题,SO通常不会为你写代码,所以这里有一些算法建议:
有什么优雅的方法可以做到这一点?(除了试错法之外)
什么是“优雅”?(就像美取决于旁观者的眼光一样......)

  • 简单吗 *

String.makeIterator开始,写一个while循环,只要字节数≤ 32,就在前缀后面加上Character s。
这是一个非常简单的循环,最坏的情况是32次迭代和32次追加。

  • “智能”搜索策略?*

您可以根据String中每个Character的 * 平均 * 字节长度并使用String.Prefix(Int)来实现策略。
例如,在第一个示例中,字符数为2,字节数为36,平均为18字节/字符,18只进入32一次(我们不处理小数字符或字节!)所以从Prefix(1)开始,其具有28的字节计数并且留下1个字符和8个字节-所以剩余部分具有8的平均字节长度并且您正在寻找最多4个字节,8除以4的零次,你就完成了。
上面的例子显示了扩展(或不扩展)前缀猜测的情况。如果前缀猜测太长,可以使用前缀字符和字节计数而不是原始字符串的计数从头开始算法。
如果你在实现算法时遇到困难,可以提出一个新的问题,展示你所写的代码,描述问题,毫无疑问,有人会帮助你完成下一步。
高温

kcrjzv8t

kcrjzv8t2#

我发现StringString.UTF8View共享相同的索引,所以我设法创建了一个非常简单(和有效?)的解决方案,我认为:

extension String {
    func utf8Prefix(_ maxLength: Int) -> Substring {
        if self.utf8.count <= maxLength {
            return Substring(self)
        }

        var index = self.utf8.index(self.startIndex, offsetBy: maxLength+1)
        self.formIndex(before: &index)
        return self.prefix(upTo: index)
    }
}

解释(假设maxLength == 32startIndex == 0):

第一种情况(utf8.count <= maxLength)应该很清楚,这是不需要工作的地方。
对于第二种情况,我们首先得到utf8索引33,它可以是

  • A:字符串的endIndex(如果它正好是33个字节长),
  • B:字符开头的索引(在前一个字符的33个字节之后)
  • C:字符中间某处的索引(在前面字符的〈33字节之后)

因此,如果我们现在将我们的索引向后移动一个字符(具有formIndex(before:)),则这将跳到index之前的第一扩展的文法簇边界,其在情况A和B中是该字符之前的一个字符,并且在C中是该字符的开始。
在任何情况下,utf8索引现在将被保证至多为32,并且在扩展的字素簇边界处,因此prefix(upTo: index)将安全地创建长度≤32的前缀。
“但并不完美"
理论上,这也应该是最优的解决方案,即前缀的count尽可能地接近maxLength,但有时当字符串以包含多个Unicode标量的扩展字素簇结尾时,formIndex(before: &index)会比需要的多后退一个字符,因此前缀结束时会更短。我不太清楚为什么会这样。
一个不那么优雅但完全“正确”的解决方案是这样的(仍然只有O(n)):

extension String {
    func utf8Prefix(_ maxLength: Int) -> Substring {
        if self.utf8.count <= maxLength {
            return Substring(self)
        }

        let endIndex = self.utf8.index(self.startIndex, offsetBy: maxLength)
        var index = self.startIndex
        while index <= endIndex {
            self.formIndex(after: &index)
        }
        self.formIndex(before: &index)
        return self.prefix(upTo: index)
    }
}
xqk2d5yq

xqk2d5yq3#

我喜欢你提出的第一个解决方案。我发现如果去掉formIndex,它会更正确(也更简单)地工作:

extension String {
    func utf8Prefix(_ maxLength: Int) -> Substring {
        if self.utf8.count <= maxLength {
            return Substring(self)
        }

        let index = self.utf8.index(self.startIndex, offsetBy: maxLength)
        return self.prefix(upTo: index)
    }
}
9nvpjoqh

9nvpjoqh4#

我的解决方案如下所示:

extension String {
    func prefix(maxUTF8Length: Int) -> String {
        if self.utf8.count <= maxUTF8Length { return self }
        var utf8EndIndex = self.utf8.index(self.utf8.startIndex, offsetBy: maxUTF8Length)
        while utf8EndIndex > self.utf8.startIndex {
            if let stringIndex = utf8EndIndex.samePosition(in: self) {
                return String(self[..<stringIndex])
            } else {
                self.utf8.formIndex(before: &utf8EndIndex)
            }
        }
        return ""
    }
}

它取可能的最大utf8索引,使用Index.samePosition(in:)方法检查它是否是有效的字符索引。如果不是,它逐个减少utf8索引,直到找到有效的字符索引。
优点是你可以用utf16代替utf8,它也会起作用。

相关问题