LoginSignup
3

posted at

updated at

シェルスクリプトでUNIX時間⇔日付の相互変換を行う関数(POSIX準拠)

はじめに

参考としてシェルスクリプトで UNIX 時間 と日時を取得・変換する "一般的な" コマンドです。

# 現在の UNIX 時間を出力
$ date +%s
1586250991

# 指定した日時の UNIX時間 を出力
$ date +%s --date '2020-01-02 03:04:05' # Linux
$ date -j -f "%Y-%m-%d %H:%M:%S" '2020-01-02 03:04:05' +%s # macOS
1577901845

# UNIX 時間 で日時を指定して指定したフォーマットで出力
$ date --date @1577901845 +"%Y-%m-%d %H:%M:%S" # Linux
$ date -r 1577901845 +"%Y-%m-%d %H:%M:%S" # macOS
2020-01-02 03:04:05

見てわかると思いますが Linux (GNU版) と macOS (FreeBSD版) でそれぞれ書き方が異なります。date コマンド自体は POSIX で規定されていますが、よく使いそうなこれらのオプションは POSIX 準拠ではありません。また UNIX 時間 を出力する +%s は macOS でも使えますが、これも POSIX 準拠ではないため、例えば Solaris 10 の date コマンドは対応していません。

そこで date コマンドのうち POSIX 準拠のオプションのみを使って実装した UNIX 時間の出力関数と、UNIX 時間と日常の日時との相互変換を行うシェル関数を実装しました。外部コマンドは date コマンドを現在の日時を取得するのに使っているだけで、日付の変換はシェルスクリプトだけで実装しているので高速です。

実装

特に解説するようなこともないのでいきなり実装です。

2023-06-03 追記 別実装を下の方に追記しています。

# 現在の UNIX 時間 の取得
unixtime() {
  datetime2unixtime "$(date -u +'%Y-%m-%d %H:%M:%S')"
}

# 日時(%Y-%m-%d %H:%M:%S 形式)-> UNIX 時間
datetime2unixtime() {
  set -- "${1%% *}" "${1##* }"
  set -- "${1%%-*}" "${1#*-}" "${2%%:*}" "${2#*:}"
  set -- "$1" "${2%%-*}" "${2#*-}" "$3" "${4%%:*}" "${4#*:}"
  set -- "$1" "${2#0}" "${3#0}" "${4#0}" "${5#0}" "${6#0}"
  [ "$2" -lt 3 ] && set -- $(( $1-1 )) $(( $2+12 )) "$3" "$4" "$5" "$6"
  set -- $(( (365*$1)+($1/4)-($1/100)+($1/400) )) "$2" "$3" "$4" "$5" "$6"
  set -- "$1" $(( (306*($2+1)/10)-428 )) "$3" "$4" "$5" "$6"
  set -- $(( ($1+$2+$3-719163)*86400+$4*3600+$5*60+$6 ))
  echo "$1"
}

# UNIX 時間 -> 日時(%Y-%m-%d %H:%M:%S 形式)
unixtime2datetime() {
  set -- $(( $1%86400 )) $(( $1/86400+719468 )) 146097 36524 1461
  set -- "$1" "$2" $(( $2-(($2+2+3*$2/$3)/$5)+($2-$2/$3)/$4-(($2+1)/$3) ))
  set -- "$1" "$2" $(( $3/365 ))
  set -- "$@" $(( $2-( (365*$3)+($3/4)-($3/100)+($3/400) ) ))
  set -- "$@" $(( ($4-($4+20)/50)/30 ))
  set -- "$@" $(( 12*$3+$5+2 ))
  set -- "$1" $(( $6/12 )) $(( $6%12+1 )) $(( $4-(30*$5+3*($5+4)/5-2)+1 ))
  set -- "$2" "$3" "$4" $(( $1/3600 )) $(( $1%3600 ))
  set -- "$1" "$2" "$3" "$4" $(( $5/60 )) $(( $5%60 ))
  printf "%04d-%02d-%02d %02d:%02d:%02d\n" "$@"
}

# 使用例
unixtime # => 現在の UNIX 時間
date +%s # 同等のコマンド (Linux)

datetime2unixtime "2020-04-07 01:23:45" # => 1586222625
date -u +%s --date "2020-04-07 01:23:45" # 同等のコマンド (Linux)

unixtime2datetime "1586222625" # => 2020-04-07 01:23:45
date -u --date @1586222625 +"%Y-%m-%d %H:%M:%S" # 同等のコマンド (Linux)

一見してなんじゃこりゃ?と思うかもしれませんが、たくさんの set は変数を使用しないようにしているためです。シェルスクリプトは POSIX 準拠の範囲ではグローバル変数しかないので使用している変数名がぶつからないように代わりに位置パラメータを使っています。少し読みづらいと思いますが一つずつ展開していけばよく見かけるような公式に戻ります。

計算式についてはネットで検索して見つけたものを(書き方の変更程度で)使用しているだけなので解説はできません。date2unixtime は有名なフェアフィールドの公式らしいです。参考にした場所は忘れてしまいましたが検索すればいくつも出てくると思います。unixtime2dateこちら のコードを参考にしました。

タイムゾーンや ISO 8601、RFC 3339 の対応は意図的にやっていません。これらに対応するのは難しくないはずなので必要な人がやればよいというスタンスです。スクリプトに組み込んで再利用しやすいようにコアのみをシェル関数として実装しています。(コードは修正の有無に関係なく自由に使ってもらって構いません。参照元とかも明記しなくて良いです。)

テスト

こういうのは計算式を見た所で正しいかなんて判断のしようがないので date コマンドの結果と比較してテストします。時・分・秒に関しては計算は難しくないので軽く済ませて、日付についてはエポック日の 1970-01-01 から 1日(86400秒)単位で 3000 年まで一致するかをテストしています。(テストコードは Linux用です。POSIX 準拠にはしていません。)

i=0 t=5025 # 1970-01-01 01:23:45
while [ $t -lt 32503647600 ]; do # 3000-01-01 00:00:00
  a=$(unixtime2datetime "$t")
  b=$(date -u +"%Y-%m-%d %H:%M:%S" --date @$t)
  c=$(datetime2unixtime "$b")
  if [ "$a" = "$b" ] && [ "$c" = "$t" ]; then
    [ $(( i % 1000)) -eq 0 ] && date -u --date @$t
    t=$((t+86400)) i=$((i+1))
  else
    echo "error $a $b : $c $t"
    exit 1
  fi
done

実装(変数に戻す+ISO 8601版)

2023-06-03 追記

標準出力に出力するのではなく変数に戻すタイプに修正し、日付の形式を ISO 8601(タイムゾーンなし)に変更しました。変数に戻す理由はパフォーマンスのためで標準出力に出力しないのであればこちらの方が速いです。

# 現在の UNIX 時間 の取得
unixtime() {
  datetime2unixtime "$1" "$(date -u +'%Y-%m-%dT%H:%M:%S')"
}

# 日時(%Y-%m-%dT%H:%M:%S 形式)-> UNIX 時間
datetime2unixtime() {
  set -- "$1" "${2%%T*}" "${2##*T}"
  set -- "$1" "${2%%-*}" "${2#*-}" "${3%%:*}" "${3#*:}"
  set -- "$1" "$2" "${3%%-*}" "${3#*-}" "$4" "${5%%:*}" "${5#*:}"
  set -- "$1" "$2" "${3#0}" "${4#0}" "${5#0}" "${6#0}" "${7#0}"
  [ "$3" -lt 3 ] && set -- "$1" $(($2-1)) $(($3+12)) "$4" "$5" "$6" "$7"
  set -- "$1" $((365*$2+$2/4-$2/100+$2/400)) "$3" "$4" "$5" "$6" "$7"
  set -- "$1" "$2" $(((306*($3+1)/10)-428)) "$4" "$5" "$6" "$7"
  set -- "$1" $((($2+$3+$4-719163)*86400+$5*3600+$6*60+$7))
  eval "$1=\$2"
}

# UNIX 時間 -> 日時(%Y-%m-%dT%H:%M:%S 形式)
unixtime2datetime() {
  set -- "$1" $(($2%86400)) $(($2/86400+719468)) 146097 36524 1461
  set -- "$@" $(($3-(($3+2+3*$3/$4)/$6)+($3-$3/$4)/$5-(($3+1)/$4)))
  set -- "$1" "$2" "$3" $(($7/365))
  set -- "$1" "$2" "$4" $(($3-(365*$4+$4/4-$4/100+$4/400)))
  set -- "$1" "$2" "$3" "$4" $((($4-($4+20)/50)/30))
  set -- "$1" "$2" "$4" "$5" $((12*$3+$5+2))
  set -- "$@" $(($5/12)) $(($5%12+1)) $(($3-(30*$4+3*($4+4)/5-2)+1))
  set -- "$1" "$6" "$7" "$8" $(($2/3600)) $(($2%3600/60)) $(($2%3600%60))
  set -- "$1" "$2" $(($3+100)) $(($4+100)) $(($5+100)) $(($6+100)) $(($7+100))
  set -- "$1" "$2-${3#1}-${4#1}T${5#1}:${6#1}:${7#1}"
  eval "$1=\$2"
}

使用例

unixtime v && echo "$v" # => 現在の UNIX 時間
date +%s                # => 現在の UNIX 時間

datetime2unixtime v "2023-01-02T01:23:45" && echo "$v" # => 1672622625
date -u +%s --date "2023-01-02T01:23:45"               # => 1672622625

datetime2unixtime v "2023-12-31T01:23:45" && echo "$v" # => 1672622625
date -u +%s --date "2023-12-31T01:23:45"               # => 1672622625

unixtime2datetime v "1672622625" && echo "$v"   # => 2020-04-07T01:23:45
date -u --date @1672622625 +"%Y-%m-%dT%H:%M:%S" # => 2020-04-07T01:23:45

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3