👼

神話のプログラム言語 Odin 日付操作とログ操作編

2024/12/18に公開

アイキャッチを変更しました。Odin言語の記載を行う時には、これからアイキャッチに、神のマークを入れるようにします。(これが神かどうかは微妙ですが…😭)
それでは、Odinでの日付取得やログ表示などは、どうするのかについて、今回は記載していきます。

1.時間操作

Odin言語での時間関数は、まだ完璧ではないと思います。
何故なら、タイムゾーンの導入も先月2024年11月に導入されたばかりですので、これから基本ライブラリは、もっと良くなっていくと思いますが、今現時点の時間操作方法を説明します。

1-1.タイマー操作:sleep関数

まず、sleep関数ですが、sleepなどの時間関数を扱う場合は、inport "core:time"をインポートして利用します。また、時間の単位については、time.Microsecondなどで単位表示して使う事が出来ます。
以下のようにtime01ディレクトリにmain.odinを作成して実行してみます。

time01/main.odin
package main

import "core:fmt"
import "core:time"

main :: proc() {
  fmt.println("start:", time.now()) // 開始時間 time.now()で現日付を表示
  timer := 100 * time.Microsecond   // time.Microsecondでミリ秒設定 100ミリ秒を設定している
                                    // time.Nanosecondでナノ秒設定
                                    // time.Microsecondでマイクロ秒設定
                                    // time.Secondで秒設定
                                    // time.Minuteで分設定
                                    // time.Hourで時間設定
  time.sleep(timer)
  fmt.println("end:", time.now())   // 終了時間 time.now()で現日付を表示
}

以下のように実行すると、time.now関数は、UTC時間で表示されます。

$ odin run .\time01\
start: 2024-12-18 13:00:37.837520100 +0000 UTC
end: 2024-12-18 13:00:37.837814100 +0000 UTC

1-2.タイマー操作:tick関数

tick関数はラップ表示機能と増分表示機能があります。
tick_lap_time関数がランプタイムの表示で、tick_since関数は増分量を表示してくれます。
以下のようにtime02ディレクトリにmain.odinを作成して実行してみます。

time02/main.odin
package main

import "core:fmt"
import "core:time"

main :: proc() {
  start, end: time.Tick

  fmt.printfln("--- 1度だけのtick時間表示")
  start = time.tick_now()         // 開始時間取得
  time.sleep(100 * time.Millisecond)
  end = time.tick_now()           // 終了時間取得
  duration := time.tick_diff(start, end)
  fmt.printfln("%v", duration)    // 105.2744ms

  fmt.printfln("--- ラップ時間表示")
  start = time.tick_now()
  for i in 0 ..= 10 {
    time.sleep(200 * time.Millisecond)
    duration = time.tick_lap_time(&start)
    fmt.printfln("%v", duration)    // ラップタイムが表示される 201.2114ms 202.5384ms ...
  }

  fmt.printfln("--- 増加時間表示")
  start = time.tick_now()
  for i in 0 ..= 10 {
    time.sleep(200 * time.Millisecond)
    duration = time.tick_since(start)
    fmt.printfln("%v", duration)    // 増加分でタイムが表示 201.1117ms 402.7548ms ...
  }
}

実行すると以下のように表示されます。

$ odin run .\time02\
--- 1度だけのtick時間表示
102.4409ms
--- ラップ時間表示
214.8984ms
201.6516ms
203.5381ms
203.0259ms
204.2812ms
203.9555ms
209.7021ms
202.5051ms
202.9365ms
203.8408ms
201.1618ms
--- 増加時間表示
206.2357ms
410.8632ms
615.1059ms
818.4887ms
1.0206505s
1.2268074s
1.4289198s
1.6305082s
1.8312992s
2.0336494s
2.2380513s

1-3.タイマー操作:stopwatch関数

stopwatch関数は開始時から終了時までの時間を表示します。
tick関数のような増分やラップ時間を計測する事は出来ません。
以下のようにtime03ディレクトリにmain.odinを作成して実行してみます。

time03/main.odin
package main

import "core:fmt"
import "core:time"

main :: proc() {
  sw: time.Stopwatch

  // stopwatch操作
  time.stopwatch_reset(&sw)     // ストップウォッチのリセット
  time.stopwatch_start(&sw)     // ストップウォッチ開始
  // 200ミリ秒待機
  time.sleep(200 * time.Millisecond)
  time.stopwatch_stop(&sw)      // ストップウォッチ停止
  // ストップウォッチの結果表示
  fmt.println(sw)   // Stopwatch{running = false, _start_time = Tick{_nsec = 10413855746100}, _accumulation = 201.8388ms}
}
$ odin run .\time03\
Stopwatch{running = false, _start_time = Tick{_nsec = 45411339840400}, _accumulation = 212.2903ms}

1-4.日付操作

time.now()関数は現在の日付をUTC表記で表示します。
time.time_to_datetime関数で、年月日などの数字に変換してくれます。
以下のようにdate01ディレクトリにmain.odinを作成して実行してみます。

date01/main.odin
package main

import "core:fmt"
import "core:time"
import "core:time/datetime"

main :: proc() {
  fmt.println("現在の日付:", time.now()) // time.now()で現日付を表示するがUTC時間であるため、日本時間と9時間のズレがある

  today := time.now()

  // Time型を日付に変換
  year, month, day := time.date( today )        // 年月日をそれぞれ求める
  hour, minute, second := time.clock_from_time( today ) // 時分秒をそれぞれ求める
  fmt.printfln("%v/%v/%v %v:%v:%v", 
        year, month, day, hour, minute, second) // これだと月が文字表記になる

  // Time型を日付に変換
  datetime, _ := time.time_to_datetime( today ) // 但し、日付はUTC時間で表示される
  fmt.printfln("%v/%v/%v %v:%v:%v",
        datetime.year, datetime.month, datetime.day, datetime.hour, datetime.minute, datetime.second)
}

実行すると、time.date関数を使った時のみ、月の表示が英字で表示されます。

$ odin run .\date01\
現在の日付: 2024-12-18 13:05:55.185431600 +0000 UTC
2024/December/18 13:5:55
2024/12/18 13:5:55

1-5.日付操作

datetime.add_days_to_date関数で、現在の日付から加算日付を求める事が出来ます。
※あくまで日付だけです。
以下のようにdate02ディレクトリにmain.odinを作成して実行してみます。

date02/main.odin
package main

import "core:fmt"
import "core:time"
import "core:time/datetime"

main :: proc() {
  dt, _ := time.time_to_datetime( time.now() )

  // 日付加算
  fmt.printfln("before: %04v/%02v/%02v %02v:%02v:%02v", dt.year, dt.month, dt.day, dt.time.hour, dt.time.minute, dt.time.second)
  add_dt, _ := datetime.add_days_to_date(dt.date, -10)  // 10日前を求める
  fmt.printfln("after: %04v/%02v/%02v %02v:%02v:%02v", add_dt.year, add_dt.month, add_dt.day, dt.time.hour, dt.time.minute, dt.time.second)
}
$ odin run .\date02\
before: 2024/12/18 13:07:20
after: 2024/12/08 13:07:20

1-6.タイムゾーン関数

time.now()関数で、現在の日付をUTC時間で表示されますが、timezoneを使う事で、各国の現在時間を算出する事が出来ます。
以下のようにdate03ディレクトリにmain.odinを作成して実行してみます。

date03/main.odin
package main

import "core:fmt"
import "core:time"
import "core:time/datetime"
import "core:time/timezone"

main :: proc() {
  tz, _ := timezone.region_load("Asia/Tokyo")          // 日本のタイムゾーンを求める
  defer timezone.region_destroy(tz)

  dt_utc, _ := time.time_to_datetime( time.now() )    // 現日付を求める UTC時間
  dt, _ := timezone.datetime_to_tz(dt_utc, tz)        // 現日付をタイムゾーンから求める
  fmt.printfln("%04v/%02v/%02v %02v:%02v:%02v", dt.date.year, dt.date.month, dt.date.day, dt.time.hour, dt.time.minute, dt.time.second)
}

実行すると、UTC表示では無く、日本時間が正常に表示されます。

$ odin run .\date03\
2024/12/18 22:07:49

2.ログ制御

2-1.コンソールログ

以下にコンソールログ出力制御のプログラムを記載します。
log01ディレクトリに以下のファイルを記載して実行します。
contextエリアのloggerに、ログ出力情報(ログレベル、出力形式)を設定し、ログを出力します。
ログのレベルは、INFOレベルから出力させるようにしています。
出力形式は、出力ヘッダーと記載された物しか出力できません。
また、出力時間もUTC時間で表示されてしまいます。

log01/main.odin
package main

import "core:log"

// 出力ヘッダー
my_Console_Logger_Opts :: log.Options{
  .Level,           // INFO, WARN, ERROR などのタイトル
  .Terminal_Color,  // 出力カラー
  .Date,            // 日付
  .Time,            // 時間
  // .Line,            // 出力行
  // .Procedure,       // 出力した関数名
}

main :: proc() {
  // コンソールログの設定
  logger := log.create_console_logger(.Info, my_Console_Logger_Opts, "")
  context.logger = logger // contextエリアにコンソールロガーを設定

  log.debug("debug Program started")  // contextエリアにロガーを設定すれば、後はどこでも出力される
  log.info("info Program started")
  log.warn("warn Program started")
  log.error("error Program started")

  log.destroy_console_logger(logger)  // ロガーの削除
}

実行すると、コンソール上に、INFO、WARN、ERRORのみが表示されます。
DEBUGは表示されません。また、日付はUTC時間で表示されます。

$ odin run .\log01\
[INFO ] --- [2024-12-18 13:10:07] info Program started
[WARN ] --- [2024-12-18 13:10:07] warn Program started
[ERROR] --- [2024-12-18 13:10:07] error Program started

標準ログライブラリを変更してlocaltime時間を表示させる

ログ出力を、UTC時間からlocaltime時間に変更する方法は、二通りあり、1つは、自分で一からログを作成する方法と、2つ目は、現在のOdin標準ライブラリのログを書き換える方法です。
余り好ましくはありませんが、現在のOdin言語では、まだログ制御も完璧ではありませんので、以下のように書き換えましょう。

/odin/core/log/file_console_logger.odin
import "core:c/libc"  // c.libcをヘッダーに追加

// do_time_header関数を強制的に変更する
do_time_header :: proc(opts: Options, buf: ^strings.Builder, t: time.Time) {
  when time.IS_SUPPORTED {
    if Full_Timestamp_Opts & opts != nil {
      fmt.sbprint(buf, "[")
      // clibcからlocaltimeを抽出
      tc := libc.time(nil)
      tm := libc.localtime(&tc)
      y, m, d := tm.tm_year+1900, tm.tm_mon+1, tm.tm_mday
      h, min, s := tm.tm_hour, tm.tm_min, tm.tm_sec
      // 下記2行をコメントアウトにする
      // y, m, d := time.date(t)
      // h, min, s := time.clock(t)

上記の/odin/core/log/file_console_logger.odinを改変する事で、ログがlocaltimeで表示されます。

$ odin run .\log01\
[INFO ] --- [2024-12-18 22:12:34] info Program started
[WARN ] --- [2024-12-18 22:12:34] warn Program started
[ERROR] --- [2024-12-18 22:12:34] error Program started

2-2.ファイルログ

ファイルログは、ファイルハンドラーを取得後に、ファイルログを作成します。
また、ファイルログの出力形式も、コンソールと同じで、出力ヘッダーに記載された物しか出力できません。それと、ログ出力も、ログローテーションの機能はありません、指定したファイルにのみログファイルを作るだけです。

log02/main.odin
package main

import "core:os"
import "core:log"
import "core:fmt"

my_File_Logger_Opts :: log.Options{
  .Level,           // INFO, WARN, ERROR などのタイトル
  // .Terminal_Color,  // 出力カラー
  .Date,            // 日付
  .Time,            // 時間
  // .Line,            // 出力行
  // .Procedure,       // 出力した関数名
}
main :: proc() {
  log_file, err := os.open("log22.txt", os.O_APPEND)
  defer os.close(log_file)

  logger : log.Logger = log.create_file_logger(log_file, .Info, my_File_Logger_Opts)
  defer log.destroy_file_logger(logger)
  context.logger = logger // contextエリアにファイルロガーを設定

  log.debug("debug Program started")
  log.info("info Program started")
  log.warn("warn Program started")
  log.error("error Program started")

  fmt.println("end")
}

2-3.ミックスログ

コンソールとファイルログを一緒に扱いたい場合は、以下のように行います。
context.loggerが一つしかないため、ミックスログを生成し、その中にコンソールとファイルのロガーを設定する方法です。
この方法も、余りいい方法とは思えないですが、現時点のOdinでのログ出力は下記の通り行います。

log03/main.odin
package main

import "core:os"
import "core:log"
import "core:fmt"

my_Console_Logger_Opts :: log.Options{
  .Level,           // INFO, WARN, ERROR などのタイトル
  .Terminal_Color,  // 出力カラー
  .Date,            // 日付
  .Time,            // 時間
  // .Line,            // 出力行
  // .Procedure,       // 出力した関数名
}
my_File_Logger_Opts :: log.Options{
  .Level,           // INFO, WARN, ERROR などのタイトル
  // .Terminal_Color,  // 出力カラー
  .Date,            // 日付
  .Time,            // 時間
  // .Line,            // 出力行
  // .Procedure,       // 出力した関数名
}

main :: proc() {
  log_file, err := os.open("log05.txt", os.O_CREATE | os.O_WRONLY | os.O_APPEND)
  fmt.println("errNo:", err)
  defer os.close(log_file)

  logger_file : log.Logger = log.create_file_logger(log_file, .Info, my_Console_Logger_Opts)
  defer log.destroy_file_logger(logger_file)
  logger_console := log.create_console_logger(.Info, my_File_Logger_Opts, "")
  defer log.destroy_console_logger(logger_console)
  // 複数のロガーを結合
  logger_multi := log.create_multi_logger(logger_file, logger_console)
  defer log.destroy_multi_logger(logger_multi)

  context.logger = logger_multi // contextエリアにコンソールとファイルのロガーを設定

  log.debug("debug Program started")
  log.info("info Program started")
  log.warn("warn Program started")
  log.error("error Program started")

  fmt.println("end")
}

おわりに

ログ制御については、現時点UTC時間しか表示されないし、ファイルログはログローテーション出来ないし、ミックスログの記載が面倒だったりと、まだまだ、修正が必要かと思います。
次回は、Dear ImGuiの導入とプログラム方法について、記載していきたいと思います。

Discussion