从任何UTF-16偏移量中,找到位于字符边界

时间:2017-09-25 15:05:27

标签: swift string unicode swift4

我的目标:String中给出任意UTF-16位置,找到代表String.Index的相应Character(即扩展的字形群集) )指定的UTF-16代码单元是。

的一部分

示例:

(I put the code in a Gist for easy copying and pasting.)

这是我的测试字符串:

let str = "‍"

(注意:要将字符串视为单个字符,您需要在最近的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

给定一个任意的UTF-16偏移(比方说2),我可以创建一个相应的String.Index

let utf16Offset = 2
let utf16Index = String.Index(encodedOffset: utf16Offset)

我可以使用此索引下标字符串,但如果索引不落在Character边界上,则下标返回的Character可能不会覆盖整个字形集群:< / p>

let char = str[utf16Index]
print(char)
// → ‍
print(char.unicodeScalars.map { "0x\(String($0.value, radix: 16))" })
// → ["0x1f3fe", "0x200d", "0x1f692"]

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

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

您可以测试索引是否落在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!)

问题:

我认为问题是最后一个表达式返回true The documentation for String.Index.init(_:within:)说:

  

如果以sourcePosition传递的索引表示扩展字形集群的开头 - 字符串的元素类型 - 则初始化程序成功。

这里,utf16Index并不代表扩展字形集群的开始 - 字形集群从偏移0开始,而不是偏移2.然而初始化程序成功。

因此,我通过反复递减索引encodedOffset和测试isOnCharacterBoundary来尝试查找字形集群的开始失败。

我忽略了什么吗?还有另一种方法来测试索引是否落在Character的开头?这是Swift中的错误吗?

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

更新:查看有趣的Twitter thread about this question

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

2 个答案:

答案 0 :(得分:3)

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

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

示例:

let str = "a‍bcd‍‍‍e"
for utf16Offset in 0..<str.utf16.count {
    if let idx = str.index(utf16Offset: utf16Offset) {
        print(utf16Offset, str[idx])
    }
}

输出:

0 a
1 ‍
2 ‍
3 ‍
4 ‍
5 ‍
6 ‍
7 ‍
8 b
9 
10 
11 
12 
13 c
14 
15 
16 d
17 ‍‍‍
18 ‍‍‍
19 ‍‍‍
20 ‍‍‍
21 ‍‍‍
22 ‍‍‍
23 ‍‍‍
24 ‍‍‍
25 ‍‍‍
26 ‍‍‍
27 ‍‍‍
28 e 

答案 1 :(得分:0)