アイヌ語は日本語と異なり、閉音節(子音で終わる音節)も存在するので、表記の際音素文字であるラテン文字なら、そのまま p, t, k, m, n, s, r などの子音文字を後ろの付ければ良いわけなので、アイヌ語ローマ字表記では、何も問題が生じない。しかし、元々開音節言語である日本語に特化したカタカナのような仮名文字で表記する際、鼻音 n は「ン」でなんとかなる(実はそれでもまずい事になっているけどここでは割愛する)が、p, t, k, m, n, s, r, h はどうしようもないので、特殊の捨て仮名(小書き仮名文字)を利用することになっている。
具体的には以下のような特殊仮名文字(通称 アイヌ語仮名)である。
お分かり頂けただろうか…
r がいっぱいあるのは、まあちょっとめんどくさいけど r が 5 種類あると処理すれば良いけど
p くんに着目してください
「ㇷ゚」は「プ」が小ちゃくなっているが、その肩に乗ってある丸っこい「゜」にお気づきでしょうか
そう、あれは、U+309A COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK という結合文字である。
以上のように、アイヌ語で使われる仮名文字の中でも「ト゚」や「ツ゚」同様、仮名文字と結合文字の2つのコードポイントの結合文字列である。
何が問題かというと、正規表現エンジンによっては、U+309A を Unicode Property 的なカタカナと見なす実装とそうでない実装があるということである。例えば、「カㇷ゚」(U+30AB, U+31F7, U+309A)という文字列に対し、以下の正規表現エンジンによって、マッチングした結果を一覧した。
| エンジン | 正規表現 | 結果 |
|---|---|---|
| Perl (PCRE 2) | /\p{Katakana}/g | ["カ", "ㇷ", U+309A] |
| Python (re) | 未サポート | 未サポート |
| Python (regex) | re.findall(r'\p{Katakana}', 'カㇷ゚') | ["カ", "ㇷ"] |
| JavaScript | 'カㇷ゚'.match(/\p{Script=Katakana}/gu) | ["カ", "ㇷ"] |
| Golang (regexp) | regexp.MustCompile(`\p{Katakana}`).FindAllString("カㇷ゚", -1) | ["カ", "ㇷ"] |
| C# | Regex.Matches("カㇷ゚", @"\p{IsKatakana}"); | ["カ"] |
| Java (java.util.regex) | Pattern.compile("\\p{IsKatakana}").matcher("カㇷ゚").find(); | ["カ", "ㇷ"] |
| Ruby | 'カㇷ゚'.scan(/\p{Katakana}/) | ["カ", "ㇷ"] |
| Rust (regex) | Regex::new(r"\p{Katakana}").unwrap().find_iter("カㇷ゚") | ["カ", "ㇷ"] |
以上の表のように、言語によってかなりバラバラで、概ね["カ"]、["カ", "ㇷ"]、["カ", "ㇷ", U+309A] の三種類の結果が出る。
マッチング時に、U+309A を後ろに入れて? でマークしする(一文字扱いする)か、カタカナと文字グループに一緒に入れる(PCREの結果に統一する)か、のが穏当かな。
# pip install regeximport regexas rere.findall(r'\p{Katakana}\u309a?','カㇷ゚')# ['カ', 'ㇷ゚']re.findall(r'[\p{Katakana}\u309a]','カㇷ゚')# ['カ', 'ㇷ', '゚']下の追記も参照。
# pip install regeximport regexas rere.findall(r'\p{Katakana}\p{Mn}*','カㇷ゚')# ['カ', 'ㇷ゚']# pip install regex# pip install unisegimport regexas reimport uniseg.graphemeclusteras gc[charfor charin gc.grapheme_clusters('カㇷ゚')if re.match(r'\p{Katakana}', char)]# ['カ', 'ㇷ゚']'カㇷ゚'.match(/\p{Script=Katakana}\u309a?/gu)// ['カ', 'ㇷ゚']'カㇷ゚'.match(/[\p{Script=Katakana}\u309a]/gu)// ['カ', 'ㇷ', '゚']とのことらしいので
[...newIntl.Segmenter("mul").segment("カㇷ゚")].filter(({ segment})=>/\p{sc=Katakana}/u.test(segment)).map(({ segment})=> segment);['カ','ㇷ゚']'カㇷ゚'.match(/\p{sc=Katakana}\p{Mn}*/gu))// ['カ', 'ㇷ゚']// npm install @stdlib/string-to-grapheme-cluster-iteratorimportgraphemeClusters2iteratorfrom'@stdlib/string-to-grapheme-cluster-iterator';console.log([...graphemeClusters2iterator('カㇷ゚')].filter((c)=>/\p{sc=Katakana}/u.test(c)));// ['カ', 'ㇷ゚']// TODO...
コメント欄には大変ありがたいことに、こうなっている理由を詳しくご解説くださったコメントがございましたので、ご参考のほどお願い申し上げます。
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
各言語での正規表現マッチの結果がPerlとC#だけ異なるように見えますが、Perlに関しては\p{Katakana}が\p{Script_Extensions=Katakana}の省略表記とみなされるからです。(Perl 5.24までは\p{Katakana}は\p{Script=Katakana}の省略表記でしたが、Perl 5.26から\p{Script_Extensions=Katakana}の省略表記に変更されました。)
Perlでも/\p{Script=Katakana}/gを使えば["カ", "ㇷ"]が返りますし、JavaScriptでも/\p{Script_Extensions=Katakana}/guを使えば["カ", "ㇷ", U+309A]が返ります。
C# (.NET)に関しては、\p{Katakana}がKatakana scriptではなくKatakana blockにマッチするからです。.NETの正規表現における\p{}はGeneral_CategoryまたはBlockプロパティしか指定できず、残念ながらScriptまたはScript_Extensionsプロパティによるマッチには対応していなさそうです。