隠し部屋の秘境

豆知識的な小ネタ見つけては投稿していきます

unicodeのひらがなと50音表を対応させる

どうも。今回はひらがなをひらがならしく扱いたいという事で関数を作成しました。

ひらがなをひらがならしく?と思いましたよね

まあすぐに意味が分かると思います。

目次

はじめに

Unicodeのひらがなは次のような順番で並んでいます。

ぁあ…かが…さざ…ただ…つづてで...なにぬ…はばひび…ま…ゃや…ら…ゎわゐゑをん

ある程度は規則的に並んでいるものの、簡単な法則で表そうとすると案外上手くいかない。

そこで、これを次のような50音表のように扱える関数を考えることにしました。

f:id:theEasyPuzzle666:20220722080800j:image

これを見る限り、ひらがなは次の3つの要素に分解出来ることが分かります

かな文字 = (行,段,種)

行: 横軸, 子音を決める

段: 縦軸, 母音を決める

種: 濁点や小文字などのオプション

 

例えば、「ぢ」ならた行,イ段, 濁点ありで(3,1,1)となります。

この2番目の数字から「ぢ」がイ段である事を取得できたり、3番目の引数を0にすれば濁点を落とせたりと、ひらがなとして扱いやすい表現である事が分かります。

これを「かな文字表現」と呼ぶことにします。

 

つまり逆に言えば、ひらがなをこの3つの要素に分解する事で扱いやすい50音表を構築しようという訳です。

では肝心の分解する方法はどうしたら良いでしょうか?

まずは分かりやすさのために要点をまとめて説明します。結果だけ知りたい場合はこちら

基礎概念や導出過程などは後ほど。

構築手順

Step1: かな文字表現→Unicode

まずはかな文字表現からUnicode値を求める関数から考えていきます。その後で逆変換を求めます。

以降、Unicode値をUまたはU値と呼びます。

Unicodeにおける最初のひらがなは「ぁ」で値は12353です。この値が基準値となります。土台です。

ここでかな文字表現の5×行+段に相当する値をXで置くことにします。この値が骨組みのような役割を担います。

後は肉付けとして補正項を導入します。濁点付きの文字などが一切なければXで十分ですが、残念ながらあるので、文字によって値にズレが生じてきます。そのズレを個々にεとします。

するとそれをかき集めてやれば、ズレを修正することができる訳です。

ここまでで、U = 12353 + X + ∑ε と表せることが分かります。

問題は補正項の求め方です。

これは効果の範囲と値を指定してやる事で解決します

その方法ですが、

ε = (X-開始)×オプションの種数 (開始≦X<終了) & ズレの合計 (終了≦X) とします。

 

例えば、は行に関する補正値は

・開始は「は」(X=25)

・終了は「ま」(X=30)の直前

・オプションの種数は2種類(濁点,半濁点)

・ズレの合計は10個 なので、

ε = 2(X-25) (25≦X<30), 10 (30≦X) となります。

 

また、や行ではイ段,エ段が飛んでいます。ここの表現は次のようにします。

ε = -1×飛ばす個数×増え方 (跳躍先≦X)

 

ここではや行のイ段で説明します。

・跳躍先は「ゆ」(X=37)

・飛ばす個数は1個

・増え方は2づつ なので、

ε = -2 (37≦X) となります。

 

これを他についてもまとめていくと、最終的に式は次のようになります。

U = 12353 + X +

{X (0≦X<20), 20 (20≦X)} +

{X-17 (17≦X<18), 1 (18≦X)} +

{2(X-25) (25≦X<30), 10 (30≦X)} +

{X (35≦X<40), 5 (40≦X)} +

{-2 (37≦X)} + {-2 (39≦X)} +

{X-45 (45≦X<46), 1 (46≦X)} +

{-1 (48≦X)}

 

以下にjavascriptのコードを示します。

※ ≦ は実際は<= です


const KanaToUnicode = function(Row, Stg, Opt){

  const X = 5*Row + Stg;

  return 12353 + X +

    X *(0≦X<20) + 20 *(20≦X) +

    (X-17) *(17≦X<18) + 1 *(18≦X) +

    2(X-25) *(25≦X<30) + 10 *(30≦X) +

    X-35 *(35≦X<40) + 5 *(40≦X) +

    (-2) *(37≦X) + (-2) *(39≦X) +

    (X-45) *(45≦X<46) + 1 *(46≦X) +

    (-1) *(48≦X);

}


 

ちなみにもう少し短く出来ます。

X-17 (17≦X<18)とか0に決まってるので、そういった無駄なのを省略したり、行の範囲は直接行で表現したり、三項演算子というのを使ったりすると


const KanaToUnicode = function(Row, Stg, Opt){

  const X = 5*Row + Stg;

  return 12353 + X +

    (Row<4 ? : 20) + (18≦X) +

    (Row==5 ? 2*Stg : 10 *(5<Row)) +

    (40≦X) + (46≦X) - (48≦X);

}


こんなにすっきりします。

どちらも実行すると上手くいくのが分かります。

Step2: 変数変換

ではこの関数の逆変換を求めていきます。

ここで、12353が邪魔なので、

新たに「規格化Unicode値」NU = U-12353を定義します。

するとNU = X + ∑ε で表せる訳ですね。

補正項のうち、範囲の表現にXが残っているのは厄介なのでNUで表し直します。

その方法ですが、この式のXに開始値, 終了値を代入する事でこれが直接NUの範囲になります。

 

例えば、あ行〜た行の0≦X<20は

  • X=0   で NU=0
  • X=20 で NU=41 より

0≦NU<41といった具合です。

順々に求めていって丸ごと置き換えるだけです。

Step3: 式変形

最後は変数分離を行うのみです。

ただし、場合分け関数なのでこのまま変形するのは非常に非効率的です。

まずは式を1つに絞ります。

つまり、この式のまま実装し、先にNUを代入する事で場合分けを解除します。

後はXが残るので方程式を解かせるプログラムを組めば完了です。数学じゃないので1発公式にする必要は無い訳です。

ここで、Xの係数kと定数cをカウントする関数に路線チェンジします。

するとNU = kX + c ∴ X = (NU - c)/k で表すことが出来ます。

kで割り切れない事がありますが、これはオプションが原因です。NU-cをkで割った余りがちょうどオプションの番号に相当します。

よってオプションもXも求まったため、Unicode値を3つのパラメーターに分解できたという事です。

♢完成です!♢

以下にjavascriptのコードを示します。


const KanaFromUnicode = function(U){

  const NU = U - 12353;

  var k = 1;

  var c = 0;

  //あ行〜た行

  if(0≦NU<41)

    k+=1;

  if(41≦NU)

    c+=20;

  //っ

  if(34≦NU<37){

    k+=1;

    c-=17;

  }

  if(37≦NU)

    c+=1;

  //は行

  if(46≦NU<61){

    k+=2;

    c-=50;

  }

  if(61≦NU)

    c+=10;

  //や行

  if(66≦NU<72){

    k+=1;

    c-=35;

  }

  if(72≦NU)

    c+=5;

  if(68≦NU)

    c-=2;

  if(70≦NU)

    c-=2;

  //わ行

  if(77≦NU<79)

    c+=1;

  if(79≦NU){

    k+=1;

    c-=45;

  }

  //集計

  const kX = NU-c;

  const Opt = kX%k;

  const X = (kX-Opt)/k;

  const Stg = X%5;

  const Row = (X-Stg)/5;

  return {Row, Stg, Opt};

}


長いです。短くする方法検討中です。

 

使用例としては、

KanaFromUnicode関数でRow,Stg,Optを取得して、

  • KanaToUnicode(Row,Stg,0)を行えば濁点が落とせます
  • KanaToUnicode(Row,2,Opt)を行えば同じ行のイ段が取得できます

凄い便利です。

 

ここから下は、自分でも何か編み出したい方が参考にして頂ければ幸いです。

天才だから思いついたでは片付けません。

読めば分かりますが天才的な事は一切ありません。至って自然な成り行きです。

章末にまとめがあります。

発見及び導出の過程

苦労や感動,素晴らしさや魅力を伝えようとすると無駄に長くなるものです。

余談終わり。

KanaFromUnicode関数→KFU関数

KanaToUnicode関数→KTU関数

まず逆変換という発想についてですが、いきなりKFU関数を考えようとすると手も足も出ませんでした。

そのため、参考程度にその逆演算であるKTU関数を考える事にしました。

それでもKFU関数には歯が立たなかったので直接KTU関数を改造できないか考えたという経緯です。

Step1

歯が立たなかったのには理由があります。

かな→Unicodeは3つの引数から1つの値を導くのに対し、かな←Unicodeについては1つの引数から3つの値を導く必要があるのです。

普通に考えたら圧倒的情報不足です。

ここで、Xという定義が登場します。

Xは

KTU関数作成時に「ひらがなをひらがならしく」の名目上、オプションを一切無視した考え方が欲しく、5*行+段を導入したところ頻出したので(書くのが面倒になって)定義した訳ですが、これも1価の数値なのです。

にも関わらず、5で割った商と余りがそれぞれ行と段になる事に気づき、パラメーター数を増やす事は可能な事を確信しました。

その理由が、0≦段<5という条件にある事も分かります。

少なくとも、行と段はXから求めれば良さそうなので、Xとオプションを求める問題に帰着しました。

Step2

とりあえずXを求めてしまいたかったのでオプションは後で考えます。

KTU関数が完成した時点では補正項の範囲はXで表されていました。Xが不明な状態で、補正値がXの範囲で表されると 補正値の選び方も不明になるので、これをUnicodeの範囲に直す必要がありました。

ただ、直そうにもXについて解く前なので(開始≦X<終了)に直接代入,変形する事は出来なそうです。

どうしたら良いでしょうか?

最も頭を抱えた問題ですが、意外にも簡単な方法で解決出来ました。

とりあえずここで邪魔な12353を排除してすっきり考えたかったのでNUが登場しました。

そして、既に解説済みですがXが分かっていればUnicodeを求める事はできる状態なので、Xの開始,終了の値を代入してやればUnicodeの開始,終了の値になるのです。

ここで、Xについての範囲を(開始≦X<終了)と表現する必要がある事にも気づきました。

例えばX値で言うと「つ」は(X=17)も(17≦X<18)も同じですが、これらをUnicodeに直したとき

  • 前者は「つ」しか指定できないのに対して
  • 後者は「つ」,「づ」,「っ」を全て指定できる事が分かります。

(16<X≦17)としなかったのは、(X=17)が指定するのは「つ」であり、「づ」や「っ」がその後ろに続くためです。

そのため「て」の直前で範囲を〆める事で狙い通り指定できると考えました。

Step3

ここまでで構築の体系化までを済ませ、最後に変数分離を行う所まで来ました。

ここで、場合分けを残したまま式変形を施すのはエグかったので先にNUを代入して方程式を1通りに決めるとの事でした。

ここでkとcをカウントする方針に変えることが出来た理由をお話して終わりたいと思います。

1番の理由は、僕が簡単にコンピューターに方程式を解かせれる事を知っていたからだと思います。知っていたからこそすんなり妥協出来たのだと考えています。

もう1つ理由を挙げるとすれば、コンピューター関数は数学関数とは別物であると認識した事でしょうか。

まとめると
  • 無理そうでも案外出来る→ある程度足掻いてみよう
  • グループ化は便利→効率化を意識しよう
  • 成り行きだけで案外何とかなる→試行錯誤を重ねよう
  • 知ってる事が多いと有利→勉強しよう
  • 似てても得意分野が違ったりする→あるものは利用しよう

こんな所でしょうか。割とありがちですね

 

おわりに

お疲れ様でした。

ここまで読んでいただき、心より感謝致します。

今回の内容は気に入って頂けたでしょうか。もし役に立った,参考になった或いは為になったのであれば尚幸いです。

ご意見,ご指摘があればコメントよりお願いします。

それではまたお会いしましょう。