为什么Swift不考虑ZWJ序列表情符号包含其每个组成代码点?

zengzsys  于 2023-05-16  发布在  Swift
关注(0)|答案(6)|浏览(222)

字符👩‍👩‍👧‍👦(有两个女人,一个女孩和一个男孩的家庭)编码如下:

USV  gc  sc    bc     age  name
U+1F469  So  Zyyy  ON     6.0   [WOMAN](https://emojipedia.org/emoji/%F0%9F%91%A9/) 
 U+200D  Cf  Zinh  BN     1.1   [ZERO WIDTH JOINER](https://en.wikipedia.org/wiki/Zero-width_joiner) 
U+1F469  So  Zyyy  ON     6.0  WOMAN
 U+200D  Cf  Zinh  BN     1.1  ZERO WIDTH JOINER
U+1F467  So  Zyyy  ON     6.0   [GIRL](https://emojipedia.org/emoji/%F0%9F%91%A7/) 
 U+200D  Cf  Zinh  BN     1.1  ZERO WIDTH JOINER
U+1F466  So  Zyyy  ON     6.0   [BOY](https://emojipedia.org/emoji/%F0%9F%91%A6/)

所以它的编码很有趣单元测试的完美目标。然而,斯威夫特似乎不知道如何对待它。我的意思是

"👩‍👩‍👧‍👦".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!的东西,这也不会改变。

hs1ihplo

hs1ihplo1#

这与String类型在Swift中的工作方式以及contains(_:)方法的工作方式有关。
'👩‍👩‍👧‍👦'就是所谓的表情符号序列,它被渲染为字符串中的一个可见字符。序列由Character对象组成,同时它由UnicodeScalar对象组成。
如果你检查字符串的字符数,你会看到它是由四个字符组成的,而如果你检查unicode标量计数,它会给你一个不同的结果:

print("👩‍👩‍👧‍👦".characters.count)     // 4
print("👩‍👩‍👧‍👦".unicodeScalars.count) // 7

现在,如果你解析字符并打印它们,你会看到看起来像普通字符的东西,但实际上前三个字符在它们的UnicodeScalarView中包含一个emoji和一个零宽度的joiner:

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(_:)方法时,它会按照您所期望的那样工作。由于您没有与包含零宽度连接符的表情符号进行比较,因此该方法不会找到除最后一个字符之外的任何匹配字符。
为了扩展这一点,如果您创建一个由以零宽度joiner结尾的emoji字符组成的String,并将其传递给contains(_:)方法,它也将计算为false。这与contains(_:)完全相同,range(of:) != nil试图找到与给定参数的精确匹配。由于以零宽度joiner结尾的字符形成不完整的序列,因此该方法尝试找到参数的匹配,同时将以零宽度joiner结尾的字符组合成完整的序列。这意味着该方法在以下情况下永远不会找到匹配:
1.参数以零宽度连接器结束,并且
1.要解析的字符串不包含不完整的序列(即以零宽度连接符结尾并且后面不跟随兼容字符)。
演示:

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
tp5buhyn

tp5buhyn2#

第一个问题是你正在用contains(Swift的String不是Collection)桥接到Foundation,所以这是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

(This与本·莱杰罗的观点完全吻合我在注意到他回复之前就贴了这个。离开,以防对任何人都更清楚。)

oknwwptz

oknwwptz3#

Swift似乎认为ZWJ是一个扩展的字素簇,其前面紧跟着一个字符。我们可以在将字符数组Map到它们的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将扩展的字素簇分组为单个字符。例如,取朝鲜文字符(它们组合在一起构成韩语单词“one”:한):

"\u{1112}\u{1161}\u{11AB}".contains("\u{1112}") // false

这找不到,因为三个代码点被分组为一个簇,作为一个字符。类似地,\u{1F469}\u{200D}WOMANZWJ)是一个簇,它充当一个字符。

s1ag04yj

s1ag04yj4#

Swift 4.0更新

String在Swift 4更新中收到了很多修订,如SE-0163中所述。这个演示使用了两个表情符号,代表了两种不同的结构。两者都与一系列emoji相结合。
👍🏽是两个表情符号👍🏽的组合
👩‍👩‍👧‍👦是四个emoji的组合,零宽度连接器连接。格式为👩‍joiner👩‍joiner👧‍joiner👦

1.数量

在Swift 4.0中,emoji被视为字形簇。每个emoji都被视为1。count属性也可直接用于字符串。所以你可以这样直接调用。

"👍🏽".count  // 1. Not available on swift 3
"👩‍👩‍👧‍👦".count  // 1. Not available on swift 3

在Swift 4.0中,字符串的字符数组也被算作字素簇,所以下面的代码都打印1。这两个表情符号是表情符号序列的示例,其中几个表情符号组合在一起,在它们之间具有或不具有零宽度连接符\u{200d}。在Swift 3.0中,这样的字符串的字符数组将每个表情符号分开,并导致具有多个元素的数组(表情符号)。在此过程中将忽略连接器。然而,在Swift 4.0中,字符数组将所有emoji视为一个整体。所以任何emoji的值都是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方法忽略了emoji中的零宽度joiner。因此,"👩‍👩‍👧‍👦"的四个emoji组件中的任何一个都返回true,如果检查joiner,则返回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
oymdgrw7

oymdgrw75#

其他答案讨论了Swift做了什么,但没有详细说明为什么。
你希望A等于Å吗我想你会的
其中一个是带有组合符的字母,另一个是单个组合字符。你可以给一个基本字符添加许多不同的组合器,而人类仍然会认为它是一个单一的字符。为了处理这种差异,创建了字素的概念来表示人类认为字符的内容,而不管使用的码点是什么。
现在,短信服务已经将字符组合成图形表情符号多年:)🙂。因此,各种emoji被添加到Unicode中。
这些服务也开始将emoji组合成复合emoji。
当然,没有合理的方法将所有可能的组合编码到单独的码点中,因此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使这变得更容易,但这并不意味着没有其他事情变得更困难。
例如,在这个级别上工作可以更容易地在复合字符的中间意外拆分字符串。
你不经意间问的是,为什么这个更高层次的表示不像一个更低层次的表示那样工作。答案当然是不应该的。
如果你问自己“为什么这必须如此复杂”,答案当然是“人类”。

vqlkdk9b

vqlkdk9b6#

表情符号,就像unicode标准一样,看似复杂。肤色、性别、工作、人群、零宽度连接器序列、标志(2个字符的unicode)和其他复杂因素都会使表情符号解析变得混乱。一棵圣诞树、一片比萨饼或一堆便便都可以用一个Unicode码位表示。更不用说,当新的表情符号被引入时,iOS支持和表情符号发布之间存在延迟。事实上,不同版本的iOS支持不同版本的Unicode标准。

**TL; DR.**我致力于这些功能,并开源了一个库,我是JKEmoji的作者,以帮助解析带有表情符号的字符串。它使解析变得简单:

print("I love these emojis 👩‍👩‍👧‍👦💪🏾🧥👧🏿🌈".emojiCount)

5
它通过定期刷新最新Unicode版本(最近的12.0)的所有已识别emoji的本地数据库,并通过查看未识别emoji字符的位图表示,将它们与运行的操作系统版本中识别为有效emoji的内容进行交叉引用。

备注

以前的一个答案被删除,因为我的图书馆做广告,没有明确说明我是作者。我再次承认这一点。

相关问题