D言語で九九表を表示


Twitterで、九九表を表示するプログラム(Java)を見かけたのでD言語でもやってみた

0. まずは文字列をベタ書き

D
void multiplicationTable0()
{
    import std.stdio;

    "* | 1  2  3  4  5  6  7  8  9
-----------------------------
1 | 1  2  3  4  5  6  7  8  9
2 | 2  4  6  8 10 12 14 16 18
3 | 3  6  9 12 15 18 21 24 27
4 | 4  8 12 16 20 24 28 32 36
5 | 5 10 15 20 25 30 35 40 45
6 | 6 12 18 24 30 36 42 48 54
7 | 7 14 21 28 35 42 49 56 63
8 | 8 16 24 32 40 48 56 64 72
9 | 9 18 27 36 45 54 63 72 81"
    .writeln;
}

最速のコード
改行がそのまま表示されるのが何気に便利
学校の課題をこれで提出したら(たぶん)やり直し

1. foreachで繰り返す

D
void multiplicationTable1()
{
    import std.stdio;

    "* |%2s".writef(1);
    foreach (num; 2 .. 10)
        " %2s".writef(num);

    "\n-----------------------------".writeln;

    foreach (col; 1 .. 10) {
        "%s |%2s".writef(col, col);
        foreach (row; 2 .. 10)
            " %2s".writef(col * row);
        writeln;
    }
}

素直に繰り返しの部分をforeachにしたバージョン
writef(C言語のprintf相当)の書式指定子は楽ちん

処理の流れをながめていると、大まかに3つの処理がある
(行見出しの表示、区切り線の表示、九九本体の表示)
ので、なんとなく分けて書いてみる

ロジックをわかりやすく書き直したい

上のforeach版でもわかりやすいが、順番に並んでいる要素の最初や最後だけ
特別扱いをしてあげないといけない部分で、もやっとする

例えば行見出しの部分で、1の手前には空白が1文字、2~9の手前には空白が2文字
空いているのが気になってしまう

要は順番に並んでいる要素の、間にだけ空白を入れたいだけなのだ

2. 標準ライブラリを使用する

こういう一般的な悩み(?)は、標準ライブラリで何とかなる場合が多い
std.stringjoinがそのままの機能なので、
次のように書き換えてみた

D
void multiplicationTable2()
{
    import std.stdio;
    import std.string    : format, join;
    import std.range     : iota, repeat, tee, No;
    import std.algorithm : map, each;

    (
        "* |" ~                   // * |
        iota(1, 10)               //  1  2  3  4  5  6  7  8  9
            .map!(num => num.format!"%2s")
            .join(" ")

    ).writeln;

    "-".repeat(29).join.writeln;  // -----------------------------

    iota(1, 10)                   // # |## ## ## ## ## ## ## ## ##
        .tee!(col => "%s |".writef(col), No.pipeOnPop)
        .each!(col => iota(1, 10)
            .map!(row => (col * row).format!"%2s")
            .join(" ")
            .writeln);
}

ついでにforeachiotamap/eachteeで置き換え
意味もなくrepeatなども使用している

3. 標準出力への書き込み回数を減らす

ここまできたらwriteを一回だけにしたい
1つの大きな文字列を作ってもいいが、ごちゃごちゃしそうなので
まずは、これまでの3つのまとまりをそれぞれ要素とする配列を作成して
最後にすべての要素を改行でjoinする方向でやってみよう
(ついでにマジックナンバーを少し整理)

D
void multiplicationTable3()
{
    import std.stdio;
    import std.conv      : to;
    import std.string    : format, join;
    import std.range     : iota, repeat;
    import std.algorithm : map;

    enum E = 9;
    enum S = E * 3 + 1 + 1;
    [
        "* |" ~              // * |
        iota(1, E + 1)       //  1  2  3  4  5  6  7  8  9
            .map!(num => num.format!"%2s")
            .join(" "),

        "-".repeat(S).join,  // -----------------------------

        iota(1, E + 1)       // # |## ## ## ## ## ## ## ## ##
            .map!(col => col.to!string ~ " |" ~
                iota(1, E + 1)
                    .map!(row => (col * row).format!"%2s")
                    .join(" "))
            .join("\n")

    ].join("\n").writeln;
}

表の大きさを変更できるようにしたい……が

9x9以外にも20x20なども表示できるようにするにはどうしよう
九九の表そのものは、実行時に引数で大きさを渡せば何とでもなりそうだが
問題はformatの書式指定子の部分がコンパイル時に決まることだ。

自前で必要な表示桁数を計算して、空白を補ってもいいが
ここはD言語らしくコンパイル時に解決する方向で考えよう

4. テンプレート引数

D言語のテンプレート関数には、型名の他に文字列などコンパイル時に決まるものを
テンプレート引数として渡すことができる
これを使って、書式指定子の%2s%3s%4sなどに書き換える

D
void multiplicationTable4(const int E = 9)()
{
    import std.stdio;
    import std.conv      : to;
    import std.string    : format, join;
    import std.range     : iota, repeat;
    import std.algorithm : map;

    enum R = E.to!string.length.to!string;         // 行見出しの文字数
    enum C = (E ^^ 2).to!string.length.to!string;  // 列見出しの文字数
    enum S = E * (C.to!int + 1) + R.to!int + 1;    // 区切り線の長さ
    [
        "*".format!("%" ~ R ~ "s |") ~         // * |
        iota(1, E + 1)                         //  1  2  3  4  5  6  7  8  9
            .map!(num => num.format!("%" ~ C ~ "s"))
            .join(" "),

        "-".repeat(S).join,                    // -----------------------------

        iota(1, E + 1)                         // # |## ## ## ## ## ## ## ## ##
            .map!(col => col.format!("%" ~ R ~ "s |") ~
                iota(1, E + 1)
                  .map!(row => (col * row).format!("%" ~ C ~ "s"))
                  .join(" "))
            .join("\n")

    ].join("\n").writeln;
}

この関数を呼び出すときは
関数名!(テンプレート引数)(実行時引数)
という形式で呼び出すことになる
※実行時引数がない場合、()は省略できる

D
void main()
{
    multiplicationTable4();       // 9x9
    multiplicationTable4!(5)();   // 5x5
    multiplicationTable4!(20);    // 20x20
    multiplicationTable4!39;      // 39x39
}

5. CTFE

よく考えたらコンパイル時に表示できそう

D
auto multiplicationTable5(const int E = 9)()
{
    // 前略
    return
    [
        "*".format!("%" ~ R ~ "s |") ~         // * |
    // 中略
    ].join("\n");
}
D
pragma(msg, multiplicationTable5); // コンパイル時

void main()
{
    import std.stdio;
    multiplicationTable5.writeln;  // 実行時
}

おわり

format部分は、テンプレートmixinにした方が見やすいかも
D言語で書けたのでとりあえず満足