字符👩👩👧👦(有两个女人,一个女孩和一个男孩的家庭)被编码为:
U+1F469
WOMAN
,
U+200D
ZWJ
,
U+1F469
WOMAN
,
U+200D
ZWJ
,
U+1F467
GIRL
,
U+200D
ZWJ
,
U+1F466
BOY
所以它的编码非常有趣;单元测试的完美目标。然而,斯威夫特似乎并不知道如何对待它。这就是我的意思:
"👩👩👧👦".contains("👩👩👧👦") // true
"👩👩👧👦".contains("👩") // false
"👩👩👧👦".contains("\u{200D}") // false
"👩👩👧👦".contains("👧") // false
"👩👩👧👦".contains("👦") // true
所以,斯威夫特说它包含自己(好)和一个男孩(好!)。但它接着说它不包含女人、女孩或零宽度细木工。这里发生了什么事?为什么斯威夫特知道它包含一个男孩而不是一个女人或女孩?我可以理解它是否将其视为单个字符并且只识别它包含它自己,但它只有一个子组件而没有其他子组件的事实让我感到困惑。
如果我使用 "👩".characters.first!
之类的东西,这不会改变。
更令人困惑的是:
let manual = "\u{1F469}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}"
Array(manual.characters) // ["👩", "👩", "👧", "👦"]
即使我将 ZWJ 放在那里,它们也不会反映在字符数组中。接下来的事情有点说明:
manual.contains("👩") // false
manual.contains("👧") // false
manual.contains("👦") // true
所以我对字符数组有同样的行为......这非常烦人,因为我知道数组是什么样子的。
如果我使用 "👩".characters.first!
之类的东西,这也不会改变。
"👩👩👧👦".contains("\u{200D}")
仍然返回 false,不确定这是错误还是功能。
这与 String
类型在 Swift 中的工作方式以及 contains(_:)
方法的工作方式有关。
'👩👩👧👦 ' 是所谓的表情符号序列,它被呈现为字符串中的一个可见字符。该序列由 Character
个对象组成,同时又由 UnicodeScalar
个对象组成。
如果您检查字符串的字符数,您会看到它由四个字符组成,而如果您检查 unicode 标量计数,它会显示不同的结果:
print("👩👩👧👦".characters.count) // 4
print("👩👩👧👦".unicodeScalars.count) // 7
现在,如果您解析字符并打印它们,您会看到看似普通的字符,但实际上前三个字符在 UnicodeScalarView
中包含表情符号和零宽度连接符:
for char in "👩👩👧👦".characters {
print(char)
let scalars = String(char).unicodeScalars.map({ String($0.value, radix: 16) })
print(scalars)
}
// 👩
// ["1f469", "200d"]
// 👩
// ["1f469", "200d"]
// 👧
// ["1f467", "200d"]
// 👦
// ["1f466"]
如您所见,只有最后一个字符不包含零宽度连接符,因此在使用 contains(_:)
方法时,它可以按预期工作。由于您没有与包含零宽度连接符的表情符号进行比较,因此该方法不会找到除最后一个字符之外的任何匹配项。
为了对此进行扩展,如果您创建一个由以零宽度连接符结尾的表情符号字符组成的 String
,并将其传递给 contains(_:)
方法,它也将评估为 false
。这与 contains(_:)
与 range(of:) != nil
完全相同,后者试图找到与给定参数的完全匹配。由于以零宽度连接符结尾的字符形成不完整的序列,因此该方法尝试找到参数的匹配项,同时将以零宽度连接符结尾的字符组合成完整序列。这意味着如果出现以下情况,该方法将永远找不到匹配项:
参数以零宽度连接符结尾,并且要解析的字符串不包含不完整的序列(即以零宽度连接符结尾并且后面没有兼容字符)。
展示:
let s = "\u{1f469}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}" // 👩👩👧👦
s.range(of: "\u{1f469}\u{200d}") != nil // false
s.range(of: "\u{1f469}\u{200d}\u{1f469}") != nil // false
但是,由于比较只向前看,您可以通过向后工作在字符串中找到其他几个完整的序列:
s.range(of: "\u{1f466}") != nil // true
s.range(of: "\u{1f467}\u{200d}\u{1f466}") != nil // true
s.range(of: "\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") != nil // true
// Same as the above:
s.contains("\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}") // true
最简单的解决方案是为 range(of:options:range:locale:)
方法提供特定的比较选项。选项 String.CompareOptions.literal
对精确的逐个字符等效进行比较。作为旁注,这里的字符的意思是不是 Swift Character
,而是实例和比较字符串的 UTF-16 表示 - 但是,因为 String
不允许格式错误UTF-16,这本质上相当于比较 Unicode 标量表示。
这里我重载了 Foundation
方法,所以如果您需要原始方法,请重命名此方法或其他名称:
extension String {
func contains(_ string: String) -> Bool {
return self.range(of: string, options: String.CompareOptions.literal) != nil
}
}
现在,该方法对每个字符都“应该”工作,即使序列不完整:
s.contains("👩") // true
s.contains("👩\u{200d}") // true
s.contains("\u{200d}") // true
第一个问题是您使用 contains
桥接到 Foundation(Swift 的 String
不是 Collection
),所以这是 NSString
行为,我不相信它可以像 Swift 那样强大地处理组合的 Emoji。也就是说,我相信 Swift 现在正在实现 Unicode 8,这也需要围绕 Unicode 10 中的这种情况进行修改(所以当他们实现 Unicode 10 时这可能会发生变化;我还没有深入研究是否会这样做)。
为了简化事情,让我们摆脱 Foundation,并使用 Swift,它提供了更明确的视图。我们将从字符开始:
"👩👩👧👦".characters.forEach { print($0) }
👩
👩
👧
👦
好的。这正是我们所期望的。但这是一个谎言。让我们看看这些角色到底是什么。
"👩👩👧👦".characters.forEach { print(String($0).unicodeScalars.map{$0}) }
["\u{0001F469}", "\u{200D}"]
["\u{0001F469}", "\u{200D}"]
["\u{0001F467}", "\u{200D}"]
["\u{0001F466}"]
啊……所以它是["👩ZWJ", "👩ZWJ", "👧ZWJ", "👦"]
。这让一切都变得更加清晰。 👩 不是此列表的成员(它是“👩ZWJ”),但👦 是成员。
问题在于 Character
是一个“字素簇”,它将事物组合在一起(如附加 ZWJ)。您真正要寻找的是 unicode 标量。这完全符合您的预期:
"👩👩👧👦".unicodeScalars.contains("👩") // true
"👩👩👧👦".unicodeScalars.contains("\u{200D}") // true
"👩👩👧👦".unicodeScalars.contains("👧") // true
"👩👩👧👦".unicodeScalars.contains("👦") // true
当然,我们也可以寻找其中的实际角色:
"👩👩👧👦".characters.contains("👩\u{200D}") // true
(这严重重复了 Ben Leggiero 的观点。我在注意到他已经回答之前发布了这个。离开以防万一任何人都更清楚。)
ZWJ
代表什么?
String
据称已改回集合类型。这会影响你的答案吗?
似乎 Swift 认为 ZWJ
是一个扩展的字素簇,字符紧接在它前面。当将字符数组映射到它们的 unicodeScalars
时,我们可以看到这一点:
Array(manual.characters).map { $0.description.unicodeScalars }
这将从 LLDB 打印以下内容:
▿ 4 elements
▿ 0 : StringUnicodeScalarView("👩")
- 0 : "\u{0001F469}"
- 1 : "\u{200D}"
▿ 1 : StringUnicodeScalarView("👩")
- 0 : "\u{0001F469}"
- 1 : "\u{200D}"
▿ 2 : StringUnicodeScalarView("👧")
- 0 : "\u{0001F467}"
- 1 : "\u{200D}"
▿ 3 : StringUnicodeScalarView("👦")
- 0 : "\u{0001F466}"
此外,.contains
将扩展字素簇分组为单个字符。例如,取韩文字符 ᄒ
、ᅡ
和 ᆫ
(它们组合成韩语中的“一”:한
):
"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false
这找不到 ᄒ
,因为这三个代码点被分组到一个集群中,充当一个字符。类似地,\u{1F469}\u{200D}
(WOMAN
ZWJ
) 是一个簇,它充当一个字符。
其他答案讨论了 Swift 的作用,但没有详细说明原因。
你认为“Å”等于“Å”吗?我希望你会。
其中一个是带有组合符的字母,另一个是单个组合字符。您可以将许多不同的组合器添加到基本角色中,而人类仍然会认为它是单个角色。为了处理这种差异,创建了字素的概念来表示人类对字符的看法,而不管使用的代码点如何。
多年来,短信服务一直在将字符组合成图形表情符号:)
→ 🙂
。因此,各种表情符号被添加到 Unicode。
这些服务也开始将表情符号组合成复合表情符号。
当然,没有合理的方法将所有可能的组合编码为单独的代码点,因此 Unicode 联盟决定扩展字素的概念来包含这些复合字符。
如果您尝试在字素级别使用它,那么这归结为 "👩👩👧👦"
应该被视为单个“字素集群”,正如 Swift 默认情况下所做的那样。
如果您想检查它是否包含 "👦"
作为其中的一部分,那么您应该下降到较低的级别。
我不知道 Swift 语法,所以这里有一些 Perl 6,它对 Unicode 具有类似的支持级别。 (Perl 6 支持 Unicode 版本 9,因此可能存在差异)
say "\c[family: woman woman girl boy]" eq "👩👩👧👦"; # True
# .contains is a Str method only, in Perl 6
say "👩👩👧👦".contains("👩👩👧👦") # True
say "👩👩👧👦".contains("👦"); # False
say "👩👩👧👦".contains("\x[200D]"); # False
# comb with no arguments splits a Str into graphemes
my @graphemes = "👩👩👧👦".comb;
say @graphemes.elems; # 1
让我们降低一个层次
# look at it as a list of NFC codepoints
my @components := "👩👩👧👦".NFC;
say @components.elems; # 7
say @components.grep("👦".ord).Bool; # True
say @components.grep("\x[200D]".ord).Bool; # True
say @components.grep(0x200D).Bool; # True
不过,下降到这个水平会使一些事情变得更难。
my @match = "👩👩👧👦".ords;
my $l = @match.elems;
say @components.rotor( $l => 1-$l ).grep(@match).Bool; # True
我假设 Swift 中的 .contains
使这变得更容易,但这并不意味着没有其他事情变得更加困难。
例如,在此级别上工作可以更容易地在复合字符的中间意外拆分字符串。
您无意中问的是为什么这种更高级别的表示不像较低级别的表示那样工作。答案当然是,不应该。
如果你问自己“为什么要这么复杂”,答案当然是“人类”。
rotor
和 grep
在这里做什么? 1-$l
是什么?
rotor
。代码 say (1,2,3,4,5,6).rotor(3)
产生 ((1 2 3) (4 5 6))
。这是一个列表列表,每个长度为 3
。 say (1,2,3,4,5,6).rotor(3=>-2)
产生相同的结果,只是第二个子列表以 2
而不是 4
开头,第三个以 3
开头,依此类推,产生 ((1 2 3) (2 3 4) (3 4 5) (4 5 6))
。如果 @match
包含 "👩👩👧👦".ords
,那么 @Brad 的代码只会创建一个子列表,因此 =>1-$l
位是不相关的(未使用)。仅当 @match
短于 @components
时才有意义。
grep
尝试匹配其调用者中的每个元素(在本例中为 @components
的子列表列表)。它尝试将每个元素与其匹配器参数(在本例中为 @match
)进行匹配。如果 grep
产生至少一个匹配项,则 .Bool
返回 True
。
斯威夫特 4.0 更新
如 SE-0163 中所述,String 在 Swift 4 更新中收到了大量修订。两个表情符号用于此演示,代表两种不同的结构。两者都与一系列表情符号相结合。
👍🏽
是两个表情符号 👍
和 🏽
的组合
👩👩👧👦
是四个表情符号的组合,连接了零宽度连接器。格式为 👩joiner👩joiner👧joiner👦
1.计数
在 Swift 4.0 中,表情符号被算作字素簇。每个表情符号都计为 1。count
属性也可直接用于字符串。所以你可以直接这样调用它。
"👍🏽".count // 1. Not available on swift 3
"👩👩👧👦".count // 1. Not available on swift 3
字符串的字符数组在 Swift 4.0 中也算作字素簇,因此以下两个代码都打印 1。这两个 emoji 是 emoji 序列的示例,其中几个 emoji 组合在一起,它们之间有或没有零宽度连接符 \u{200d}
.在 swift 3.0 中,此类字符串的字符数组将每个表情符号分开,并生成一个包含多个元素的数组(表情符号)。在此过程中忽略加入者。然而,在 Swift 4.0 中,字符数组将所有表情符号视为一个整体。所以任何表情符号的那个永远是1。
"👍🏽".characters.count // 1. In swift 3, this prints 2
"👩👩👧👦".characters.count // 1. In swift 3, this prints 4
unicodeScalars
在 Swift 4 中保持不变。它在给定字符串中提供唯一的 Unicode 字符。
"👍🏽".unicodeScalars.count // 2. Combination of two emoji
"👩👩👧👦".unicodeScalars.count // 7. Combination of four emoji with joiner between them
2.包含
在 Swift 4.0 中,contains
方法会忽略表情符号中的零宽度连接符。因此,对于 "👩👩👧👦"
的四个表情符号组件中的任何一个,它都会返回 true,如果您检查加入者,则返回 false。但是,在 Swift 3.0 中,joiner 并没有被忽略,而是与前面的 emoji 组合在一起。因此,当您检查 "👩👩👧👦"
是否包含前三个组件 emoji 时,结果将为 false
"👍🏽".contains("👍") // true
"👍🏽".contains("🏽") // true
"👩👩👧👦".contains("👩👩👧👦") // true
"👩👩👧👦".contains("👩") // true. In swift 3, this prints false
"👩👩👧👦".contains("\u{200D}") // false
"👩👩👧👦".contains("👧") // true. In swift 3, this prints false
"👩👩👧👦".contains("👦") // true
表情符号,很像 unicode 标准,看似复杂。肤色、性别、工作、人群、零宽度连接序列、标志(2 个字符的 unicode)和其他复杂性可能会使表情符号解析变得混乱。一棵圣诞树、一片披萨或一堆便便都可以用一个 Unicode 代码点来表示。更不用说当引入新的表情符号时,iOS 支持和表情符号发布之间存在延迟。这和不同版本的 iOS 支持不同版本的 unicode 标准的事实。
TL;DR. 我致力于这些功能并开源了一个库,我是 JKEmoji 的作者,以帮助解析带有表情符号的字符串。它使解析变得如此简单:
print("I love these emojis 👩👩👧👦💪🏾🧥👧🏿🌈".emojiCount)
5
它通过从最新的 unicode 版本(最近的 12.0)定期刷新所有已识别表情符号的本地数据库并通过查看位图将它们与正在运行的操作系统版本中被识别为有效表情符号的内容进行交叉引用来实现这一点无法识别的表情符号字符的表示。
笔记
先前的答案因宣传我的图书馆而被删除,但没有明确说明我是作者。我再次承认这一点。
"👩👩👧👦".count
的计算结果为1
。