CLI with Rustで幸せになる話、タイムゾーンを添えて (original) (raw)

自己紹介


平和な日本の時間事情


日本はいいぞ、なんてったって、単一のタイムゾーンで夏時間が存在しないからね!

日本標準時JST)は、日本国内で公式に採用されている標準時であり、協定世界時(**UTC)に9時間**を加えた時間。

UTCの魔の手がやってきた!!!


あるCloudサービスを使っていたときに、「時刻設定はUTCで指定できる」と言われた。

える、知っているか、UTCで指定できるということは、JSTでは指定できないんだよ。

手軽かつ安全にTimezoneの変換したい


Timezoneを使う機会はだいたい、コードの実行するタイミングを指定するタイミングでやってくる。時間を間違えると、他の更新日時の関係で動かないコードがあれば、大惨事に。

設定ミスはしたくないが、いちいちブラウザを開くのも面倒。

SystemのPythonに変更を加えたくない


CLIで手軽に使えるものはないか」とChatGPTに聞いたら、Python製のソフトウェアをお勧めされた。しかし、globalでpipを使うと月日とともに崩壊する未来があるので、systemのPythonはできれば使いたくない。

とはいえ、それ以外の方法で作ると過剰な気がする。

Pythonにご飯を食べさせてもらっている立場なので文句を言うつもりはない

補足: Pythonの開発環境が年々良くなっている話(余ったら話す)

少し前まで、Docker経由が前提となりがちだったPythonだが、uv によるプロジェクトの一元管理が可能になったため、docker経由しなくても十分開発できるようになりつつある。

uvとは、次世代Python Package Managerのryeの作者であるMitsuhiko氏とastral社がタッグを組んで開発をしているPython Projectのマネージャー。RustのCargoに近い。

uvでは以下の要素を管理できる。

docs.astral.sh


汎用言語のインタープリターが強力な手段であることは、理解しつつも、仕事で使うツールであれば、持ち運びのしやすいバイナリ形式、特に単一のバイナリ形式で欲しい。(雑に usr/local/bin/ に突っ込んだら動いてほしい)

せっかくなので作ることに。

補足: インタープリタ型言語の実行方法

Pythonなどのインタープリタ型の言語(特にASTを利用する言語)の場合、以下のような工程をインタープリタというソフトウェアが行い実行される。

※ 実際には、JITコンパイル(Just In Compile)というプログラムの実行時にバイトコード中間言語をネイティブコード(機械語)に動的にコンパイル(変換)する技術があり、JavaScriptRubyなど主要言語で採用されている。 ※ オペコードを生成しなくても動かすことは可能、例えば、ASTを巡回しながら順番に実行する方法がある。

Rustを採用した背景


Rustを使った背景としては以下の要素がある。

github.com

個人開発

自分用のツールだったので、Rustを導入しやすかった。

チームで使うツールであれば、(よほどの理由がない限り)チームメイトが慣れている言語や、それに近い言語を選定する傾向があるので、ここは大きいと思う。

単一のバイナリで動かせる

持ち運びが便利なツールが欲しくて、自作した。つまり、簡単に使える単一バイナリのツールが欲しかった。

使いやすい入力パーサーがある

Rustには、使いやすいCLIのユーザー入力のパーサーがあり、開発がしやすい。

use clap::Parser;

/// Simple program to greet a person #[derive(Parser, Debug)] #[command(version, about, long_about = None)] struct Args { /// Name of the person to greet #[arg(short, long)] name: String,

/// Number of times to greet
#[arg(short, long, default_value_t = 1)]
count: u8,

}

fn main() { let args = Args::parse();

for _ in 0..args.count {
    println!("Hello {}!", args.name);
}

}

https://github.com/clap-rs/clap

この件以来、RustでCLIを作ることが多くなったので、どう使うのがいいか色々検証している。最近の趣味のプロジェクト(AST巡回型言語)だと以下のようなReceiverを作ってwrapしている。

※ これが最適という主張ではないので注意

use clap::{Arg, ArgAction, ArgMatches, Command};

pub(super) struct Receiver {}

impl Receiver { pub(super) fn new() -> Self { Receiver {} }

pub(super) fn receive(&self) -> ArgMatches {
    let command: Command = Receiver::load_command_settings();
    command.get_matches()
}

fn load_command_settings() -> Command {
    Command::new("rast")
        .about("AST Based Language")
        .author("shunsock")
        .version("0.1.0")
        .arg(Arg::new("expression").required(false).short('e'))
        .arg(Arg::new("file").required(false).short('f'))
        .arg(
            Arg::new("debug")
                .short('d')
                .long("debug")
                .action(ArgAction::SetTrue)
                .help("Enable debug mode"),
        )
}

}

自分が使える

すぐに使いたかったので、普段書いている言語から選んだ。選択肢は3つで、気分で決めた。

開発秘話的な


サマータイム(DST: Daylight Saving Time)

GitHubでPublicに公開した後、RedditというSNSで意見を募った。インストールしてくれた人がいて、「コーナーケースでバグあったよ」と教えてくれた。

サマータイム(夏時間)とは、夏季の一定期間に時計を通常の標準時より1時間進める制度です。これにより、夕方の明るい時間を有効活用し、エネルギーの節約や生活の質の向上を図ることが目的とされています。

サマータイムの開始時(春)

サマータイムの終了時(秋)

サマータイム開始時(春に時計を1時間進めるとき)

サマータイム終了時(秋に時計を1時間戻すとき)

サマータイム対応

タイムゾーンの変換は、ChronoChrono Tzというライブラリを使っている。このライブラリでは、タイムゾーンの変換が一対一でないことを前提に、 Single, Ambiguous , None という計算結果の値を用意している。

Timezone Translatorでもこの実装を採用し、エラーハンドリングを行っている

pub(crate) fn convert(&self) -> Result<DateTime, TranslationError> {

let mapped: MappedLocalTime<DateTime<Tz>> = self.from_tz.from_local_datetime(&self.time);

match mapped {
    LocalResult::Single(time) => Ok(time.with_timezone(&self.to_tz)),
    LocalResult::Ambiguous(time_earliest, time_latest) => {
        Ok(select_time_with_ambiguous_time_strategy(
            self.ambiguous_time_strategy,
            self.to_tz,
            time_earliest,
            time_latest,
        ))
    }
    LocalResult::None => {
        let error = TranslationError::TranslationError {
            time: self.time,
            from_tz: self.from_tz,
            to_tz: self.to_tz,
        };
        Err(error)
    }
}

}

github.com

タイムゾーンが入力されていないときの挙動

タイムゾーンの入力がめんどくさい人むけに時間の自動入力をサポートしています。主に自分。

pub(crate) fn command_provider() -> Command { let now: String = provide_local_timezone_string(); let now_str: &'static str = Box::leak(now.into_boxed_str());

Command::new("tzt - Timezone Translator")
    .version("0.3.0")
    .author("shunsock")
    .about("translate time from one timezone to another")
    .arg(time())
    .arg(from(now_str))
    .arg(to(now_str))
    .arg(ambiguous_time_strategy())

}

変数を渡すことでデフォルトの値を設定している。

use clap::Arg;

pub(crate) fn from(timezone: &'static str) -> Arg { Arg::new("from_timezone") .short('f') .long("from") .value_name("FROM_TIMEZONE") .help("The original timezone (e.g. America/New_York) @see https://docs.rs/chrono-tz/latest/chrono_tz/enum.Tz.html") .required(false) .default_value(timezone) }

実は、この機能気合いでやっていたりします。環境変数を見たり、システムのタイムゾーンのファイルを見に行ったりします。

pub(crate) fn provide_local_timezone_string() -> String {

let env_var_tz: Option<String> = EnvironmentVariableTzProvider::new(None).get_env_var_tz();
if let Some(env_var_tz) = env_var_tz {
    return env_var_tz;
}


let tz_from_etc_localtime: Option<String> = get_system_timezone_from_etc_localtime();
if let Some(tz_from_etc_localtime) = tz_from_etc_localtime {
    return tz_from_etc_localtime;
}


let tz_from_etc_timezone: Option<String> = get_system_timezone_from_etc_timezone();
if let Some(tz_from_etc_timezone) = tz_from_etc_timezone {
    return tz_from_etc_timezone;
}

let error_message = "System Timezone Not Found:
Could not find local timezone. Please set TZ environment variable.
";
panic!("{}", error_message);

}

https://github.com/shunsock/timezone_translator/blob/main/src/infrastructure/current_local_timezone_provider/local_timezone_string_provider.rs

まとめ

参考にしている本とか

Rustのプログラミングは公式ドキュメントとこの本で学びました。

amzn.to

途中でプログラミング言語の話が出てきたので、こちらも載せておきます。

amzn.to

JavaとCですが、参考になると思います。

amzn.to