【问题标题】:Swift 5: String prefix with a maximal UTF-8-lengthSwift 5:具有最大 UTF-8 长度的字符串前缀
【发布时间】:2020-06-22 00:43:07
【问题描述】:

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

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

【问题讨论】:

  • 我不确定我理解前缀是什么意思。在获得 32 字节大小之前,您可以使用替代形式的前缀(重复或替代)。您可以查看有关此类特殊情况的 unicode(例如,一种长度为 2 字节,另一种长度为 3 字节)
  • @GiacomoCatenazzi 前缀是从起始索引开始的子字符串。有许多这样的“特殊情况”,它们的字节数可能超过 3 个字节(就像在我的示例中一样?????????????????????????????? ?? 有 28 个字节)
  • @MJK:我的意思是:有各种 修饰符,长度为 2 到 4 个字节。 Unicode 建议不要有超过 16 或 32 个(现在我不记得了)代码点,但是“unicode 键盘”可以插入它们,为单个字符提供无限长度的 unicode 序列 [unicode 字符串总是可以标准化的另一个原因] .无论如何,我建议您从 Unicode 代码点开始(稍后再考虑 UTF-8 编码)。
  • 阅读答案,我仍然认为我没有得到您的要求。也许与unicode.org/reports/tr29 相关,然后通过添加将被忽略的组合代码点来进行归一化的逆操作?
  • @GiacomoCatenazzi 我不明白有什么不明白的。我想要一个带有给定字符串的前 n 个字符(即 Swift-Characters aka.ext.grapheme clusters)的前缀,以便该前缀的 UTF-8 编码最长为 32 个字节,而n 尽可能大。

标签: swift string unicode utf-8


【解决方案1】:

我喜欢您提出的第一个解决方案。如果您取出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)
    }
}

【讨论】:

  • 哦,所以即使没有 formIndex(before:) 这会自动“向下舍入”到下一个最小的扩展字素簇边界?伟大的!这么简单,我为什么不试试……
【解决方案2】:

我发现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 &lt;= maxLength) 应该很清楚,那是不需要工作的地方。
对于第二种情况,我们首先得到 utf8-index 33,即

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

因此,如果我们现在将索引向后移动一个字符(使用 formIndex(before:)),这将跳转到 index 之前的第一个扩展字素簇边界,如果 A 和 B 是之前的一个字符,而在 C 中则是它的开头性格。
无论如何,utf8-index 现在将被保证最多为32 并且位于扩展的字素簇边界,因此prefix(upTo: index) 将安全地创建一个长度≤32 的前缀。


……但这并不完美。
从理论上讲,这也应该始终是最佳解决方案,即前缀的count 尽可能接近maxLength,但有时当字符串以包含多个Unicode 标量的扩展字形簇结尾时,formIndex(before: &amp;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)
    }
}

【讨论】:

  • 首先,这说明了为什么您应该尝试自己的解决方案! ;-) 做得好。不幸的是,当您指出您的原始解决方案似乎很脆弱时(考虑将 utf8 索引传递给String 时“前身”的含义,它可能不明确)。您的第二个解决方案使用迭代的方式与使用 makeIterator 的解决方案类似,但由于复制较少,因此在效率上应该略有优势,因此“优雅”。
  • O(n) 适合小 n:更简单。否则,您应该使用unicode.org/reports/tr29/#Random_Access 检查以前的集群从哪里开始,然后继续查看是否有其他集群。所以:复杂,你需要 Unicode 数据库。
  • @GiacomoCatenazzi 我不需要手动操作,String.formIndex(…) 已经为我完成了。
【解决方案3】:

您没有尝试过解决方案,因此通常不会为您编写代码。因此,在这里为您提供一些算法建议:

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

优雅的定义是什么? (就像美丽取决于旁观者的眼睛......)

简单吗?

String.makeIterator 开头,编写一个while 循环,只要字节数≤32,将Characters 附加到您的前缀。

这是一个非常简单的循环,最糟糕的情况是 32 次迭代和 32 次追加。

“智能”搜索策略?

您可以根据String 中每个Character平均字节长度并使用String.Prefix(Int) 来实施策略。

例如对于您的第一个示例,字符数为 2,字节数为 36,平均为 18 个字节/字符,18 变为 32 一次(我们不处理小数字符或字节!)所以从 Prefix(1) 开始,它的字节数为 28,剩下 1 个字符和 8 个字节 - 所以剩余部分的平均字节长度为 8,您最多再寻找 4 个字节,8 进入 4 个零次,然后就完成了。

上面的例子展示了扩展(或不扩展)你的前缀猜测的情况。如果您的前缀猜测太长,您可以使用前缀字符和字节数而不是原始字符串从头开始算法。

如果您在实现算法时遇到问题,请提出一个新问题来显示您编写的代码,描述问题,毫无疑问有人会帮助您进行下一步。

HTH

【讨论】:

  • 谢谢,这些都是很好的建议!但我认为我找到了一个更“优雅”(我想我会定义为最大限度地简化效率)的解决方案。看我的回答。
猜你喜欢
  • 1970-01-01
  • 2017-09-05
  • 1970-01-01
  • 2019-05-03
  • 1970-01-01
  • 2016-11-26
  • 1970-01-01
  • 1970-01-01
  • 2019-07-30
相关资源
最近更新 更多