SATySFi でイオニア式記数法

これは[SATySFi Advent Calendar 2018](https://adventar.org/calendars/3418)の4日目の記事です。（5日目は[@bd_gfngfn](https://twitter.com/bd_gfngfn)さんです。）

先行例として、[@hanachin_『SATySFiで漢数字』](https://qiita.com/hanachin_/items/57efc97a9d3fb921fa92)、[@nekketsuuu『kansuji-of-int』](https://nekketsuuu.github.io/satysfi-my-soul/code-kansuji-of-int.html)のように漢数字を表示するものはあるが、古代ギリシャ語・現代ギリシャ語で使われるイオニア式記数法はいまだ無いようである。本稿は古代ギリシャ語による現代の出版物にて最も普通に用いられているものについて述べる。

成果物は https://github.com/na4zagin3/SATySFi-grcnum にある。2018年12月3日現在、テスト文書を処理するには[最新の SATySFi](https://github.com/gfngfn/SATySFi) を使う必要があり、また、TheanoOldStyle という名のフォントを要する。「[SATySFi 用のフォントライブラリを作ってみた](https://qiita.com/na4zagin3/items/627599ca746756e93bc1)」にフォントのインストール方法があるので、参考にしてくださると嬉しく思う。

注意：本稿においては、十進数表記において 3 桁区切り（例 123,456）ではなく 4 桁区切り（例 1234,5678）を用いる。日本語同様、古代ギリシャ語も万進法を用いており、好都合だからである。

![screenshot.png](https://qiita-image-store.s3.amazonaws.com/0/20044/aa9341e5-e5e1-b624-f8e1-b8b61ca3e9d2.png)


## イオニア式記数法について
ギリシャ数字とも呼ばれる。ローマ数字（I, II, III等）とは大きく異なり、各文字に割当てられている数価を用いて表す。例えば、123 は $123 = 100+20+3$ であるため、ρ ($=100$) と κ ($=20$) と γ ($=3$) を並べ、ρκγʹ となる。ここで、「ʹ」は語の表記ではなく数の表記であることを表すものである。（中世写本では $\overline{\text{ρκγ}}$ のように上線が用いられ、「ʹ」は分数に用いられていたのであるが、現代では単に「ʹ」を用いる慣習である。）

### 1 から 9999 まで
1 から 900 までは、昔の文字を含めたギリシャ文字 27 字を用いて表す。
1000 から 9000 に対しては 1 から 9 までの数価を持つ文字に「͵」を前置したものを用いる。

|$n$|数価 $n$ の文字|数価 $10n$ の文字|数価 $100n$ の文字|数価 $1000n$ の文字|
|---|---|---|---|---|
|1|α|ι|ρ|͵α|
|2|β|κ|σ|͵β|
|3|γ|λ|τ|͵γ|
|4|δ|μ|υ|͵δ|
|5|ε|ν|φ|͵ε|
|6|ϛ|ξ|χ|͵ϛ|
|7|ζ|ο|ψ|͵ζ|
|8|η|π|ω|͵η|
|9|θ|ϟ|ϡ|͵θ|

### 1,0000 から 9999,9999 まで
1万以上の数は、日本語や中国語の中数の「万」と同じく、$n\times 10^4 + m\ (n,m < 10^4)$ の様に表す。例えば、12,0003 は $\overset{ιβ}{M}γ'\ (ιβ=12,\ M=1,0000,\ γ=3)$ となる。

## 実装
https://github.com/na4zagin3/SATySFi-grcnum の実装の解説を行う。長いので、時間が有り余っている人以外は「[比較](#%E6%AF%94%E8%BC%83)」の節まで飛ばして欲しい。

### ユーティリティー関数
```satysfi
  % s が空文字列でない時のみ f を s に適用
  let apply-non-empty f s = match s with
    | ` ` -> s
    | s -> f s

  % 10進数表記での桁毎の配列に変換
  % >>> explode-into-digits 1
  % [1]
  % >>> explode-into-digits 123
  % [1; 2; 3]
  let explode-into-digits : int -> int list | n =
    let-rec sub
      | acc n = if n >= 10 then sub ((n mod 10) :: acc) (n / 10) else n::acc
    in
    sub [] n

  % 所謂zip
  % >>> zip [`a`; `b`] [1; 2]
  % [(`a`, 1); (`b`, 2)]
  let-rec zip : 'a list -> 'b list -> ('a * 'b) list
    | (x :: xs) (y :: ys) = (x, y) :: (zip xs ys)
    | _ _ = []

  % 配列を n 要素づつに分割
  % >>> split-by 2 [1; 2; 3; 4; 5]
  % [[1; 2]; [3; 4]; [5]]
  let split-by : int -> 'a list -> 'a list list | n =
    let-rec sub
      | xss xs _ [] = List.reverse ((List.reverse xs) :: xss)
      | xss xs 0 ys = sub ((List.reverse xs) :: xss) [] n ys
      | xss xs n (y :: ys) = sub xss (y :: xs) (n - 1) ys
    in
    sub [] [] n

  % List の要素を繰り返す
  % >>> repeat 2 [`a`; `b`]
  % [`a`; `b`; `a`; `b`]
  let repeat n xs =
    let-rec sub
    | 0 acc = acc
    | n acc = sub (n - 1) (xs :: acc)
    in
      List.concat (sub n [])

  % String を繰り返す
  % >>> repeat-string 2 `ab`
  % `abab`
  let repeat-string n str =
    List.fold-left (^) ` ` (repeat n [str])

  % 長い方を返す
  % >>> max-length 1. 2.
  % 2.0
  let max-length l1 l2 =
    if l1 >' l2
    then l1
    else l2

  % Haskell の Maybe.catMaybes 相当
  let concat-maybe =
    let-rec sub
    | acc [] = List.reverse acc
    | acc ((None) :: xs) = sub acc xs
    | acc ((Some x) :: xs) = sub (x :: acc) xs
    in
      sub []

  % Haskell の List.intersperse 相当
  let intersperse c =
    let-rec sub
    | [] = []
    | (x :: []) = [x]
    | (x :: xs) = x :: c :: sub xs
    in
      sub
```

### 1,0000 未満の表記
1,0000 未満の表記については、一般の `string` として表すことができるので、`string` に変換する。

#### 文字の定義
```satysfi
  let s-gnls = `͵`
  let ones = [` `; `α`; `β`; `γ`; `δ`; `ε`; `ϛ`; `ζ`; `η`; `θ`]
  let tens = [` `; `ι`; `κ`; `λ`; `μ`; `ν`; `ξ`; `ο`; `π`; `ϟ`]
  let hundreds = [` `; `ρ`; `σ`; `τ`; `υ`; `φ`; `χ`; `ψ`; `ω`; `ϡ`]

  let thousands = List.map (apply-non-empty (fun s -> s-gnls ^ s)) ones

  let number-symbols = [ones; tens; hundreds; thousands]
```
これにより、`number-symbols` は ``[[` `; α`; `β`;...]; [` `; `ι`;...]; [` `; `ρ`;...]; [` `; `͵α`;...]]`` となる。

#### 1000 未満の数を変換
```satysfi
  % 1000 未満の数についての表記
  % >>> simple-digits 1234
  % `͵ασλδ`
  let simple-digits : int list -> string | rds =
    let symbol-digits = List.reverse (zip rds number-symbols) in
    List.fold-left (^) ` ` (List.map (fun (d, ss) -> List.nth d ss |> Option.from `!`) symbol-digits)
```
桁毎に文字に変換する。

### 1,0000 以上の表記
1,0000 以上の表記については、今回は縦に積む必要があるので、`inline-text` に変換する。

与えられた数を十進表記にし、4桁の塊毎に文字表記に変換する。最下位の塊はそのまま、その一つ前の塊は Μ の上に載せる。

#### `inline-text` に関するユーティリティー関数
```satysfi
  let-inline \show-int n = embed-string (arabic n)
  let-inline \show-string s = embed-string s
  let-inline \show-inline-text s = s

  % b1 の上に b2 を中央で揃えて載せる。
  let overhang b1 b2 =
    let (l1, _, _) = get-natural-metrics b1 in
    let (l2, _, _) = get-natural-metrics b2 in
    let total-length = max-length l1 l2 in
    let side-kern1 = inline-skip ((total-length -' l1) *' 0.5) in
    let side-kern2 = inline-skip ((total-length -' l2) *' 0.5) in
    let bs = line-stack-bottom [side-kern1 ++ b1; side-kern2 ++ b2] in
    script-guard Latin (no-break bs)

  % フォントをギリシャ語用のものに変更
  let-inline ctx \greek it =
    let ctx =
      ctx |> set-font Latin (`TheanoOldStyle`, 1., 0.)
          |> set-font OtherScript (`TheanoOldStyle`, 1., 0.)
    in
    read-inline ctx it
```

#### M
1 億以上の数を表すには標準的な方法が無い（$10^{4n}$　の　$n$　を　M　と共に書いたりする）のだが、今回は M の数を横に増やし、$\overset{ιβ}{MM}'$　のようにすることとした。

```satysfi
  let-inline \myriad-symbol = embed-string `Μ`

  % Μ を n 回繰り返したものの上に、str を載せ、両者とも中央揃えにする。 
  let-inline ctx \stack-myriad n str =
    let size = get-font-size ctx in
    let inter-space = inline-skip (size *' 0.1) in
    let ctx-coeff =
      ctx |> set-font-size (size *' 0.5)
          |> set-manual-rising  (size *' 0.2)
    in
    let myriad-symbol = embed-string (repeat-string n `Μ`) in
    let myriads-ib = read-inline ctx myriad-symbol in
    let coeffs-ib = read-inline ctx-coeff (embed-string str) in
    let (_, coeffs-ht, _) = get-natural-metrics coeffs-ib in
    overhang coeffs-ib myriads-ib

  % m が空なら空、そうでなければ n 回 M を繰り返したものの上に m を載せる。
  let-rec render-myriad-it
    | 0 m = Some (embed-string m)
    | _ ` ` = None
    | n m =
    Some {\stack-myriad(n)(m);}
```

#### 1,0000 以上の数を変換
遂に準備は整った。数を十進法表記し、4桁毎に区切り、それぞれイオニア式表記に変換した上で M の上に載せる (render-myriad-it)。

`{\show-inline-text(x);\show-inline-text(y);}` は、単に `inline-text` 型の文字列 `x` と `y` と結合するものである。

```satysfi
  let it-aristarchos n =
    % 数を十進数表記したものを4桁毎に区切る
    let ds = explode-into-digits n in
    let rds = List.reverse ds in
    let ms = List.map simple-digits (split-by 4 rds) in

    % 4桁の塊毎に文字表記に変換する
    let myriads = List.reverse (List.mapi render-myriad-it ms) in
    let myriads = concat-maybe myriads in

    % 文字表記に変換した塊を繋ぎ合せ、最後に数字を表す記号を付加
    let inter-myriad = {\inline-skip-em(0.2);} in
    let number-symbol = embed-string s-keraia in
    let myriads = List.append (intersperse inter-myriad myriads) [number-symbol] in
    List.fold-left (fun x y -> {\show-inline-text(x);\show-inline-text(y);}) {} myriads

    % 文字表記に変換したものをギリシャ語用フォントで出力
  let-inline \grcnum n =
    let sa = string-aristarchos n in
    {\greek{\show-inline-text(it-aristarchos rds);}}
```

# 比較
普段、あまり $\TeX$ 芸はしないのだが、これを $\TeX$ で書くのはいかにも大変だろう。ここでは、万表記しない部分（[grcnum.satyh 8行目から43行目](https://github.com/na4zagin3/SATySFi-grcnum/blob/9a5032f85f8b10d6e9e1290e6fde13f77c3a8217/grcnum.satyh#L8-L43)）と、大方同じく動作する Polyglossia の $\TeX$ 実装（[gloss-greek.ltf 238行目から285行目](https://github.com/reutenauer/polyglossia/blob/ebeec205a844b9f2e7df015095f42aeb8134324c/tex/gloss-greek.ldf#L238-L285)）と比較してみよう。

双方共に行数は同程度であるが、SATySFi 実装には、`zip` や `explode-into-digits` など汎用的に使い回せる関数が半分を占める。対して、$\TeX$ 実装を、部分的に切り出して他の用途に使うことは難しい。関数型言語による部品化の容易さによるものである。

```
% https://github.com/na4zagin3/SATySFi-grcnum/blob/9a5032f85f8b10d6e9e1290e6fde13f77c3a8217/grcnum.satyh#L8-L43
  let s-keraia = `ʹ`
  let s-gnls = `͵`
  let ones = [` `; `α`; `β`; `γ`; `δ`; `ε`; `ϛ`; `ζ`; `η`; `θ`]
  let tens = [` `; `ι`; `κ`; `λ`; `μ`; `ν`; `ξ`; `ο`; `π`; `ϟ`]
  let hundreds = [` `; `ρ`; `σ`; `τ`; `υ`; `φ`; `χ`; `ψ`; `ω`; `ϡ`]

  let apply-non-empty f s = match s with
    | ` ` -> s
    | s -> f s

  let thousands = List.map (apply-non-empty (fun s -> s-gnls ^ s)) ones

  let number-symbols = [ones; tens; hundreds; thousands]

  let explode-into-digits : int -> int list | n =
    let-rec sub
      | acc n = if n >= 10 then sub ((n mod 10) :: acc) (n / 10) else n::acc
    in
    sub [] n

  let-rec zip : 'a list -> 'b list -> ('a * 'b) list
    | (x :: xs) (y :: ys) = (x, y) :: (zip xs ys)
    | _ _ = []

  let split-by : int -> 'a list -> 'a list list | n =
    let-rec sub
      | xss xs _ [] = List.reverse ((List.reverse xs) :: xss)
      | xss xs 0 ys = sub ((List.reverse xs) :: xss) [] n ys
      | xss xs n (y :: ys) = sub xss (y :: xs) (n - 1) ys
    in
    sub [] [] n

  % takes rev digits
  let simple-digits : int list -> string | rds =
    let symbol-digits = List.reverse (zip rds number-symbols) in
    List.fold-left (^) ` ` (List.map (fun (d, ss) -> List.nth d ss |> Option.from `!`) symbol-digits)

% let \greeknumber n = n |> explode-into-digits |> simple-digits
```

```
% https://github.com/reutenauer/polyglossia/blob/ebeec205a844b9f2e7df015095f42aeb8134324c/tex/gloss-greek.ldf#L238-L285
\def\greeknumber#1{%
  \ifnum#1<\@ne\space\gr@ill@value{#1}%
  \else
    \ifnum#1<10\expandafter\gr@num@i\number#1%
    \else
      \ifnum#1<100\expandafter\gr@num@ii\number#1%
      \else
        \ifnum#1<\@m\expandafter\gr@num@iii\number#1%
        \else
          \ifnum#1<\@M\expandafter\gr@num@iv\number#1%
          \else
            \ifnum#1<100000\expandafter\gr@num@v\number#1%
            \else
              \ifnum#1<1000000\expandafter\gr@num@vi\number#1%
              \else
                \space\gr@ill@value{#1}%
              \fi
            \fi
          \fi
        \fi
      \fi
    \fi
  \fi
}
\def\Greeknumber#1{%
  \expandafter\MakeUppercase\expandafter{\greeknumber{#1}}}
\let\greeknumeral=\greeknumber
\let\Greeknumeral=\Greeknumber
\def\gr@num@i#1{%
  \ifcase#1\or α\or β\or γ\or δ\or ε\or Ϛ\or ζ\or η\or θ\fi
  \ifnum#1=\z@\else\anw@true\fi\anw@print}
\def\gr@num@ii#1{%
  \ifcase#1\or ι\or κ\or λ\or μ\or ν\or ξ\or ο\or π\or ϟ\fi
  \ifnum#1=\z@\else\anw@true\fi\gr@num@i}
\def\gr@num@iii#1{%
  \ifcase#1\or ρ\or σ\or τ\or υ\or φ\or χ\or ψ\or ω\or ϡ\fi
  \ifnum#1=\z@\anw@false\else\anw@true\fi\gr@num@ii}
\def\gr@num@iv#1{%
  \ifnum#1=\z@\else ͵\fi
  \ifcase#1\or α\or β\or γ\or δ\or ε\or Ϛ\or ζ\or η\or θ\fi
  \gr@num@iii}
\def\gr@num@v#1{%
  \ifnum#1=\z@\else ͵\fi
  \ifcase#1\or ι\or κ\or λ\or μ\or ν\or ξ\or ο\or π\or ϟ\fi
  \gr@num@iv}
\def\gr@num@vi#1{%
  ͵\ifcase#1\or ρ\or σ\or τ\or υ\or φ\or χ\or ψ\or ω\or ϡ\fi
  \gr@num@v}
```

# 跋
いよいよ明日は SATySFi の生みの親、gfn 先生による SATySFi の目玉新機能「SATySFiのMarkdown入力機能について」です。明日が待ち遠しゅうございますね。