文字 �������� (女性 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 はなぜ、文字自体に男の子が含まれていることは知っているのに、女性や女の子が含まれていることは知らないのでしょうか。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!
。
ベストアンサー1
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
- 引数はゼロ幅の結合子で終了し、
- 解析する文字列に不完全なシーケンス (つまり、ゼロ幅の結合子で終了し、その後に互換性のある文字が続かない) が含まれていません。
実証するには:
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ではなく、インスタンスと比較文字列の両方の UTF-16 表現を意味します。ただし、 は不正な UTF-16 を許可しないため、これは基本的に Unicode スカラー表現を比較するのと同じです。Character
String
ここではメソッドをオーバーロードしている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