ポケベル変換表のコードゴルフ
はじめに
みなさんこんにちは。株式会社ハウテレビジョン CTO のめもりーです。
最近,マネジメントの話やら経営と技術の接続のような記事や登壇が多く,たまには技術的な記事でも書こうかなと思った次第です。
この記事は HowTelevision Advent Calendar 18 日目の記事です。
ポケベルって知ってますか?
ポケベルはポケットベルのことで 80 年代くらいから流行り始めた携帯電話の前進のようなものです。まぁ私は生まれてないんですが。
以下の表をもとに数字で打っていって変換したものが相手側のデバイスに表示されるのです。公衆電話でよく並んだという話も噂には聞きます。
1桁目\2桁目 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 0 |
---|---|---|---|---|---|---|---|---|---|---|
1 | あ | い | う | え | お | A | B | C | D | E |
2 | か | き | く | け | こ | F | G | H | I | J |
3 | さ | し | す | せ | そ | K | L | M | N | O |
4 | た | ち | つ | て | と | P | Q | R | S | T |
5 | な | に | ぬ | ね | の | U | V | W | X | Y |
6 | は | ひ | ふ | へ | ほ | Z | ? | ! | ― | / |
7 | ま | み | む | め | も | ¥ | & | |||
8 | や | ( | ゆ | ) | よ | * | # | 空白 | ||
9 | ら | り | る | れ | ろ | 1 | 2 | 3 | 4 | 5 |
0 | わ | を | ん | ゛ | ゜ | 6 | 7 | 8 | 9 | 0 |
たとえば 6195690169934504
は「ハローワールド」になります。
コードゴルフ
コードゴルフはコードの可読性を犠牲にしてコードをいかに短く書くかを競うプログラミングの遊びです。
昔,PHP カンファレンス福岡の GMO ペパボさんが開催していたイベントで一位を取り 10 万円分のクーポンを頂いたことがあります。
ちなみに私はコードゴルフはめちゃくちゃ好きです。
ポケベルのコードゴルフの実装
ポケベルの話をたまたま会社の歓迎会のときにしていて,ちょっと調べてました。勝手に変換に規則性なくて大変かなと思ってたんですが,調べるにつれてある一定の規則性があることがわかったので「あれ,コードゴルフできそうだな」と思って始めたのが始まりです。
普段は PHP で実装することが多いんですが, 今回は JavaScript で書きました。
console.log("6195690169934504".match(/.{1,2}/g).map(s=>s.split('')).map(([a,b])=>[[162,2],[65],[171,2],[70],[181,2],[117],[191,2,2],[196,2,3],[80],[202],[85],[207,3],90,63,33,45,47,[222,1],53221,38,0,0,0,227,40,230,41,232,42,35,32,0,0,[233],[49],239,[242,1,2],153,154,[54,1,4],48].reduce((a,v,e,u,s)=>(e=v[0]?v[2]??5:1,u=v[0]??v,s=v[1]??1,a.push(...Array(e).fill(0).map((_,i)=>String.fromCodePoint((u>127?12288:0)+u+i*s))),a),[]).reduce((a,s)=>((a.l[a.r++/10|0]||=[]).push(s),a),{l:[],r:0}).l[a--<0?9:a][--b<0?9:b]).join(''))
525 バイトです。もっと短くできそうな気はするんですが,やりすぎるとのめり込んでしまうので一旦これで。ポケベルの仕様上,本来は 200 文字?までしか送れないようなのですが,一旦その制限はなしで。
あと絵文字の部分はゼロフィルにしています。
少し解説してきます。
Unicode CodePoint
ポケベルにはカタカナの Unicode CodePoint である U+30A0
~ U+30FF
が含まれるのと,A〜Z,0〜9,いくつかの記号が許容されています。アルファベットと数字,記号は ASCII コードの範囲内(0x00〜0xFF)です。
最初はベタ打ちしようと思ったのですが,それだけでも純粋に 300 バイト前後食ってしまうのでこの案は受け入れず。圧縮できそうなところは圧縮しています。
例えば,カタカナのコードポイントのア行は以下のように小文字も含まれます:
コードポイント | 読み方 | 詳細 |
---|---|---|
30A1 | ァ | 小書きア |
30A2 | ア | ア |
30A3 | ィ | 小書きイ |
30A4 | イ | イ |
30A5 | ゥ | 小書きウ |
30A6 | ウ | ウ |
30A7 | ェ | 小書きエ |
30A8 | エ | エ |
30A9 | ォ | 小書きオ |
30AA | オ | オ |
カタカナのコードポイントは U+3000
以降ではあるので下二桁だけ取り出し,一部分をスキップさせるような書き方をしています。
本来取り出したい値は U+30A2
, U+30A4
, U+30A6
, U+30A8
, U+30AA
だけのはずです。起点となる U+30A2
に +2 ずつしていったいわば等差数列のような規則性があるということがわかりますね。
これを [162, 2]
と表現することで 7 バイトで表現できます。[UnicodePoint の開始位置, スキップ数, 繰り返し回数]
または 1 ワードで実現しています。
繰り返し回数は,通常 5 文字で充足するので,省略しつつ,ワ行は特殊なので [...239,[242,1,2],...]
のような書き方をしています。
旧カタカナも Unicode Point 上では存在するのでスキップしなくてはいけないのです(U+30F0
, U+30F1
)。
[0x30A2, 0x30A4, 0x30A6, 0x30A8, 0x30AA]
と表現するだけでも 36 バイト使ってしまいますから。
(※バイト数はスペースを除いて算出)
こんな感じにうまく活用していきます。ただ一部特殊パターンもあります。ハ行がわかりやすいでしょうか。
コードポイント | 読み方 | 詳細 |
---|---|---|
30CF | ハ | ハ |
30D0 | バ | バ |
30D1 | パ | パ |
30D2 | ヒ | ヒ |
30D3 | ビ | ビ |
30D4 | ピ | ピ |
30D5 | フ | フ |
30D6 | ブ | ブ |
30D7 | プ | プ |
30D8 | ヘ | ヘ |
30D9 | ベ | ベ |
30DA | ペ | ペ |
30DB | ホ | ホ |
30DC | ボ | ボ |
30DD | ポ | ポ |
このように濁点,半濁点が含まれているんですよね。なので,ハ行の場合は 3 つスキップするように書くために [207,3]
と書いています。
そして下 2 桁のものは,コードポイントから文字に治す際に最終的に 0x3000
を加算すれば良いのです。
String.fromCodePoint((u>127?12288:0)+u+i*s)))
12288
は 0x3000
を10進数に表したものです。これで 1 バイト節約できます。そして一部には ASCII コードが含まれているので,適当な境界値 127 (0x7F)
を指定して,ASCII コードの場合は,0x3000
を加算しないようにしています。
チャンクして,マッピング
このままでは一行になってしまうので 10 桁ごとにチャンクします。
.reduce((a,s)=>((a.l[a.r++/10|0]||=[]).push(s),a),{l:[],r:0}).l
手抜きですが,reduce はこういうのに重宝するので好きです。
こうすることで 1 桁入力目, 2 桁目入力を2次元配列から取ってくるような仕組みにできます。
一方で,ポケベルはゼロベースインデックスではないので少し変換してあげる必要があります。
例えば 60
である場合,配列上で本来マッピングされてほしい位置は一次元目は 5,二次元目は 9 です。それぞれの数字を -1 すればよいんですが, 0 に -1 すると負の数になってしまうので,9 にしてあげる必要があります。
先ほどのチャンクしたコードの真後ろに以下のように付け加えてあげます。
"60".match(/.{1,2}/g).map(s=>s.split('')).map(([a,b])=>...reduce(...).l[--a<0?9:a][--b<0?9:b]
これでポケベル変換表からプログラム上の二次元配列に変換ができるようになりました。
実行結果
実行時はこんな感じで動きます。
おわりに
他の言語でもぜひ。
コードゴルフが好きな方も,興味がない方もカジュアル面談しませんか。お気軽にご連絡またはご応募ください。
Discussion