オフィスアワーがそろそろ始まるよ!()

文字列のフォーマット

Zenで利用できる文字列のフォーマットを説明します。大きく分けると2つのトピックがあります。

  1. 文字列にフォーマットして出力する
  2. 文字列から変換する

1.はZenのソースコード内で文字列を作成したい場合に利用します。std.debug.warnはこちらに属します。2.は文字列のデータを、別の形式に変換する際に利用します。例えば、ユーザーから入力された文字列の"1"を整数型の1に変換する処理です。

これらの文字列を処理するコードは、std.fmtモジュール内に実装されています。std.debugモジュールやstd.ioモジュールは、std.fmtモジュールを利用して文字列をフォーマットした後、対象のファイル (標準出力、標準エラーを含む) に文字列を書き込みます。

文字列にフォーマットして出力する

本書内では、std.debug.warn関数を用いて標準エラーに文字を出力するコードを多く掲載しています。このwarn関数はテンプレート文字列と可変長の引数から、フォーマットされた文字列を標準エラーに出力します。

例えば、次のようなコードを書くと、Hello Worldと標準エラーに出力されます。

    std.debug.warn("Hello {}\n", "World");

ここで、最初の文字列Hello {}\nはテンプレート文字列と呼ばれます。テンプレート文字列内の{}プレースホルダであり、テンプレート文字列に続く引数がこの位置に表示されることを意味します。

このテンプレート文字列と可変長引数から、文字列を作り出す処理は、std.fmt.format関数が担っています。

フォーマットAPI一覧

std.fmt.format関数を利用して文字列をフォーマットして出力するAPIには、以下のものがあります。

  • std.fmt.bufPrint: 与えたバッファにフォーマットされた文字列を出力します
  • std.fmt.allocPrint: 与えたアロケータからバッファを確保し、フォーマットされた文字列を出力します
  • std.debug.warn: 標準エラーにフォーマットされた文字列を出力します
  • std.debug.printError: warnと同様ですが、error:のプレフィックスが付き、赤色の文字で出力します
  • std.debug.printHint: warnと同様ですが、hint:のプレフィックスが付き、シアン色の文字で出力します
  • std.debug.printInfo: warnと同様ですが、info:のプレフィックスが付き、緑色の文字で出力します

std.fmt.bufPrint / std.fmt.allocPrint

プログラム内でフォーマットされた文字列を扱いたい場合に利用します。

bufPrint[]u8のバッファを与え、そのバッファに文字列を出力します。バッファの長さが不足する場合、EndOfStreamエラーを返します。

examples/ch13-io/format/src/format_api.zen:6:13

test "bufPrint" {
    var buf: [16]u8 = undefined;
    const formatted = try fmt.bufPrint(&buf, "Hello {}", "World");
    equalSlices(u8, "Hello World", formatted);

    const e = fmt.bufPrint(&buf, "too long string for the buffer");
    err(error.EndOfStream, e);
}

allocPrintはバッファの代わりにアロケータを第一引数で与えます。メモリ確保に失敗すると、OutOfMemoryエラーを返します。

warn / printError / printHint / printInfo

フォーマットされた文字列を標準エラーに出力します。warn以外は、それぞれプレフィックスが付き、メッセージの文字が着色されます。

std.fs.write.print

std.fs.Fileのインスタンスを対象に、フォーマットされた文字列を出力します。例えば、標準出力にフォーマットされた文字列を出力する場合、次のようにします。

examples/ch13-io/format/src/format_api.zen:15:18

pub fn main() !void {
    const stdout = try std.fs.getStdOut();
    try std.fs.write.print(stdout, "Hello {}\n", "World");
}
$ zen run format_api.zen 
Hello World

プレースホルダ

プレースホルダ{}はテンプレート文字列内に複数個置くことができます。後ろに続く可変長引数が順番に埋められていきます。

examples/ch13-io/format/src/placeholder.zen:3:5

test "placeholder" {
    // output: 0, one, 2.0e+00
    std.debug.warn("{}, {}, {}\n", u32(0), "one", f64(2.0));

Zenのプリミティブ型は、標準ライブラリ内でデフォルトのフォーマットで出力されます。ユーザー定義の構造体も、デフォルトでは次のように各フィールドが出力されます。

examples/ch13-io/format/src/placeholder.zen:51:60

test "format struct print" {
    const Struct = struct {
        x: f64,
        y: f64,
    };

    const s = Struct { .x = 1.0, .y = 2.0 };
    // output: Struct{ .x = 1.0e+00, .y = 2.0e+00 }
    std.debug.warn("{}\n", s);
}

タグ付き共用体も同様に、ヴァリアントとその値が出力されます。

examples/ch13-io/format/src/placeholder.zen:62:76

test "format tagged union" {
    const Value = union(enum) {
        String: []u8,
        F64: f64,
        U32: u32,
    };

    const string = Value { .String = &"hello" };
    // output: Value{ .String = hello }
    std.debug.warn("{}\n", string);

    const float = Value { .F64 = 1.2345 };
    // output: Value{ .F64 = 1.2345e+00 }
    std.debug.warn("{}\n", float);
}

エラー共用体は、エラー型の値が格納されている場合はエラーの種別、そうでない場合、格納されている値が出力されます。

examples/ch13-io/format/src/placeholder.zen:78:90

test "format error union print" {
    const Error = error {
        AnError,
    };

    const err: Error!u32 = Error.AnError;
    // output: error.AnError
    std.debug.warn("{}\n", err);

    const value: Error!u32 = u32(1);
    // output: 1
    std.debug.warn("{}\n", value);
}

ポインタはプリミティブ型と構造体とで、振る舞いが異なります。プリミティブ型は{}型名@16進数表記アドレスの形式で出力されます。

examples/ch13-io/format/src/placeholder.zen:92:97

test "format pointer print" {
    const x: u32 = 0;
    // `@`に続くアドレスは実行ごとに異なります
    // output: u32@20c65c
    std.debug.warn("{}\n", &x);
}

構造体のポインタ型は、{}では通常の構造体の値と同じ出力が、{*}型名@16進数表記アドレスの形式で出力されます。

examples/ch13-io/format/src/placeholder.zen:99:110

test "format struct pointer print" {
    const Struct = struct {
        x: f64,
        y: f64,
    };

    const s = Struct { .x = 1.0, .y = 2.0 };
    // output: Struct{ .x = 1.0e+00, .y = 2.0e+00 }
    std.debug.warn("{}\n", &s);
    // output: Struct@20c230
    std.debug.warn("{*}\n", &s);
}

プレースホルダ内には、様々なオプションを記述することができます。

配置指定

{n}n番目の位置にある引数を指定します。引数は0から数えます。

examples/ch13-io/format/src/placeholder.zen:7:8


    // output: zero, one, zero, one
    std.debug.warn("{}, {}, {0}, {1}\n", "zero", "one");
桁数

数値を桁数を指定して出力します。{:n}nに桁数を指定します。

examples/ch13-io/format/src/placeholder.zen:12:13

    // output: |01234|00001|
    std.debug.warn("|{:5}|{:5}|\n", u32(1234), u32(1));
小数点桁数

小数点以下第何位まで出力するかを指定します。{:.n}nに桁数を指定します。

examples/ch13-io/format/src/placeholder.zen:17:18

    // output: 3.14159
    std.debug.warn("{:.5}\n", f64(3.14159265359));
書式指定

プレースホルダ{}内には、文字列を出力する際の書式指定を記述することが可能です。

examples/ch13-io/format/src/placeholder.zen:22:48

    // {c}: 一文字のASCII文字として表示
    // output: A
    std.debug.warn("{c}\n", u8(65));

    // {b}: 2進数として表示
    // output: 11110001001000000
    std.debug.warn("{b}\n", u32(123456));

    // {x}: 小文字の16進数として表示
    // output: 1e240
    std.debug.warn("{x}\n", u32(123456));

    // {X}: 大文字の16進数として表示
    // output: 1E240
    std.debug.warn("{X}\n", u32(123456));

    // {e}: 浮動小数点を指数形式で表示
    // output: 1.23456e+02
    std.debug.warn("{e}\n", f64(123.456));

    // {d}: 整数 / 浮動小数点を10進数として表示
    // output: 123.456
    std.debug.warn("{d}\n", f64(123.456));

    // {s}: NULL文字終端された文字列 (C言語の文字列) を表示
    // output: hello
    std.debug.warn("{s}\n", c"hello");

ユーザー定義

構造体のフォーマット出力を自分で定義することも可能です。その場合、構造体に所定のシグネチャを持つpubで公開されたformatメソッドを定義します。

例えば、次のような座標値xyをフィールドにもつPoint構造体を、(x, y) = (xの値, yの値)のような形式でフォーマット出力したいとします。

const Point = struct {
    x: f64, y: f64,
};

formatメソッドの実装は、次のようになります。引数が多くてわかりずらい部分がありますが、重要なことは、プレースホルダーの情報が渡されるので、その情報を元にstd.fmt.formatを呼び出す、ということです。

examples/ch13-io/format/src/user_define.zen:4:22

const Point = struct {
    x: f64, y: f64,

    pub fn format(
        self: Point,
        comptime fmt_spec: []const u8,
        options: fmt.FormatOptions,
        context: var,
        comptime Errors: type,
        output: fn (@typeOf(context), []const u8) Errors!void,
    ) Errors!void {
        return fmt.format(context,
                          Errors,
                          output,
                          "(x, y) = ({d:.3}, {d:.3})", // テンプレート文字列
                          self.x,
                          self.y);
    }
};

std.fmt.formatの第4引数はテンプレート文字列で、第5引数以降はテンプレート文字列のプレースホルダーを埋めるための引数です。

このPoint構造体の値をフォーマット出力すると、自身で定義した形式でフォーマットされて出力されます。

examples/ch13-io/format/src/user_define.zen:24:28

test "user defined format" {
    const point = Point { .x = 1.0, .y = 2.0 };
    // output: (x, y) = (1.000, 2.000)
    std.debug.warn("{}\n", point);
}

文字列から変換する

文字列を解析し、整数型や浮動小数点型に変換します。コマンドラインから入力を受け付けたり、ファイルからデータを読み取る場合に利用できます。これを実現する標準ライブラリ関数は、std.fmtモジュールにある次の関数です。

  • parseInt: 符号付き整数の形式で表現された文字列を、任意の整数型に変換します
  • parseUnsigned: 符号なし整数の形式で表現された文字列を、任意の整数型に変換します
  • parseFloat: 文字列を任意の浮動小数点型に変換します

parseInt / parseUnsigned

parseIntの関数シグネチャは、fn parseInt(comptime destType: type, string: []const u8, radix: u8) !destTypeです。第一引数がターゲットとする整数型、第二引数が文字列スライス、第三引数が基数です。基数は、2〜36進数の中から選択できます。

parseIntで正常に整数値が得られる使用例は次の通りです。ターゲットとする整数型には、符号付き / 符号ないずれも選択でき、任意ビット幅を指定できます。また、数字の前に+ / -が入っていてもかまいません

examples/ch13-io/format/src/parse_int.zen:6:27

test "parseInt" {
    const a = try fmt.parseInt(i32, "10", 10);
    ok(a == 10);

    const b = try fmt.parseInt(i32, "+10", 10);
    ok(b == 10);

    const c = try fmt.parseInt(i32, "-10", 10);
    ok(c == -10);

    const d = try fmt.parseInt(u32, "10", 10);
    ok(d == 10);

    const e = try fmt.parseInt(u32, "1001", 2);
    ok(e == 9);

    const f = try fmt.parseInt(u32, "FF", 16);
    ok(f == 255);

    const g = try fmt.parseInt(u5, "31", 10);
    ok(g == 31);
}

ターゲットとする整数型が符号なしの場合には、-が入った文字列は変換が失敗し、error.InvalidCharacterが返ってきます。スペース () を含む、数値として変換できない文字列がある場合も、error.InvalidCharacterが返ってきます。文字列を変換した結果が、ターゲットとする整数型内に収まらない場合も変換に失敗し、error.Overflowが返ってきます。

examples/ch13-io/format/src/parse_int.zen:29:38

test "fail to parseInt" {
    const invalid_minus_sign = fmt.parseInt(u32, "-10", 10);
    err(error.InvalidCharacter, invalid_minus_sign);

    const invalid_char = fmt.parseInt(u8, " 10", 10);
    err(error.InvalidCharacter, invalid_char);

    const overflow = fmt.parseInt(u8, "256", 10);
    err(error.Overflow, overflow);
}

parseUnsigned-から始まる文字列を受け付けないことを除き、parseIntと同等です。

parseFloat

parseFloatの関数シグネチャは、fn parseFloat(comptime destType: type, s: []const u8) !destTypeです。第一引数はターゲットとする浮動小数点型で、f16 / f32 / f64 / f128のいずれかです。第二引数は文字列スライスです。

parseFloatの利用例を以下に示します。文字列は指数表現でも変換できます。nan+inf / -infも変換可能です。

examples/ch13-io/format/src/parse_int.zen:40:74

test "parseFloat" {
    const approxEq = std.math.approxEq;
    const epsilon = 1e-7;

    // ターゲットとして指定できる浮動小数点型
    inline for ([_]type{ f16, f32, f64, f128 }) |floatType| {
        const zero = try fmt.parseFloat(floatType, "0");
        ok(zero == 0.0);

        const plus_zero = try fmt.parseFloat(floatType, "+0");
        const minus_zero = try fmt.parseFloat(floatType, "-0");
        ok(plus_zero == 0.0);
        ok(minus_zero == 0.0);

        const pi = try fmt.parseFloat(floatType, "3.141");
        const minus_pi = try fmt.parseFloat(floatType, "-3.141");
        ok(approxEq(floatType, pi, 3.141, epsilon));
        ok(approxEq(floatType, minus_pi, -3.141, epsilon));

        // 指数表現
        const exp = try fmt.parseFloat(floatType, "1.23456e+2");
        ok(approxEq(floatType, exp, 123.456, epsilon));
    }

    // NaNも変換可能
    // NaN同士は比較できないため、ビットレベルで同じことをテスト
    const nan = try fmt.parseFloat(f64, "NaN");
    ok(@bitCast(u64, nan) == @bitCast(u64, std.math.nan(f64)));
 
    // `inf` / `-inf`も変換可能
    const inf = try fmt.parseFloat(f64, "inf");
    const minus_inf = try fmt.parseFloat(f64, "-inf");
    ok(inf == std.math.inf(f64));
    ok(minus_inf == -std.math.inf(f64));
}

☰ 人の生きた証は永遠に残るよう ☰
Copyright © 2018-2019 connectFree Corporation. All rights reserved.
Zen, the Zen three-circles logo and The Zen Programming Language are trademarks of connectFree corporation in Japan and other countries.