我的目标:在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。
答案 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 = "abcde"
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)