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

kuhbmx9i  于 2023-02-21  发布在  Swift
关注(0)|答案(4)|浏览(189)

我有一个可以包含任意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

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

6rvt4ljy

6rvt4ljy1#

你没有尝试解决问题,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的零倍你就完了。
上面的例子显示了扩展(或不扩展)前缀猜测的情况。如果前缀猜测太长,可以使用前缀字符和字节计数而不是原始字符串的计数从头开始算法。
如果你在实现你的算法时遇到困难,问一个新的问题,展示你写的代码,描述问题,毫无疑问有人会帮助你完成下一步。
高温加热

yv5phkfx

yv5phkfx2#

我发现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的情况下,index在该字符的开始之前一个字符,在C中,index在该字符的开始之前一个字符。
在任何情况下,utf8-index现在将被保证至多为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)
    }
}
jfewjypa

jfewjypa3#

我喜欢你提出的第一个解决方案,我发现如果去掉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)
    }
}
0sgqnhkj

0sgqnhkj4#

我的解决方案如下所示:

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,它也可以工作。

相关问题