zig で DateTime のライブラリを作って公開してみた
1. はじめに
zigはまだ安定版がリリースされていない言語 (2024/9/16現在: v0.13.0) ですが、Zenn でも様々な記事が書かれ少しずつ目にする機会が増えていたのでずっと気になっていました。
今回 zig を触ってみる中で、タイムスタンプと日付を取り扱う DateTime のライブラリを実装して公開しました。
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 由来の独自フォーマットみたいです) に以下のような記載が自動的に記載されます。
ハッシュを含めて記載されていますね。今回はブランチ指定なので、適宜上のコマンドを再実行することで、ブランチの先頭に上書きされます(たぶん)。
.dependencies = .{
.datetime = .{
.url = "git+https://github.com/ymd-h/datetime-zig.git#master",
.hash = "1220a1b3dc0fbebf34ecaf5b637b9178dd227e93f64c6f9e0aa3ed1d2d86dd270480",
},
},
更に、ここからは手動でビルドスクリプトの 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 プログラム内での使い方
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 のライブラリを実装して公開しました。
興味をもっていただけたら、使ってみてもらえると嬉しいです。
今は「時刻 (タイムスタンプ)」しか取り扱っていないですが、「時間」を扱う操作なんかも実装したいなぁと考えています。(それより早いタイミングでバージョン切るほうがよさそうですかね。)
Discussion