はじめに
awkでUNIX時間⇔日付の相互変換を行う関数を書いてみました。以下の記事の awk 移植版です。ずっと前から書きたいと思っていたのですが面倒で。これで行列指向データの指定フィールドの日付処理が簡単にできるようになりました。なおシェルスクリプトで日時処理をしたいのであれば Datautils を使うのが楽です。
実装
コードはシェルスクリプト版を置き換えただけです。ローカル変数が普通に使えるっていいですね。日時の形式は ISO 8601(タイムゾーンなし)形式です。
# 日時(%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
コマンドの出力結果と比較してテストをしています。
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 スクリプトの冒頭に書いて、例えばこんな感じで使うことができます。
{
dt = unixtime2datetime($1)
ut = datetime2unixtime(dt)
print dt, ut
}
$ 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 版でパフォーマンステストをしてみました。以下のコードを使ってテストしています。
i=0
while [ "$i" -lt 100000 ]; do
unixtime2datetime dt "$i"
datetime2unixtime ut "$dt"
echo "$dt $ut"
i=$((i + 1))
done
BEGIN {
for (i = 0; i < 100000; i++ ) {
dt = unixtime2datetime(i)
ut = datetime2unixtime(dt)
print dt, ut
}
}
なお time
コマンドの出力が縦に長いので、以下の形式に変更しています。
TIMEFORMAT=$'real %3lR\tuser %3lU\tsys %3lS\tcpu %P'
結果(シェルスクリプト版)
dash は優秀、bash が少し遅いですね。ローカル変数を使うように修正したら速くなりそうですが。もしローカル変数に置き換えたいという人がいたら、シェルスクリプト版よりも awk 版を参考にしたほうが良いと思います。シェルスクリプト版のコードは意味がわからないので。
$ 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 倍ぐらいの速度が出ています。
$ 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 で。念のために言っておくと、これは「コアのコード」として提供しているもので、これを利用して日時処理やタイムゾーンなどを加えた便利関数を作れますよというものです。