web-dev-qa-db-ja.com

なぜSwift文字列でoのような絵文字が奇妙に扱われるのですか?

キャラクター???? (女性が2人、女の子が1人、男の子が1人の家族)は、次のようにエンコードされています。

U+1F469WOMAN
‍U+200DZWJ
U+1F469WOMAN
U+200DZWJ
U+1F467GIRL
U+200DZWJ
U+1F466BOY

それで、それは非常に興味深いエンコードです。単体テストに最適なターゲット。しかし、Swiftはそれを処理する方法を知らないようです。これが私の言っていることです:

"????‍????‍????‍????".contains("????‍????‍????‍????") // true
"????‍????‍????‍????".contains("????") // false
"????‍????‍????‍????".contains("\u{200D}") // false
"????‍????‍????‍????".contains("????") // false
"????‍????‍????‍????".contains("????") // true

それで、Swiftはそれがそれ自身(良い)と男の子(良い!)を含むと言います。しかしそれはそれからそれが女性、女の子、またはゼロ幅のジョイナーを含んでいないと言います。 ここで何が起きているの?なぜ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!のようなものを使ってもこれは変わりません。

515
Ben Leggiero

これは、SwiftでString型がどのように機能するか、および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とまったく同じであることに関係しています。range(of:options:range:locale:)は、指定された引数に完全に一致するものを見つけようとします。幅がゼロのジョイナで終わる文字は不完全なシーケンスを形成するため、メソッドは、幅がゼロのジョイナで終わる文字を完全なシーケンスに結合しながら引数の一致を見つけようとします。これは、次の場合にメソッドが一致を検出しないことを意味します。

  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

最も簡単な解決策は、特定の比較オプションを [FUNCTION] メソッドに提供することです。オプションString.CompareOptions.literalは、1文字ごとの正確な同等性で比較を実行します。補足説明として、ここでの文字の意味はnotSwift 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
390
xoudini

最初の問題はcontainsを使ってFoundationにブリッジしていることです(SwiftのStringCollectionではありません)、これはNSStringの振る舞いです。そうは言っても、Swiftは、現在Unicode 8を実装していると考えています。Unicode10でこの状況を修正する必要もありました(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", "????"]です。それはすべてをもう少し明確にします。 ????このリストのメンバーではありません(それは "???? ZWJ"です)が、????メンバーです。

問題は、Characterが(grafme cluster)であり、それが(ZWJをアタッチするように)ものを構成することです。あなたが本当に探しているのは、ユニコードスカラです。そしてそれはあなたが期待している通りに動作します。

"????‍????‍????‍????".unicodeScalars.contains("????") // true
"????‍????‍????‍????".unicodeScalars.contains("\u{200D}") // true
"????‍????‍????‍????".unicodeScalars.contains("????") // true
"????‍????‍????‍????".unicodeScalars.contains("????") // true

そしてもちろん、そこにある実際のキャラクターも探すことができます。

"????‍????‍????‍????".characters.contains("????\u{200D}") // true

(これはBen Leggieroの主張と大いに重複しています。私は彼が答えたと気付く前にこれを投稿しました。

106
Rob Napier

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は拡張書記素クラスタを単一の文字にグループ化します。たとえば、ハングル文字の、およびを取ります(これらを組み合わせて韓国語の単語を "one"にします:한)。

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

3つのコードポイントが1つの文字として機能する1つのクラスターにグループ化されているため、これはを見つけることができませんでした。同様に、\u{1F469}\u{200D}WOMANZWJ)は1つのクラスターであり、1つの文字として機能します。

74
Ben Leggiero

Swift 4.0のアップデート

SE-0163 に記載されているように、StringはSwift 4アップデートで多くのリビジョンを受け取ります。このデモでは、2つの異なる構造を表す2つの絵文字が使用されます。両方とも絵文字のシーケンスと組み合わされています。

????????は、2つの絵文字、????および????の組み合わせです。

????‍????‍????‍????は4つの絵文字を組み合わせたもので、幅ゼロの結合子が接続されています。フォーマットは????‍joiner????‍joiner????‍joiner????です

1.カウント

Swift 4.0では。絵文字は書記素クラスタとしてカウントされます。絵文字はすべて1としてカウントされます。countプロパティはstringでも直接利用できます。それで、あなたは直接それをこのように呼ぶことができます。

"????????".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メソッドはemojiのzero width joinerを無視します。そのため、"????‍????‍????‍????"の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
18
Fangming

他の答えではSwiftが何をするのかについて議論していますが、その理由については詳しく説明しません。

あなたは“Å”が“Å”と等しいと思いますか?私はあなたがそうすることを期待しています。

そのうちの1つはコンバイナ付きの文字で、もう1つは1つの合成文字です。基本キャラクターにさまざまなコンバイナーを追加することができますが、人間はそれを単一のキャラクターと見なすことになります。この種の矛盾に対処するために、書記素の概念は、使用されるコードポイントに関係なく、人間が文字を考慮するものを表すために作成されました。

現在、テキストメッセージングサービスは:)????の間、文字をグラフィカルな絵文字に結合してきました。そのため、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がそれを容易にすると思いますが、それ以上困難になるものが他にないというわけではありません。

このレベルで作業すると、たとえば誤って複合文字の途中で文字列を分割することがはるかに簡単になります。


誤ってこの高レベルの表現が低レベルの表現のように機能しないのはなぜでしょうか。答えはもちろんです、そうではありません。

あなたが「 なぜこれがそれほど複雑でなければならないのですか 」と自問しているなら、答えはもちろん「 人間 」です。

18
Brad Gilbert

Emojisは、Unicode標準とよく似ており、一見複雑なようです。肌の色調、性別、仕事、人々のグループ、幅が0のジョイナシーケンス、フラグ(2文字のUnicode)、その他の複雑な問題により、絵文字の解析が面倒になることがあります。クリスマスツリー、ピザのスライス、または糞の山は、すべて単一のUnicodeコードポイントで表すことができます。新しい絵文字が導入されたとき、iOSのサポートと絵文字のリリースの間には遅れがあることは言うまでもありません。それと、異なるバージョンのiOSが異なるバージョンのUnicode標準をサポートしているという事実。

TL; DR。 私はこれらの機能に取り組み、絵文字で文字列を解析するのを手助けするために JKEmoji の著者であるライブラリを開きました。構文解析は次のように簡単になります。

print("I love these emojis ????‍????‍????‍????????????????????????????".emojiCount)

5

最新のUnicodeバージョン( 12.0 最近)で認識されているすべての絵文字のローカルデータベースを定期的に更新し、それらを実行中のOSバージョンで有効な絵文字として認識されているものと相互参照します。認識できない絵文字のビットマップ表現.

_ note _

私の著者であることを明確に述べずに、私の図書館を宣伝するための以前の答えは削除されました。私はまたこれを認めています。

0
Joe