にせねこメモ

はてなダイアリーがUTF-8じゃないので移ってきました。

OpenTypeフォントでFizzBuzz

(2015-05-10 GSUBの設定をよりシンプルにしました。次を参照ください:
OpenTypeフォントでFizzBuzz(その2) - にせねこメモ )


OpenType フォントには、パターンマッチによって文字の列を置換する機能があります(GSUB)。これを利用して FizzBuzz を表示するフォントを作成しました。

FizzBuzz

FizzBuzzとは、1から順に数え上げて行くが、3の倍数のときはFizz、5の倍数のときはBuzz、3と5の倍数のときはFizzBuzzと言う、という言葉遊びです。ある人がプログラミングができるかどうかを調べるための問題としてこれをプログラム上に実装させることがしばしば使われます。


OpenTypeフォントには数を数え上げる機能がないので、数字が与えられた時に、その数字が3または5の倍数であるかを判定して、適切な表示を行うこととします。

例えば、

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20

という文字列をそのフォントで表示させた場合

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz, 16, 17, Fizz, 19, Buzz

と表示されるようにする、ということです。

実行例

以下に実際に動かしている例を示します。 Firefox、あるいはChrome, SafariなどのWebkitブラウザで表示するとFizzBuzzが正しく表示されるはずです。スマートフォンからの場合はこちらのページをご覧ください。

コピーしてテキストエディタ等に貼り付けてみて下さい。

1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100
また、下のテキストエリアに数字を入力して動作を確認することもできます。

制限

  • 32ケタまでの数にしか対応していません。それより大きい数ではFizz、Buzzは出力されますが先頭の数字が消えずに残ります。
  • 正の整数にしか対応していません。

サンプルデータ

Download fizzbuzz_1.01.zip (2015-05-05版)

サンプルデータでは M+ OUTLINE FONTS の mplus-1p-regular.ttf の字形データを利用しています。

サンプルデータについて

  • fizzbuzz_1.01.otf が完成したフォントファイルです。
    • このフォントファイルをインストールし(FizzBuzz font regular)、‘liga’を適用して数字を表示させるとFizzBuzzが動くはずです。
  • fizzbuzz_gsub-excluded.otf はフィーチャーファイルを適用する前のフォントデータ
  • fizzbuzz.fea はフィーチャーファイルです。
  • makefont.py は Python スクリプトで、 FontForgePython から利用することによって fizzbuzz_gsub-excluded.otf に fizzbuzz.fea を適用して fizzbuzz_1.01.otf を作成します。

ここではフィーチャファイルの適用に FontForge を利用していますが、同様のフォントオーサリングソフトでも可能だと思います。なお FontForgeGUIからフォントを出力したところGSUBが正常に出力されませんでした。

解説

グリフの用意

3の倍数の判定のため、先頭からその位置までの各位の数字の和を3で割った余りを記録させる必要があり、そのためのグリフを追加しています。(<数字の英語名>_<0,1,2>mod3 という名前にしてあります)

普通の数字(@num_normal) 0 1 2 3 4 5 6 7 8 9
剰余0(@num_0) 00 10 20 30 40 50 60 70 80 90
剰余1(@num_1) 01 11 21 31 41 51 61 71 81 91
剰余2(@num_2) 02 12 22 32 42 52 62 72 82 92

また、 Fizz, Buzz, FizzBuzz を表示するためのグリフ Fizz.liga, Buzz.liga, FizzBuzz.liga を追加しています。
加えて、 不要な数字を消すための何も表示されない幅0のグリフ nullchar を追加しています。

GSUBの設定

サンプルデータの fizzbuzz.fea ファイルによってGSUBを定義しています。詳しくは当該ファイルを参照してください。

3の倍数の判定

まず3の倍数の判定をします。3の倍数の判定は、「各位の数字の和が3で割り切れれば、その数は3の倍数」ということによっています。
先頭から数を追っていって、先頭からその桁までの数字の和の3で割った余りを記録します。これを記録するために3状態を表現できるようグリフを追加してあります。

状態遷移は次図のようになります。
f:id:nixeneko:20150504003551p:plain
これに沿って、前の数字の状態(@num_0, @num_1, @num_2 クラス)から現在見ている数字の置換先(それぞれ @num_after_0, @num_after_1, @num_after_2 クラス)を定義していて、それによって置換することで3で割った余りを記録します。

今の数字 0 1 2 3 4 5 6 7 8 9
前が剰余0(@num_0)の時の置換先@num_after_0 00 11 22 30 41 52 60 71 82 90
前が剰余1(@num_1)の時の置換先@num_after_1 01 12 20 31 42 50 61 72 80 91
前が剰余2(@num_2)の時の置換先@num_after_2 02 10 21 32 40 51 62 70 81 92

check_mod3:

substitute @num_0 @num_normal' by @num_after_0;
substitute @num_1 @num_normal' by @num_after_1;
substitute @num_2 @num_normal' by @num_after_2;

substitute @num_normal_0mod3 @num_normal' by @num_after_0;
substitute @num_normal_1mod3 @num_normal' by @num_after_1;
substitute @num_normal_2mod3 @num_normal' by @num_after_2;

前半3行が前の数字の状態によって今の数字の置換を行うためのもの、後半3行については数字の最初に適用するためのもの(最初に状態を記録するためのもの)です。

これを 1234567890987654321 に適用すると次図のようになります。
f:id:nixeneko:20150504014210p:plain

普通の数字をなくす

change_first_glyf:

substitute @num_normal_0mod3' @num_notnormal by @num_0_0mod3;
substitute @num_normal_1mod3' @num_notnormal by @num_1_1mod3;
substitute @num_normal_2mod3' @num_notnormal by @num_2_2mod3;

ここで2桁以上の数については先頭の桁のみ普通の数字になっているので、それを「3の剰余が記録された数字」に置換えます。実際には、次の文字が「3の剰余が記録された数字」である普通の数字を「3の剰余が記録された数字」に置き換えています。
これによって2桁以上の数には普通の数字が含まれなくなります。また、1桁の数は普通の数字のまま残ります。
f:id:nixeneko:20150504015520p:plain

一桁の数字の置き換え

when_single_digit:

substitute [\three \six \nine] by \Fizz.liga;
substitute \five by \Buzz.liga;

ここで普通の数字として残っているのは一桁の数字だけなので、ここで普通の数字を、 3, 6, 9 については Fizz 、 5 については Buzz に入替えます。これによって1桁の数に対応しています。

最後の桁を残して普通の数字に戻す

clean_without_last:

substitute @num_0' @num_notnormal by @num_normal;
substitute @num_1' @num_notnormal by @num_normal;
substitute @num_2' @num_notnormal by @num_normal;

最後の桁以外について、普通の数字に戻します。これによって最後の桁だけが「3の剰余が記録された数字」になります。
f:id:nixeneko:20150504020802p:plain

Fizz Buzz の判定

check_fizzbuzz:

substitute [\zero_0mod3 \five_0mod3] by \FizzBuzz.liga;
substitute @num_0_not0mod5 by \Fizz.liga;
substitute [\zero_1mod3 \five_1mod3 \zero_2mod3 \five_2mod3] by \Buzz.liga;
substitute @num_1_not0mod5 by @num_normal_not0mod5;
substitute @num_2_not0mod5 by @num_normal_not0mod5;

3の剰余が記録された数字(最後の桁)について、その剰余が0かそれ以外か(=3の倍数かどうか)と、その数字が「0, 5」かそれ以外か(=5の倍数かどうか)を見て、最後の桁を適切に Fizz, Buzz, FizzBuzz, あるいは普通の数字に置換えます。

置換元 00 10 20 30 40 50 60 70 80 90
置換先 FizzBuzz Fizz Fizz Fizz Fizz FizzBuzz Fizz Fizz Fizz Fizz
置換元 01 11 21 31 41 51 61 71 81 91
置換先 Buzz 1 2 3 4 Buzz 6 7 8 9
置換元 02 12 22 32 42 52 62 72 82 92
置換先 Buzz 1 2 3 4 Buzz 6 7 8 9

f:id:nixeneko:20150504022832p:plain

数字の消去

最後の文字が数字ではなく Fizz, Buzz, FizzBuzz のいずれかであった場合は、それ以前の数字を消す必要があります。そのため、数字を段階的に nullchar (幅0で何も表示要素のないグリフ) に置き換えて、数字が表示されないようにします。


delete_figures_2pow4: Fizz,Buzz,FizzBuzzのいずれか(@chr_str)の前に16個の数字が並んでいる場合、その最初の数字を nullchar にして非表示にする

substitute @num_normal' @num_normal @num_normal @num_normal 
	    @num_normal @num_normal @num_normal @num_normal
	    @num_normal @num_normal @num_normal @num_normal
	    @num_normal @num_normal @num_normal @num_normal 
	    @chr_str by \nullchar;

delete_figures_2pow3: 数字でない文字(@chr_notnum)の前に8個の数字が並んでいる場合、その最初の数字を nullchar にして非表示にする

substitute @num_normal' @num_normal @num_normal @num_normal
	    @num_normal @num_normal @num_normal @num_normal
	    @chr_notnum by \nullchar;

delete_figures_2pow2: 数字でない文字(@chr_notnum)の前に4個の数字が並んでいる場合、その最初の数字を nullchar にして非表示にする

substitute @num_normal' @num_normal @num_normal @num_normal
	    @chr_notnum by \nullchar;

delete_figures_2pow1: 数字でない文字(@chr_notnum)の前に2個の数字が並んでいる場合、その最初の数字を nullchar にして非表示にする

substitute @num_normal' @num_normal @chr_notnum by \nullchar;

delete_figures_2pow0: 数字でない文字(@chr_notnum)の前に1個の数字が並んでいる場合、その数字を nullchar にして非表示にする

substitute @num_normal' @chr_notnum by \nullchar;

f:id:nixeneko:20150504030637p:plain

Fizz, Buzz, FizzBuzz を表示できるよう分解

decomp_str:

substitute \FizzBuzz.liga by \F \i \z \z \B \u \z \z;
substitute \Fizz.liga by \F \i \z \z;
substitute \Buzz.liga by \B \u \z \z;

最後に、 Fizz, Buzz, FizzBuzz の字(合字)を個別のアルファベットに分解して(F, i, z, z, B, u, z, z)、表示できるようにします。
f:id:nixeneko:20150504032003p:plain

(2015-05-05 これをするとAdobe Illustratorなどで正しく表示されない様なので、 Fizz, Buzz, FizzBuzz の字自体に図形をもたせて表示させるように変更しました。)


実際に 1234567890987654321 がどのように表示されるか確認してみましょう:

1234567890987654321

参考

(2015-05-05 Adobe Illustrator などで正常に動作しなかったテストデータおよび解説等を修正)
(2016-10-11 Google DriveのWebホスティング機能の廃止により正しく見えない状態になっていたのを修正)