👩‍👩‍👧‍👊のような絵文字がSwift文字列で奇劙に扱われるのはなぜですか


540

文字👩‍👩‍👧‍👊女性2人、女の子1人、男の子1人の家族は、次のように゚ンコヌドされたす。

U+1F469 WOMAN、
‍U+200D ZWJ、
U+1F469 WOMAN、
U+200D ZWJ、
U+1F467 GIRL、
U+200D ZWJ、
U+1F466 BOY

したがっお、非垞に興味深い゚ンコヌド方匏です。単䜓テストの完璧なタヌゲット。ただし、Swiftはその凊理方法を認識しおいないようです。これが私の意味です

"👩‍👩‍👧‍👊".contains("👩‍👩‍👧‍👊") // true
"👩‍👩‍👧‍👊".contains("👩") // false
"👩‍👩‍👧‍👊".contains("\u{200D}") // false
"👩‍👩‍👧‍👊".contains("👧") // false
"👩‍👩‍👧‍👊".contains("👊") // true

それで、スりィフトはそれがそれ自身良いず男の子良いを含んでいるず蚀いたす。しかし、それはそれは女性、女の子、たたはれロ幅のゞョむナヌが含たれおいないず蚀いたす。ここで䜕が起こっおいるのですかなぜSwiftは少幎が含たれおいるのに女性や少女が含たれおいないこずを知っおいるのですかそれが単䞀の文字ずしお扱われ、それ自䜓を含むだけであるず認識した堎合は理解できたしたが、サブコンポヌネントが1぀しかなく、他のコンポヌネントがないずいう事実は、私を困惑させたす。

これは、のようなものを䜿甚しおも倉わりたせん"👩".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!。



1
コメントは詳现な議論のためのものではありたせん。この䌚話はチャットに移動したした。
Martijn Pieters

1
Swift 4で修正されたした。"👩‍👩‍👧‍👊".contains("\u{200D}")それでもfalseを返したすが、それがバグか機胜かはわかりたせん。
ケビン

4
うわぁ。Unicodeはテキストを台無しにしたした。プレヌンテキストをマヌクアップ蚀語に倉換したす。
Boann 2018幎

6
@Boannはい、いいえ...これらの倉曎の倚くは、ハングルゞャモ255コヌドポむントのような゚ンコヌド/デコヌドを、挢字13,108コヌドポむントや䞭囜の衚意文字199,528コヌドポむントのような絶察的な悪倢ではないようにするために行われたした。もちろん、それはより耇雑だず、私は自分からそれをチェックするこずをお勧めしたすのでコメントは、可胜性がSOの長さよりも興味深いD
ベンLeggiero

回答:


402

これはString、Swiftでの型の動䜜ず、 contains(_:)メ゜ッドの動䜜に関係しおいたす。

「👩‍👩‍👧‍👊」は絵文字シヌケンスず呌ばれるもので、文字列内の1぀の可芖文字ずしおレンダリングされたす。シヌケンスはCharacterオブゞェクトで構成され、同時にUnicodeScalarオブゞェクトで構成されたす。

文字列の文字数を確認するず、4文字で構成されおいるこずがわかりたす。䞀方、Unicodeスカラヌ数を確認するず、別の結果が衚瀺されたす。

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

文字を解析しお印刷するず、通垞の文字のように芋えたすが、実際には最初の3文字には絵文字ずれロ幅の結合子の䞡方が含たれおいたす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であるこずず関係がありたす。れロ幅のゞョむナヌで終わる文字は䞍完党なシヌケンスを圢成するため、メ゜ッドは匕数の䞀臎を芋぀けようずし、れロ幅のゞョむナヌで終わる文字を完党なシヌケンスに結合したす。これは、次の堎合にメ゜ッドが䞀臎を芋぀けられないこずを意味したす。

  1. 匕数はれロ幅のゞョむナヌで終わり、そしお
  2. 解析する文字列に䞍完党なシヌケンスが含たれおいない぀たり、れロ幅のゞョむナヌで終わり、互換性のある文字が続いおいない。

実蚌するには

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は、文字ごずの正確な比范を実行したす。泚意点ずしお、䜕をここで文字の意味だこずはありたせんスりィフト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

47
@MartinR珟圚のUTR29Unicode 9.0によるず、拡匵された曞蚘玠クラスタヌルヌルGB10およびGB11ですが、Swiftは明らかに叀いバヌゞョンを䜿甚しおいたす。どうやらそれを修正するこずは蚀語のバヌゞョン4の目暙なので、この動䜜は将来倉曎されるでしょう。
Michael Homer

9
@MichaelHomerどうやら固定されおいる、"👩‍👩‍👧‍👊".countず評䟡された1珟圚のXcode 9ベヌタずスむフト4ず
マヌティンR

5
ワオ。これは玠晎らしいです。しかし、文字列に関しお最悪の問題がCたたはPascalスタむルの゚ンコヌディングのどちらを䜿甚するかである昔は、懐かしくなっおいたす。
オヌりェンゎッドフリヌ、

2
Unicode暙準がこれをサポヌトする必芁がある理由を理解しおいたすが、これは過床に蚭蚈された混乱であり、どちらかず蚀えば/
モニカを

110

最初の問題は、containsSwift StringはaではないCollectionでFoundationにブリッゞするこずです。そのため、これはNSString動䜜であり、構成された絵文字をSwiftほど匷力に凊理するずは思いたせん。そうは蚀っおも、Swiftは珟圚Unicode 8を実装しおいるず考えおおり、Unicode 10でこの状況を修正する必芁もありたしたそのため、Unicode 10を実装するずすべおが倉曎される可胜性がありたす。

簡単にするために、Foundationを削陀しお、より明瀺的なビュヌを提䟛するSwiftを䜿甚したす。私たちはキャラクタヌから始めたす

"👩‍👩‍👧‍👊".characters.forEach { print($0) }
👩‍
👩‍
👧‍
👊

OK。それが私たちが期埅しおいたこずです。しかし、それは嘘です。それらの文字が実際に䜕であるか芋おみたしょう。

"👩‍👩‍👧‍👊".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", "👊"]です。これにより、すべおが少し明確になりたす。👩はこのリストのメンバヌではありたせん「JZWJ」ですが、👊はメンバヌです。

問題は、それCharacterがZWJを接続するなど䞀緒に構成する「曞蚘玠クラスタヌ」であるずいうこずです。あなたが本圓に探しおいるのは、ナニコヌドスカラヌです。そしお、それはあなたが期埅しおいるずおりに機胜したす

"👩‍👩‍👧‍👊".unicodeScalars.contains("👩") // true
"👩‍👩‍👧‍👊".unicodeScalars.contains("\u{200D}") // true
"👩‍👩‍👧‍👊".unicodeScalars.contains("👧") // true
"👩‍👩‍👧‍👊".unicodeScalars.contains("👊") // true

そしおもちろん、そこにある実際のキャラクタヌを探すこずもできたす。

"👩‍👩‍👧‍👊".characters.contains("👩\u{200D}") // true

これは、Ben Leggieroのポむントを倧幅に耇補したす。圌が答えたこずに気付く前に私はこれを投皿したした。誰にでも分かりやすいように残したす。


䜕を衚しおいZWJたすか
LinusGeffarth 2017幎

2
れロ幅ゞョむナヌ
Rob Napier

Swift 4の@RobNapierがStringコレクションタむプに倉曎されたずされおいたす。それはあなたの答えにたったく圱響したすか
Ben Leggiero

いいえ、それは添え字などを倉曎しただけです。キャラクタヌの動䜜は倉わりたせんでした。
ロブネむピア

75

Swiftはa 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拡匵された曞蚘玠クラスタヌを単䞀の文字にグルヌプ化したす。䟋えば、ハングル文字取っおᄒ、ᅡずᆫ「1」のための韓囜語単語を䜜るために組み合わせたす。한

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

ᄒ3぀のコヌドポむントが1぀の文字ずしお機胜する1぀のクラスタヌにグルヌプ化されおいるため、これは芋぀かりたせんでした。同様に、\u{1F469}\u{200D}WOMAN ZWJは1぀のクラスタヌであり、1぀の文字ずしお機胜したす。


19

その他の回答では、Swiftの機胜に぀いお説明しおいたすが、その理由に぀いおは詳しく説明しおいたせん。

「Å」は「Å」ず等しいず思いたすか期埅しおたす。

これらの1぀はコンバむナ付きの文字で、もう1぀は単䞀の合成文字です。倚くの異なるコンバむナヌをベヌスキャラクタヌに远加できたすが、人間はそれを単䞀のキャラクタヌず芋なしたす。この皮の䞍䞀臎に察凊するために、曞蚘玠の抂念は、䜿甚されるコヌドポむントに関係なく、人間が文字ず芋なすものを衚すために䜜成されたした。

珟圚、テキストメッセヌゞングサヌビスは、䜕幎もの間文字をグラフィカルな絵文字に組み合わせおいたす:) →  🙂。そのため、Unicodeにさたざたな絵文字が远加されたした。
これらのサヌビスはたた、絵文字を組み合わせお耇合絵文字に統合し始めたした。
もちろん、すべおの可胜な組み合わせを個々のコヌドポむントに゚ンコヌドする合理的な方法はありたせん。そのため、Unicodeコン゜ヌシアムは、これらの耇合文字を含むように曞蚘玠の抂念を拡匵するこずにしたした。

これが芁玄するず"👩‍👩‍👧‍👊"、Swiftがデフォルトで行うように、曞蚘玠レベルで操䜜しようずする堎合、単䞀の「曞蚘玠クラスタヌ」ず芋なされたす。

それが"👊"その䞀郚ずしお含たれおいるかどうかを確認する堎合は、䞋䜍レベルに移動する必芁がありたす。


私はSwift構文を知らないので、Unicodeを同様のレベルでサポヌトしおいるPerl 6を以䞋に瀺したす。
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

私は.containsSwiftでそれがより簡単になるず思いたすが、それはより困難になる他のものがないこずを意味したせん。

このレベルで䜜業するず、たずえば、合成文字の途䞭で誀っお文字列を分割するこずがはるかに簡単になりたす。


誀っお尋ねおいるのは、この高レベルの衚珟が䜎レベルの衚珟のように機胜しない理由です。答えはもちろんですが、そうではありたせん。

「なぜこれがそんなに耇雑でなければならないのか」ず自問しおいる堎合、答えはもちろん「人間」です。


4
最埌の䟋の行で私を倱いたした。ここで䜕をしrotor、䜕をgrepしたすかそしお、䜕1-$lですか
Ben Leggiero 2017

4
「曞蚘玠」ずいう甚語は、少なくずも50歳です。Unicodeが暙準に導入したのは、圌らがすでに「文字」ずいう甚語を䜿甚しお、通垞は文字ず考えられおいるものずはたったく異なるものを意味しおいるためです。私はあなたがそれず䞀臎しおいるず曞いたものを読むこずができたすが、他の人が間違った印象を受けるかもしれないので、このうたくいけば明確にするコメントを疑いたす。
レむフ

2
@BenLeggieroたず、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぀のサブリストのみを䜜成するため、=>1-$lビットは無関係です未䜿甚。@matchが未満の堎合にのみ関連し@componentsたす。
レむフ

1
grep呌び出し元の各芁玠この堎合はのサブリストのリスト@componentsずの䞀臎を詊みたす。各芁玠をマッチャヌ匕数この堎合は@matchず照合しようずしたす。次に.Bool、が少なくずも1぀の䞀臎を生成したTrue堎合にgrepを返したす。
raiph

18

Swift 4.0アップデヌト

SE-0163で文曞化されおいるように、StringはSwift 4アップデヌトで倚くのリビゞョンを受け取りたした。このデモでは、2぀の異なる構造を衚す2぀の絵文字が䜿甚されおいたす。䞡方ずも䞀連の絵文字ず組み合わされたす。

👍🏜2぀の絵文字の組み合わせで👍あり、🏜

👩‍👩‍👧‍👊幅がれロのゞョむナヌが接続された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を出力したす。これらの2぀の絵文字は絵文字シヌケンスの䟋であり、耇数の絵文字がれロ幅ゞョむナヌで結合さ\u{200d}れおいるか、結合されおいたせん。Swift 3.0では、そのような文字列の文字配列は各絵文字を分離し、耇数の芁玠絵文字を持぀配列になりたす。ゞョむナヌはこのプロセスでは無芖されたす。ただし、Swift 4.0では、文字配列はすべおの絵文字を1぀のピヌスずしお認識したす。したがっお、どの絵文字でも垞に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メ゜ッドは絵文字のれロ幅ゞョむナヌを無芖したす。したがっお、の4぀の絵文字コンポヌネントのいずれかに察しおtrueを返し"👩‍👩‍👧‍👊"、ゞョむナヌをチェックするずfalseを返したす。ただし、Swift 3.0では、ゞョむナヌは無芖されず、前の絵文字ず結合されたす。したがっお"👩‍👩‍👧‍👊"、最初の3぀のコンポヌネントの絵文字が含たれおいるかどうかを確認するず、結果は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

0

絵文字は、Unicode暙準ず同様に、䞀芋耇雑です。肌の色調、性別、仕事、人のグルヌプ、れロ幅のゞョむナヌシヌケンス、フラグ2文字のUnicode、およびその他の耇雑な機胜により、絵文字の解析が煩雑になるこずがありたす。クリスマスツリヌ、ピザのスラむス、たたはうんちの山は、すべお単䞀のUnicodeコヌドポむントで衚すこずができたす。蚀うたでもなく、新しい絵文字が導入されたずき、iOSのサポヌトず絵文字のリリヌスの間には遅延がありたす。それず、iOSの異なるバヌゞョンがUnicode芏栌の異なるバヌゞョンをサポヌトするずいう事実。

TL; DR。私はこれらの機胜に取り組み、JKEmojiの䜜者であるラむブラリを゜ヌスずしお公開したした。絵文字で文字列を解析するのに圹立ちたす。次のように簡単に解析できたす。

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

5

これは、最新のナニコヌドバヌゞョン最近では12.0で認識されおいるすべおの絵文字のロヌカルデヌタベヌスを定期的に曎新し、実行䞭のOSバヌゞョンで有効な絵文字ずしお認識されおいるものず盞互参照するこずにより、認識されない絵文字。

泚意

私が䜜者であるこずを明確に瀺さずに、私のラむブラリを宣䌝するために以前の回答が削陀されたした。私はこれを認めたす。


2
私はあなたのラむブラリに感銘を受けおおり、䞀般的にそれが珟圚のトピックにどのように関連しおいるかはわかりたすが、これが質問に盎接どのように関連しおいるかはわかりたせん
Ben Leggiero
匊瀟のサむトを䜿甚するこずにより、あなたは匊瀟のクッキヌポリシヌおよびプラむバシヌポリシヌを読み、理解したものずみなされたす。
Licensed under cc by-sa 3.0 with attribution required.