[小ネタ] 続・シェルスクリプトで日付 ⇔ UNIXTIME 相互変換の計算がしたい(BSD date, Ruby, jq)

3年ほど前に書いたやり方のアップデートです。シェルスクリプトの中で、日付の計算をしたりタイムスタンプを出力したりという需要は現在でもそこそこある(著者調べ)ということで、今回は BSD date(macOS標準の date コマンド)と Ruby、 jq について紹介します
2021.07.13

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

以前、こんな記事を書きました。

このときはこれで解決したのですが、3年ほど経ってまた同じ需要が出てきました。
そこで「もう少し楽な方法ないかな?」と調べてみたら普通に見つかったので、備忘録がてらご紹介します。

シェルスクリプトで日付の計算がしたい (BSD date)

前回のブログで「macOS標準のdateコマンド(BSD date)ではできないので〜〜」とか書いていたんですができました
ぼくが寡聞すぎただけです。大変失礼しました。。。

ちなみに macOS 11.4 Big Sur で動作確認してますが、いつの頃からできていたのかは不明です。

可読フォーマットの日付 --> UNIXTIME

以下のようにすれば、コマンド実行時の時刻情報を UNIXTIME で出力します。

% date -j +%s

なお -j オプションは、date コマンドを「システム時計の時刻設定ではないモード」で動かすオプションとのこと。ただしsudoをつけない限り macOS の時計はいじれないので、つけなくても見た目の動作に変わりはありません。以下同様です。

コマンド実行時ではなく任意の時刻についての UNIXTIME を知りたい場合は、以下のようにします。
自動解析ではなくフォーマットを自分で指定する必要があるため、多少使い勝手が落ちますが、シェルスクリプトとして使うのであれば定型化できるパターンが多いかなとも思ってます。

% timestamp='2021/07/07 00:00:00'
% date -j -f '%Y/%m/%d %T' "${timestamp}" +%s
1625583600

UNIXTIME --> 可読フォーマットの日付

逆変換も、上と同様に -f でフォーマットを指定してやれば可能です。

% unixtime=1625583600
% LANG=C date -j -f '%s' ${unixtime}
Wed Jul  7 00:00:00 JST 2021

あるいは -r オプションで、同様のことができます。こちらのほうが手軽ですね。

% unixtime=1625583600
% LANG=C date -j -r ${unixtime}
Wed Jul  7 00:00:00 JST 2021

date コマンドなので、当然出力フォーマットを指定することもできます。

% unixtime=1625583600
% date -j -r ${unixtime} '+%Y/%m/%d %T'
2021/07/07 00:00:00

組合せ: 10/31 23:00 の 5時間後を計算

いったん UNIXTIME の形式で取りだした後、 5時間分の秒数を加えて再出力します。 前回のブログと基本的な考え方は同じですので、そちらもご参照下さい。

% (
    timestamp='2020/10/31 23:00:00'
    unixtime1=$(date -j -f '%Y/%m/%d %T' "${timestamp}" +%s)
    unixtime2=$(( $unixtime1 + 5 * 60 * 60))
    date -j -r ${unixtime2} '+%Y/%m/%d %T'
)
2020/11/01 04:00:00

参考

シェルスクリプトで日付の計算がしたい (Ruby)

前回のブログでは「PythonならmacOSデフォルトで入ってるし一般的かな」と思って選択したんですが、よく考えたら最近ではRubyもそうでした。
そしてもっと手軽に時刻操作できるという。。

可読フォーマットの日付 --> UNIXTIME

時刻オブジェクトを作って strftime() でフォーマットしてあげます。

% ruby -e 'puts Time.now.strftime("%s")'

任意の時刻の場合は Time.parse() メソッドに食わせることでパースできます。timeライブラリを require (-rオプション) してあげる必要があります。

% timestamp='2021/07/07 00:00:00'
% ruby -rtime -e 'puts Time.parse("'${timestamp}'").strftime("%s")'
1625583600

UNIXTIME --> 可読フォーマットの日付

at()メソッドに食べさせてあげてください。

% unixtime=1625583600
% ruby -e 'puts Time.at('${unixtime}')'
2021-07-07 00:00:00 +0900

% ruby -e 'puts Time.at('${unixtime}').strftime("%Y/%m/%d %T")'
2021/07/07 00:00:00

組合せ: 10/31 23:00 の 5時間後を計算

Ruby内で閉じてみました。時刻オブジェクトに直接足し算できるの楽です。

timestamp='2020/10/31 23:00:00'
ruby -rtime -e '
  d = Time.parse("'${timestamp}'");
  puts (d + 5*60*60).strftime("%Y/%m/%d %T")'
2020/11/01 04:00:00

参考

シェルスクリプトで日付の計算がしたい (jq)

今回のブログを書いた直接のモチベーションは、「jq内で時刻計算できた」ことを知ったからです。jq つよい。

可読フォーマットの日付 --> UNIXTIME

BSD date と同じく、自分でフォーマットを指定してあげる形になります。
-n オプションをつけることで、入力(ファイルや標準入力)が不要になります。

% timestamp='2021/07/07 00:00:00'
% jq -rn '"'${timestamp}'"|strptime("%Y/%m/%d %T")|strftime("%s")'
1625583600

もし時刻情報が ISO 8601 表記であれば fromdate が使えます。ただしタイムゾーンが UTC の場合しか認識しないので、そこが許容できればもっと短くかける、という感じです。

% iso8601="2021-07-07T00:00:00Z"
% jq -rn '"'${iso8601}'"|fromdate'
1625616000

計算対象の時刻情報が JSON の中にあるなら、こんな感じで取りだしてあげればいいですね。

% echo '{"time":"2021-07-07T00:00:00Z"}' | jq -r '.time|fromdate'
1625616000

UNIXTIME --> 可読フォーマットの日付

strlocaltime() を使います。 strftime() だと UTC として扱われてしまうので、JST のつもりで使うと結果が変わるので要注意です。

% unixtime=1625583600
% echo ${unixtime} | jq -r '.|strflocaltime("%Y/%m/%d %T")'
2021/07/07 00:00:00
% unixtime=1625583600
% jq -rn '"'${unixtime}'"|strflocaltime("%Y/%m/%d %T")'
2021/07/07 00:00:00

このときの限らず、 jq はタイムゾーンの扱いにクセがありますので、時刻情報を扱うときにはお気をつけ下さい。

ちなみにタイムゾーンが UTC で出力が ISO 8601 で良ければ、todate という専用の function が使えるためもっとシンプルになります。

% unixtime_utc=1625616000
% echo ${unixtime_utc} | jq -r '.|todate'
2021-07-07T00:00:00Z

組合せ: 10/31 23:00 の 5時間後を計算

これも jq の中で完結させてみます。分かりやすく pipe で改行をいれてみました。

% timestamp='2020/10/31 23:00:00'
% jq -rn '
    "'${timestamp}'"
    | strptime("%Y/%m/%d %T")
    | strftime("%s")|tonumber
    | . + 5*60*60
    | strflocaltime("%Y/%m/%d %T")'
2020/11/01 04:00:00

入力が JSON ならこんな感じになりす。
全て UTC かつ ISO 8601 で完結できると todate fromdate が使えるので、シンプルに書くことができますね!

% echo '{"time":"2020-10-31T23:00:00Z"}' \
  | jq -rc '
    .time
    | fromdate
    | . + 5*60*60
    | todate
    | {"time_modified":.}'
{"time_modified":"2020-11-01T04:00:00Z"}

参考

まとめ

GNU date と Python に続き、BSD date, Ruby そして jq を使って時間情報の操作をシェルスクリプト内で行う情報をまとめました。
誰かの、特に3年後の自分のお役に立てば幸いです。