zig で DateTime のライブラリを作って公開してみた

2024/09/16に公開

1. はじめに

zigはまだ安定版がリリースされていない言語 (2024/9/16現在: v0.13.0) ですが、Zenn でも様々な記事が書かれ少しずつ目にする機会が増えていたのでずっと気になっていました。

今回 zig を触ってみる中で、タイムスタンプと日付を取り扱う DateTime のライブラリを実装して公開しました。

https://github.com/ymd-h/datetime-zig

2. モチベーション

SIMDが使いやすい(?)形で提供されているらしいということで、データ分析関連をテーマに何か実装できないだろうかと試していたところ、タイムスタンプを扱うのが大変だなぁと感じたのがモチベーションでした。

公式のissueに2021年に使いやすい DateTime の要望が挙がっており標準ライブラリに追加するべく開発が進んでいるようですが、未だ完成しているようではありませんでした。

また、検索して見つかるライブラリ zig-datetime の使い方や機能ががあまりしっくりこなかったので、自分で作りたいものをつくることにしました。

3. 使い方

3.1 パッケージマネージャーと依存モジュール

v0.11(?)以降、zig にはパッケージマネージャーが追加され、他のモジュールが使えるようになっています。。。。が、これが大変でした。

公式には十分な説明が見つけられず、公開されている第3者の記事もそのとおりに書いてもうまくいかなかったので、バージョンが進んでAPIが変わってしまったのでしょう。。。(このあたりがまだ安定版がリリースされていない開発段階の厳しいところですね。)

ここでは v0.12 (と v0.13) で確認した内容をベースに記載します。

プロジェクトディレクトリ (v0.12 以降は zig init で作成) で zig fetch コマンドを利用する事で、プロジェクトに依存ライブラリを登録できます。 (まだきちんとしたリリースが出ていないので、master ブランチを記載しています。)

シェル
zig fetch --save=datetime git+https://github.com/ymd-h/datetime-zig.git#master

build.zig.zon (.zon は Zig Object Notation 由来の独自フォーマットみたいです) に以下のような記載が自動的に記載されます。
ハッシュを含めて記載されていますね。今回はブランチ指定なので、適宜上のコマンドを再実行することで、ブランチの先頭に上書きされます(たぶん)。

build.zig.zon
    .dependencies = .{
        .datetime = .{
            .url = "git+https://github.com/ymd-h/datetime-zig.git#master",
            .hash = "1220a1b3dc0fbebf34ecaf5b637b9178dd227e93f64c6f9e0aa3ed1d2d86dd270480",
        },
    },

更に、ここからは手動でビルドスクリプトの build.zig を編集していきます。

build.zig
const std = @import("std");

pub fn build(b: *std.Build) !void {
  const exe = b.addExecutable(
    // 省略
  );

  const datetime = b.dependency(
    "datetime",  // <- build.zig.zon に登録した名前 (= zig fetch で指定した名前)
     .{ .target = target, .optimize = optimize },
  );
  exe.root_module.addImport(
    "datetime", // <- インポート時に使う名前
    datetime.module("datetime"), // <- 配布元で設定しているモジュール名
  ); // これで @import("datetime") ができるようになる。
  // 古い記事だと exe.addModule() 等と出てくるが、エラーになるし reference にも記載がない。
}

3.2 プログラム内での使い方

README.mdから抜粋
const std = @import("std");
const datetime = @import("datetime");
const DateTime = datetime.DateTime;

pub fn main(){
    // Create from Timestamp and TimeZone
    const dt = DateTime.fromTimestamp(.{ .s = std.time.timestamp() }, .{});
    _ = try DateTime.fromTimestamp(.{ .ms = std.time.milliTimestamp() }, .{});
    _ = try DateTime.fromTimestamp(.{ .us = std.time.microTimestamp() }, .{});
    _ = try DateTime.fromTimestamp(.{ .ns = std.time.nanoTimestamp() }, .{});
    _ = try DateTime.fromTimestamp(.{ .s = std.time.timestamp() }, .{ .hour = 2, .minute = 30 });

    // Parse ISO8601 date string
    const dt2 = try DateTime.parse("2024-09-15T11:15:23.987+09:00");

    // Format ISO8601
    var w = std.io.bufferedWriter(std.iogetStdOut().writer());
    const stdout = bw.writer();
    try dt.formatISO8601(stdout, .{ .format = .extended, .resolution = .ms });
    try stdout.print("\n", .{});
    try bw.flush();

    // Custom Format
    try dt.formatCustom(stdout, "%Y/%m/%d %H:%M:%S.%f %:z\n");


    // Compare DateTime
    _ = try dt.earlierThan(dt2);
    _ = try dt.laterThan(dt2);
    _ = try dt.equal(dt2);


    // Get Timestamp (elapsed time from 1970-01-01T00:00:00Z)
    _ = try dt.getTimestamp(); // i64
    _ = try dt.getMilliTimestamp(); // i64
    _ = try dt.getMicroTimestamp(); // i64
    _ = try dt.getNanoTimestamp(); // i128


    // Get day of week
    _ = try dt.dayOfWeek(); // enum { .Sunday, .Monday, .Tuesday, .Wednesday, .Thursday, .Friday, .Saturday }


    // Sort
    var dates = [_]Date{ dt, dt2 };
    try DateTime.sort(&dates, .asc);
    try DateTime.sort(&dates, .desc);
}

4. 開発で苦労した点

  • ISO8601文字列のパース
    • 標準ライブラリに正規表現がなく、結局順番に文字を確認していきました。カスタムフォーマットにも対応したいのですが、ちょっと大変そうなので未実装
  • 文字列へのフォーマット
    • 標準ライブラリ内の format / print 関数群が、フォーマット文字列を comptime (コンパイル時定数) 変数として受け取るので、フォーマット文字列を動的に組み立てる事が (多分) できず、Writer に固定の断片を少しずつ書き出す実装で対応
    • 標準ライブラリ内の Writer は厳密には struct ではなく、型に応じた struct を生成する Generics function で、色々な Writer を使いたい場合、関数の引数の型に指定できない。関数の引数は anytype とするのが正しい (?) らしい (参考)
  • ビルドシステム
    • 上でも書きましたが、モジュールとしての公開と依存パッケージをインポートできるようにするまでが、バージョン変遷で苦労

5. おわりに

zig の習作も兼ねて、タイムスタンプと日付を扱う DateTime のライブラリを実装して公開しました。

https://github.com/ymd-h/datetime-zig

興味をもっていただけたら、使ってみてもらえると嬉しいです。

今は「時刻 (タイムスタンプ)」しか取り扱っていないですが、「時間」を扱う操作なんかも実装したいなぁと考えています。(それより早いタイミングでバージョン切るほうがよさそうですかね。)

Discussion