从任何UTF-16偏移量中,找到位于Character边界上的相应String.Index

Ole*_*ann 5 string unicode swift swift4

我的目标:给定UTF-16中的任意位置String,找到String.Index代表Character指定UTF-16代码单元一部分的(即扩展字素簇)对应的内容。

例:

(我将代码放入要点中,以便于复制和粘贴。)

这是我的测试字符串:

let str = "?"
Run Code Online (Sandbox Code Playgroud)

(注意:要将字符串视为单个字符,您需要在相当新的OS /浏览器组合上阅读此字符串,该组合可以处理Unicode 9中引入的带有皮肤色调的新职业表情符号。)

它是一个Character(字素群集),由四个Unicode标量或7个UTF-16代码单元组成:

print(str.unicodeScalars.map { "0x\(String($0.value, radix: 16))" })
// ? ["0x1f468", "0x1f3fe", "0x200d", "0x1f692"]
print(str.utf16.map { "0x\(String($0, radix: 16))" })
// ? ["0xd83d", "0xdc68", "0xd83c", "0xdffe", "0x200d", "0xd83d", "0xde92"]
print(str.utf16.count)
// ? 7
Run Code Online (Sandbox Code Playgroud)

给定任意UTF-16偏移量(例如2),我可以创建一个对应的String.Index

let utf16Offset = 2
let utf16Index = String.Index(encodedOffset: utf16Offset)
Run Code Online (Sandbox Code Playgroud)

我可以使用该索引对字符串进行下标,但是如果索引不位于Character边界上,Character则下标返回的值可能不会覆盖整个字素簇:

let char = str[utf16Index]
print(char)
// ? ?
print(char.unicodeScalars.map { "0x\(String($0.value, radix: 16))" })
// ? ["0x1f3fe", "0x200d", "0x1f692"]
Run Code Online (Sandbox Code Playgroud)

否则下标操作甚至可能会陷阱(我不确定这是否是预期的行为):

let trappingIndex = String.Index(encodedOffset: 1)
str[trappingIndex]
// fatal error: Can't form a Character from a String containing more than one extended grapheme cluster
Run Code Online (Sandbox Code Playgroud)

您可以测试索引是否落在Character边界上:

extension String.Index {
    func isOnCharacterBoundary(in str: String) -> Bool {
        return String.Index(self, within: str) != nil
    }
}

trappingIndex.isOnCharacterBoundary(in: str)
// ? false (as expected)
utf16Index.isOnCharacterBoundary(in: str)
// ? true (WTF!)
Run Code Online (Sandbox Code Playgroud)

问题:

我认为问题在于最后一个表达式返回了true 文档String.Index.init(_:within:)说明:

如果传递的索引as sourcePosition表示扩展字素簇的开始(字符串的元素类型),则初始化程序成功。

在这里,utf16Index它不代表扩展的字素簇的开始-字素簇从偏移量0开始,而不是偏移量2。但是初始化程序成功。

结果,我通过重复递减索引值encodedOffset和测试来找到字素簇开始的所有尝试都isOnCharacterBoundary失败了。

我在俯视什么吗?还有另一种方法可以测试索引是否落在a的开头Character吗?这是Swift中的错误吗?

我的环境:macOS 10.13上的Swift 4.0 / Xcode 9.0。

更新:查看有关此问题的有趣的Twitter主题

更新:我将String.Index.init?(_:within:)Swift 4.0中的行为报告为错误:SR-5992

Mar*_*n R 5

可能的解决方案,使用rangeOfComposedCharacterSequence(at:)\n方法:

\n\n
extension String {\n    func index(utf16Offset: Int) -> String.Index? {\n        guard utf16Offset >= 0 && utf16Offset < utf16.count else { return nil }\n        let idx = String.Index(encodedOffset: utf16Offset)\n        let range = rangeOfComposedCharacterSequence(at: idx)\n        return range.lowerBound\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

例子:

\n\n
let str = "a\xe2\x80\x8dbcd\xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8de"\nfor utf16Offset in 0..<str.utf16.count {\n    if let idx = str.index(utf16Offset: utf16Offset) {\n        print(utf16Offset, str[idx])\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

输出:

\n\n
\n0 a\n1 \xe2\x80\x8d\n2 \xe2\x80\x8d\n3 \xe2\x80\x8d\n4 \xe2\x80\x8d\n5 \xe2\x80\x8d\n6 \xe2\x80\ x8d\n7 \xe2\x80\x8d\n8 b\n9 \n10 \n11 \n12 \n13 c\n14 \n15 \n16 d\n17 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d \n18 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n19 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n20 \xe2\x80\x8d\xe2 \x80\x8d\xe2\x80\x8d\n21 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n22 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d \n23 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n24 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n25 \xe2\x80\x8d\xe2 \x80\x8d\xe2\x80\x8d\n26 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d\n27 \xe2\x80\x8d\xe2\x80\x8d\xe2\x80\x8d \n28 e \n
\n