Help us understand the problem. What is going on with this article?

.NET FrameworkとPythonで合成文字の前方一致を比較

.NET Framework と Python で前方一致の挙動が違うことに気付きました。Unicode の等価性に関係します。一部 Mono の挙動にも注意が必要です。

正規化

Unicode には濁点やアクセントなどの付加記号が付いた文字を、まとめて 1 文字(合成済み文字)として扱う方法と、ベースの文字(基底文字)と記号(結合文字)とを分離して扱う方法があります。どちらかに統一することを正規化と呼びます。

Unicodeの正規化手段の基礎は、文字の合成と分解という概念である。文字の合成とは、基底文字と結合文字の組み合わせによる結合文字列を、単一の符号位置である合成済み文字にする手続きである。

身近な例では濁点付きの仮名が対象となります。

  • "か" U+304B + "゛" U+3099 ⇔ "が" U+304C

【参考】 Unicodeでは濁点や半濁点を別扱いしてることがあるので結合した - はてなの鴨澤

.NET Framework の文字列には正規化のためのメソッドがあります。

F#
> "\u304b\u3099".Normalize() |> Seq.map (int >> sprintf "%x");;
val it : seq<string> = seq ["304c"]

正準等価

分離した形と結合した形とを等価として扱う方法を正準等価と呼びます。

たとえば、ダイアクリティカルマークを持つ合成済みの文字は、分解すると「基底文字+結合文字のダイアクリティカルマーク」の文字列に変わるが、いずれも等価であるとみなされる。言いかえると合成済み文字 ‘ü’ は ‘u’ と結合文字の分音記号 ‘¨’ を並べたものと正準等価である。

.NET Framework や Python では、単なる文字列比較では正準等価は考慮されません。

F#
> "\u304b\u3099" = "\u304c";;
val it : bool = false
Python
>>> "\u304b\u3099" == "\u304c"
False

メソッドによっては考慮されます。

StartsWith

以下の例では、.NET Framework で正準等価が考慮されますが、Python では考慮されません。

F#
> "\u304b\u3099".StartsWith "\u304c";;
val it : bool = true

> "\u304c".StartsWith "\u304b\u3099";;
val it : bool = true
Python
>>> "\u304b\u3099".startswith("\u304c")
False
>>> "\u304c".startswith("\u304b\u3099")
False

以下の例では、文字コードの並びを見ているかどうかの違いが分かります。

F#
> "\u304b\u3099".StartsWith "\u304b";;
val it : bool = false
Python
>>> "\u304b\u3099".startswith("\u304b")
True

合成済み文字が用意されていない組み合わせでも、.NET Framework では結合文字として扱われます。

F#
> "=\u3099".StartsWith "=";;
val it : bool = false
Python
>>> "=\u3099".startswith("=")
True

= を増やすと、.NET Framework と Mono で挙動が変わります。

.NET
> "==\u3099".StartsWith "==";;
val it : bool = false
Mono
> "==\u3099".StartsWith "==";;
val it : bool = true
Python
>>> "==\u3099".startswith("==")
True

※ .NET Core 3.1 では .NET Framework 4.8 と同じ結果になることを確認しました。

移植の際に挙動の違いに悩まされました。

Python に合わせるには部分文字列を比較する方法があります。

F#
> "==\u3099".Substring(0, 2) = "==";;
val it : bool = true

※ 一般化する場合、部分文字列を取る前に長さがはみ出さないか確認する必要があります。

正規表現でも正準等価は考慮されません。

F#
> open System.Text.RegularExpressions;;
> let r = Regex "^=";;
val r : Regex = ^=

> r.Match "=\u3099";;
val it : Match = = {Captures = seq [...];
                    Groups = seq [...];
                    Index = 0;
                    Length = 1;
                    Name = "0";
                    Success = true;
                    Value = "=";}

追記

Mono の件に関してコメントをいただきました。難しい問題のようです。

関連記事

以下の記事を書くための調査をしているときに気付きました。

== の言語名に余分な結合文字が含まれていましたが、修正されていることを確認しました。定期的にチェックしているようです。

7shi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした