拗音や促音が正しい位置にあるかチェックする正規表現

また例によってニッチなことを、って「 iVoca でローマ字入力に対応」のために必要だったからなんだけど。


使って良い文字種の範囲に収まっているか、なら正規表現で簡単にチェックできる。


拗音の類(小さいゃゅょぁぃぅぇぉ)や促音(小さいっ)は周りの文字によって正しい位置かそうでないかが決まるので、そう簡単にはいかない。
「はゅーん」とかライトノベルの準ヒロインが言いそうな言葉、ローマ字でどう打ったらいいかわかりませんなんて問い合わせがサポートに来たらめんどくさい。


ネットで探しても見つからなかったので、がんばって拗音や促音が正しい位置にあるかどうかチェックする正規表現を書いてみた。
同じように困って探している人があるいはもしかしたらいるかもしれないので、ここでさらしてみる。


これらの正規表現UTF-8 の文字列に対して、正しくない場所に拗音や促音があればマッチする。
見やすさのために正規表現の途中に改行を入れてみたが、実際には1行である。

ILLEGAL_YOUON =
  /(?:^|(?!\xe3(?:
        \x81[\x86\x8d\x8e\x97\x98\xa1\xa2\xa4\xa6\xa7\xab\xb2-\xb5\xbf]
        |\x82[\x8a\x94\xa6\xad\xae\xb7\xb8]
        |\x83[\x81\x82\x84\x86\x87\x8b\x92-\x95\x9f\xaa\xb4])).)
    \xe3(?:\x81[\x81\x83\x85\x87\x89]|\x82[\x83\x85\x87\xa1\xa3\xa5\xa7\xa9]|\x83[\xa3\xa5\xa7])/

ILLEGAL_SOKUON =
  /\xe3(?:\x81\xa3|\x83\x83)
    (?:$|(?!\xe3(?:
        \x81[\x8b-\xa2\xa4-\xa9\xaf-\xbf]
        |\x82[\x80-\x82\x84\x86\x88-\x8d\x8f\x94\xab-\xbf]
        |\x83[\x80-\x82\x84-\x89\x8f-\xa2\xa4\xa6\xa8-\xad\xaf\xb4]
    )))/

puts "NG" if ILLEGAL_YOUON =~ "きょう"
puts "NG" if ILLEGAL_YOUON =~ "はゅーん" # NG
puts "NG" if ILLEGAL_YOUON =~ "ぁん" # NG

puts "NG" if ILLEGAL_YOUON =~ "きっぷ"
puts "NG" if ILLEGAL_YOUON =~ "かっあ" # NG
puts "NG" if ILLEGAL_YOUON =~ "とっ" # NG


ここで「正しい拗音の場所」とは「うきぎしじちぢつてでにひびぴふみりヴ」の後ろ、
「正しい促音の場所」とは「な行わ行以外の子音、もしくは『わ』」の前、としている。


促音については、

  • 「を」の前に促音があってもローマ字入力的には問題ないのだが、実際の文章ではそんなことはありえないので除外
  • 実際の文章では促音が先頭に来るのはおかしいが、ローマ字入力的には問題ないのでスルー

というダブルスタンダードで。
まあ「チェックが楽な方」という意味では一環している。


拗音についてはもうちょっとややこしくて、
「うきぎしじちぢつてでにひびぴふみりヴ」の後ろに「ゃゅょぁぃぅぇぉ」なので、「きぁ」「うゅ」とかもOKになってしまっている。
しかし、これをまじめにチェックし出すと大変なのだ。


たとえば「き」の後ろに来るのは「ゃゅょ」しかないが、
「し」の後ろには「ゃゅょ」以外に「ぇ」もありうる(シェイク)。
「て」の後ろは「ぃ」と「ゅ」はありうるが、あとは無い(ティーショット、テューダー朝)。


こんな調子なので、まじめにチェックするにはマトリックスを書くしか無くて、正規表現でやるのはちょっとだいぶめんどくさい。
今回は、対応するローマ字入力さえ決定できればよく、それが一意である必要がないという要件だったので、そういうめんどくささは回避しちゃった。


また、この正規表現では \xe3 などは UTF-8 の1バイト目にマッチしてくれるが、 . は「1文字」にマッチするという性質を使っているので、そうでない正規表現エンジンの場合は使えないかも。
その場合には否定先読みを使うのが難しくなるので、「拗音の前に置くべきでない文字クラス」を全て書き出す必要があるかもしれない。

その方針で正規表現を書くとこんな感じ。

ILLEGAL_YOUON =
  /(?:^|[\x20-\x7f]|[\xc3-\xc5][\x80-\xbf]
    |\xe3\x81[\x81-\x85\x87-\x8c\x8f-\x96\x99-\xa0\xa3\xa5\xa8-\xaa\xac-\xb1\xb6-\xbe]
    |\xe3\x82[\x80-\x89\x8b-\x93\xa1-\xa5\xa7-\xac\xaf-\xb6\xb9-\xbf]
    |\xe3\x83[\x80\x83\x85\x88-\x8a\x8c-\x91\x96-\x9e\xa0-\xa9\xab-\xb3\xbc])
  \xe3(?:\x81[\x81\x83\x85\x87\x89]|\x82[\x83\x85\x87\xa1\xa3\xa5\xa7\xa9]|\x83[\xa3\xa5\xa7])/

ILLEGAL_SOKUON =
  /\xe3(?:\x81\xa3|\x83\x83)
  (?:$|[\x20-\x7f]|[\xc3-\xc5][\x80-\xbf]
    |\xe3\x81[\x81-\x8a\xa3\xaa-\xae]
    |\xe3\x82[\x83-\x88\x90-\x93\xa1-\xaa]
    |\xe3\x83[\x83\x8a-\x8e\xa3-\xa8\xb0-\xb3\xbc])/


iVoca の場合、ascii 文字の他にアクセント付きのアルファベットも一部使えるので、2バイトの UTF-8 もわざわざ「拗音の前に置くべきでない文字クラス」に加えている。あーめんどくさい。
それがなくても、拗音チェックの方はだいぶ長くなってしまう。これに『「て」の後ろは「ぃ」と「ゅ」しかない』とか入れようと思ったら、どえらいことに。