にせねこメモ

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

OpenTypeフォントで万年カレンダーをつくる

はじめに

カレンダーの一つの月の日付の並びには、各曜日始まりの7種×日数の種類(28, 29, 30, 31日)の4種で合計28種ある。祝日などを考えなければ28種を使いまわすことで任意の年月に対応できる。

そんな発想で作られた、使いまわしできるカレンダーを万年カレンダーという(日数の違いにまで対応してるとは限らないが)。
という訳で、せっかくなのでこの万年カレンダーOpenTypeフォントで実装してみる。

2016-01とか2016/01とか書くと2016年1月のカレンダーが表示されるようにしたい。

完成品

f:id:nixeneko:20170211002007p:plain

Webフォントによる表示

2017/02

Javascriptでカウントアップさせてみる:

2000/01

たのしい!

ダウンロード

ソースコード

GitHubに挙げたのでそちらを参照。
github.com

  • Acalendar.ttf: フォントデータ
  • Acalendar.sfd: Fontforgeファイル (GSUB, GPOSの設定なし)
  • calendar.fea: GSUB, GPOSの定義

仕様・制限

OpenType機能の'liga', 'mkmk'を有効にして表示する。
“2017/02”, “12345/6” などといった、

(年)/(月)

の形に対応している。

  • 年は正数にしか対応していない*1
  • 6桁以上の年数はOpenTypeの'mkmk'フィーチャを有効にできる環境でないとうまく年が表示できない。
  • 年が15桁以上だと月の表示と被る可能性がある。
  • 月は2桁または1桁に対応している。
  • スラッシュはハイフンでもよい。

正規表現で表すと

\d+[-/]\d{1,2}

となる。

方針

togetter.com
ここで色々検討されているが、カレンダーが28種類のなかからどれを使うかは、

  • その年の元日の曜日は何か
  • その年は閏年かどうか
  • 何月か

によって決まる。
元日の曜日は400年ごとに同じ並びが繰り返され、次のような計算式で計算できるっぽい。(なお、日曜のoffset=3は間違い)

閏年かどうかは、西暦の年の数字が

  • 400で割り切れるときは閏年
  • 400で割り切れない、かつ、100で割り切れるときは閏年ではない
  • 400で割り切れない、かつ、100で割り切れない、かつ、4で割り切れるときは閏年
  • それ以外は(4で割り切れないとき)は閏年ではない

という風に判定でき、

  1. 4で割り切れる
  2. 100で割り切れる
  3. 400で割り切れる

かどうかの3つを組み合わせて判定できる。


OpenTypeのGSUBによる置換を使って剰余等の計算を真面目に行ってもいいのだろうが、非常に面倒臭そうだし、状態の数が結構な数になってしまいそうなので、今回は西暦の年を400で割った余りについて、400通りの置換を定義することで行う。3桁未満にも対応できるように実際には置換の数はもっと増える(というか、499個)。

実装

グリフの用意

実装してみる。
アウトラインとしてM+ Fontsの mplus-1p-medium.ttf をベースにした。

まずは始まる曜日の違い(7種)と月の日数の違い(4種)で28種類のカレンダーを用意する。
最初にグリフの用意。
日曜始まりでカレンダーを用意した。
始まる曜日と日数で、グリフ名を

cal_mon_28

など、

cal_(曜日)_(日数)

とした。

GSUBの実装

詳しくは、GitHubに上げた calendar.fea を参照されたい。


最初に年について400の剰余を計算する。
これは要するに、百の位以上の桁について4の剰余を計算すればよい。

4の剰余を表すための状態の数だけ状態に対応する数字のグリフを作成する。

数字 0 1 2 3 4 5 6 7 8 9
0 mod 4 00 10 20 30 40 50 60 70 80 90
1 mod 4 01 11 21 31 41 51 61 71 81 91
2 mod 4 02 12 22 32 42 52 62 72 82 92
3 mod 4 03 13 23 33 43 53 63 73 83 93

実際には zero_0mod4, one_0mod4, …, nine_0mod4, …, zero_3mod4, …, nine_3mod4 などという名前のグリフを用意した。


数字の並びについて、まず前に数字が続かないもの、つまり数字の最初の桁について置換をおこなう。3桁の数字の最初の文字について次のように置換をおこなう。

元の数字 0 1 2 3 4 5 6 7 8 9
置換先 00 11 22 33 40 51 62 73 80 91

これは lookup check_first_fig_mod4 で行っている。


次に、4の剰余の状態を持った数字の次に来る3つの数字のうちの1番目の数字について置換を行う。
つまり、
mod4の数字 普通の数字 普通の数字 普通の数字
という並びの時に太字にした“普通の数字”を対応するmod4の数字に置換する。

元の数字 0 1 2 3 4 5 6 7 8 9
前が剰余0か2の時の置換先 00 11 22 33 40 51 62 73 80 91
前が剰余1か3の時の置換先 02 13 20 31 42 53 60 71 82 93

これは lookup check_mod4 で行った。
これで年の400の剰余が求まった。


さて、次に、年の始まりの曜日と閏年かどうかの14状態を判定する。これは、その14状態に対応するスラッシュ“/”のグリフを用意し、年と月の間のスラッシュ“/”(あるいはハイフンでもいい)に対し置換を行うこととする。これはスラッシュの前3桁の数字について400通り(数字が2桁、1桁しかない場合を加えて499通り)の置換をずらずら列挙することになる。うっ頭が…。
これは lookup check_firstday_and_leapyear で実装した。気合、といった感じ。
状態をもつスラッシュのグリフ名は

slash_mon_leap
slash_tue_comm

などとした。(mon, tue, wed, thu, fri, sat, sun)×(leap, comm)


置換後、スラッシュが14の状態をもっている。閏年はl, 閏年でない年はnで示した。
スラッシュの14状態(一番左の列)に対して、後続する数字(一番上の行)によって次の表のようにカレンダーを表示する。表記したのはカレンダーに含まれる日数と開始曜日である。

01 02 03 04 05 06 07 08 09 10 11 12
日, n 31日 28水 31水 30土 31月 30木 31土 31火 30金 31日 30水 31金
月, n 31月 28木 31木 30日 31火 30金 31日 31水 30土 31月 30木 31土
火, n 31火 28金 31金 30月 31水 30土 31月 31木 30日 31火 30金 31日
水, n 31水 28土 31土 30火 31木 30日 31火 31金 30月 31水 30土 31月
木, n 31木 28日 31日 30水 31金 30月 31水 31土 30火 31木 30日 31火
金, n 31金 28月 31月 30木 31土 30火 31木 31日 30水 31金 30月 31水
土, n 31土 28火 31火 30金 31日 30水 31金 31月 30木 31土 30火 31木
日, l 31日 29水 31木 30日 31火 30金 31日 31水 30土 31月 30木 31土
月, l 31月 29木 31金 30月 31水 30土 31月 31木 30日 31火 30金 31日
火, l 31火 29金 31土 30火 31木 30日 31火 31金 30月 31水 30土 31月
水, l 31水 29土 31日 30水 31金 30月 31水 31土 30火 31木 30日 31火
木, l 31木 29日 31月 30木 31土 30火 31木 31日 30水 31金 30月 31水
金, l 31金 29月 31火 30金 31日 30水 31金 31月 30木 31土 30火 31木
土, l 31土 29火 31水 30土 31月 30木 31土 31火 30金 31日 30水 31金

14 × 12 = 168通り。一桁の数字にも対応するために126通りの置換を追加し計294通りになった。
これは lookup replace_calendar で行った。


さて、カレンダーを表示するのもいいけどせっかくなので何年何月ってのも表示させたい。
月については、事前に月を示す(JAN, FEB, などと表示される)グリフを用意しておき、グリフ幅0で、カレンダーのグリフの次にきたときに丁度いい位置に月の表示がくるように位置調整しておく。
これを、カレンダーのグリフに後続する2桁または1桁の数字に対して置換をおこなうことで、月の表示を可能にしている。
これは lookup replace_month で実装した。


最後に年の表示。数字をspacingなグリフにしてしまうと、並べるのは簡単だが、左側に空白ができてしまう。
という訳で、年の数字専用の幅0のグリフを用意しておき、GSUBで数字をそれらのグリフに置換し、その後GPOSのMarkToMark positioning (タグでいうと'mkmk')を使って位置調整することで年の数字を並べようと考えた。

さて、フォントが一応できたので動作を見てみる。

Adobe InDesign

「文字」パネルプルダウンメニューから「欧文合字」をオンにすると……
f:id:nixeneko:20170208224745p:plain
やったー!

Adobe Illustrator

イラレではどうかな?OpenTypeパネルから欧文合字を有効にして……
f:id:nixeneko:20170208225148p:plain
あれー?
'mkmk'が効いてない。
もし'mark'は効くであれば、'mkmk'じゃなくても'mark'とGSUBによる置換で代用することはできるのだけれど。


今回は、GSUBだけである程度まで動くようにしておく。事前に位置をずらした数字を用意しておいて、GSUBで順番に置換する。5桁あれば十分だろう。それより大きい桁数は'mkmk'を有効にできる環境で使うようにしてもらう。
これによって年を表す数字のグリフが5倍に増える。
このあたりは lookup replace_year_10000, lookup replace_year によってGSUB置換を実装、さらに lookup STACK_YEAR を使って6桁以上の年の数字が右に並ぶようにした。

結果

Adobe Illustrator

f:id:nixeneko:20170209182717p:plain
やったー!
もちろん年が6桁以上になるのは文字が重なってうまく表示できないのだけど(10万年後もグレゴリオ暦が使われてるのか、閏年の計算も同じなのか謎だけど)。

Adobe InDesign

f:id:nixeneko:20170209183216p:plain
もちろんInDesignでは6桁以上も'mkmk'でうまく並ぶ。 10^{13}年後も地球が存在してるのか知らんけど。ちなみに太陽の寿命が 10^{10}年位らしい。

*1:とはいえ、紀元前にグレゴリオ暦を適用してどうするのって気もする……。