1. ホーム
  2. スイフト

[解決済み】なぜ、Swiftの文字列では、?のような絵文字が奇妙に扱われるのですか?

2022-04-13 09:14:14

質問

字?????????????????????????????????????????????! (女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はそれ自身(良い)と男の子(良い!)を含むと言います。しかし、それは、女性、女の子、またはゼロ幅ジョイナーを含んでいないと言います。 ここで何が起こっているのでしょうか?なぜSwiftは男の子が含まれていることを知っているのに、女性や女の子を含んでいないのでしょうか? 1つの文字として扱い、それ自体を含むと認識するだけなら理解できますが、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! .

解決方法は?

これは String 型が Swift でどのように動作するか、そして contains(_:) メソッドが動作します。

この『? ' はいわゆる絵文字列で、文字列の中で目に見える1文字としてレンダリングされます。シーケンスの構成は Character オブジェクトで構成され、同時に UnicodeScalar オブジェクトを作成します。

文字列の文字数を調べると4文字で構成されていることがわかりますが、ユニコードのスカラー数を調べると、違う結果が表示されます。

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
    }
}

これで、不完全な配列であっても、各文字に対してメソッドが "should"動作するようになりました。

s.contains("????")          // true
s.contains("????\u{200d}")  // true
s.contains("\u{200d}")    // true