awkでUNIX時間⇔日付の相互変換を行う関数（POSIX準拠）

## はじめに

awkでUNIX時間⇔日付の相互変換を行う関数を書いてみました。以下の記事の awk 移植版です。ずっと前から書きたいと思っていたのですが面倒で。これで行列指向データの指定フィールドの日付処理が簡単にできるようになりました。なおシェルスクリプトで日時処理をしたいのであれば Datautils を使うのが楽です。

- [シェルスクリプトでUNIX時間⇔日付の相互変換を行う関数（POSIX準拠）](https://qiita.com/ko1nksm/items/e61728f863c1b2bc829d)
- [シェルスクリプトで日付処理ならdateコマンドは投げ捨ててDateutilsを使おう！](https://qiita.com/ko1nksm/items/46f74707693213a0da3d)

## 実装

コードはシェルスクリプト版を置き換えただけです。ローカル変数が普通に使えるっていいですね。日時の形式は ISO 8601（タイムゾーンなし）形式です。

```awk
# 日時（%Y-%m-%dT%H:%M:%S 形式）-> UNIX 時間
function datetime2unixtime(dt,   yy, mm, dd, h, m, s) {
  yy = int(substr(dt, 1, 4))
  mm = int(substr(dt, 6, 2))
  dd = int(substr(dt, 9, 2))
  h = int(substr(dt, 12, 2))
  m = int(substr(dt, 15, 2))
  s = int(substr(dt, 18, 2))
  if (mm < 3) {
    yy--
    mm += 12
  }
  yy = 365 * yy + int(yy / 4) - int(yy / 100) + int(yy / 400)
  mm = int(306 * (mm + 1) / 10) - 428
  return (yy + mm + dd - 719163) * 86400 + h * 3600 + m * 60 + s
}

# UNIX 時間 -> 日時（%Y-%m-%dT%H:%M:%S 形式）
function unixtime2datetime(ut,   yy, mm, dd, h, m, s, t1, t2, t3, t4, t5) {
  t1 = int(ut / 86400) + 719468
  t2 = int((t1 + 2 + int(3 * t1 / 146097)) / 1461)
  t2 -= int((t1 - int(t1 / 146097)) / 36524)
  t2 += int((t1 + 1) / 146097)
  t2 = int((t1 - t2) / 365)

  t3 = t1 - (365 * t2 + int(t2 / 4) - int(t2 / 100) + int(t2 / 400))
  t4 = int((t3 - int((t3 + 20) / 50)) / 30)
  t5 = 12 * t2 + t4 + 2

  yy = int(t5 / 12)
  mm = t5 % 12 + 1
  dd = t3 - (30 * t4 + int(3 * (t4 + 4) / 5) - 2) + 1

  ut %= 86400
  h = int(ut / 3600)
  ut %= 3600
  m = int(ut / 60)
  s = ut % 60

  return sprintf("%4d-%02d-%02dT%02d:%02d:%02d", yy, mm, dd, h, m, s)
}
```

以下のコードで `date` コマンドの出力結果と比較してテストをしています。

```awk
BEGIN {
  i = 0
  t = 5025 # 1970-01-01 01:23:45
  while(t < 32503647600) { # 3000-01-01 00:00:00
    a = unixtime2datetime(t)
    cmd = "date -u +'%Y-%m-%dT%H:%M:%S' --date @" t
    cmd | getline b
    close(cmd)
    c = datetime2unixtime(b)
    if ((a == b) && (c == t)) {
      if ( (i % 1000) == 0 ) {
        system("date -u --date @" t)
      }
      t = t + 86400
      i++
    } else {
      printf "error %s %s : %s %s\n", a, b, c, t
      exit(1)
    }
  }
}
```

## 使い方

先程の関数を awk スクリプトの冒頭に書いて、例えばこんな感じで使うことができます。

```awk:test.awk
{
  dt = unixtime2datetime($1)
  ut = datetime2unixtime(dt)
  print dt, ut
}
```

```console
$ seq 1685800000 1685800005 | awk -f ./test.awk
2023-06-03T13:46:40 1685800000
2023-06-03T13:46:41 1685800001
2023-06-03T13:46:42 1685800002
2023-06-03T13:46:43 1685800003
2023-06-03T13:46:44 1685800004
2023-06-03T13:46:45 1685800005
```

## パフォーマンステスト

シェルスクリプト版と awk 版でパフォーマンステストをしてみました。以下のコードを使ってテストしています。

```sh:test.sh
i=0
while [ "$i" -lt 100000 ]; do
  unixtime2datetime dt "$i"
  datetime2unixtime ut "$dt"
  echo "$dt $ut"
  i=$((i + 1))
done
```

```awk:test.awk
BEGIN {
  for (i = 0; i < 100000; i++ ) {
    dt = unixtime2datetime(i)
    ut = datetime2unixtime(dt)
    print dt, ut
  }
}
```

なお `time` コマンドの出力が縦に長いので、以下の形式に変更しています。

```sh
TIMEFORMAT=$'real %3lR\tuser %3lU\tsys %3lS\tcpu %P'
```

### 結果（シェルスクリプト版）

dash は優秀、bash が少し遅いですね。ローカル変数を使うように修正したら速くなりそうですが。もしローカル変数に置き換えたいという人がいたら、シェルスクリプト版よりも awk 版を参考にしたほうが良いと思います。シェルスクリプト版のコードは意味がわからないので。

```console
$ time dash ./test.sh > /dev/null
real 0m4.319s   user 0m4.299s   sys 0m0.020s    cpu 100.00

$ time bash ./test.sh > /dev/null
real 0m22.315s  user 0m22.243s  sys 0m0.072s    cpu 99.99

$ time ksh ./test.sh > /dev/null
real 0m11.454s  user 0m11.450s  sys 0m0.004s    cpu 100.00

$ time mksh ./test.sh > /dev/null
real 0m11.687s  user 0m11.580s  sys 0m0.045s    cpu 99.46

$ time yash ./test.sh > /dev/null
real 0m17.941s  user 0m16.908s  sys 0m1.024s    cpu 99.94

$ time zsh ./test.sh > /dev/null
real 0m17.119s  user 0m14.767s  sys 0m2.352s    cpu 100.00

$ time busybox ash ./test.sh > /dev/null
real 0m7.843s   user 0m7.125s   sys 0m0.585s    cpu 98.30
```

### 結果（awk 版）

やっぱり awk は速いですね。10 倍ぐらいの速度が出ています。

```console
$ time mawk -f test.awk > /dev/null
real 0m0.311s   user 0m0.307s   sys 0m0.004s    cpu 100.04

$ time gawk -f test.awk > /dev/null
real 0m0.712s   user 0m0.704s   sys 0m0.008s    cpu 100.02

$  time original-awk -f test.awk > /dev/null
real 0m0.693s   user 0m0.693s   sys 0m0.000s    cpu 100.02

$  time busybox awk -f test.awk > /dev/null
real 0m1.294s   user 0m1.273s   sys 0m0.020s    cpu 100.01
```

ただ awk 版があればシェルスクリプト版は不要ということはなく、多くの変換を行う場合には awk の方が優れていますが、awk コマンド自体の呼び出しに時間がかかるので、変換回数が少ない場合にはシェルスクリプト版の方が優れています。

## さいごに

もしコードを利用したいって人がいたらそのまま使うなり修正するなりご自由にどうぞ。権利は主張しません。必要ならライセンスは CC0 で。念のために言っておくと、これは「コアのコード」として提供しているもので、これを利用して日時処理やタイムゾーンなどを加えた便利関数を作れますよというものです。