二つの時間の差分を取る

%H:%M:%S形式で二つの時間がかかれているファイルから、その二つの時間の差を取得する方法を考える。用途としては、ログファイルからstartとendの時刻を取り出し、その差分を計算するなど。

$ cat time.txt
17:34:21
19:21:14

dateコマンド、awkのmktime、rubyのTimeを使う方法をそれぞれ見ていく。
dateコマンドが一番簡単で、rubyも同程度に簡単だが、時刻をパースして日時オブジェクトを作る際に、awkだけ書式に柔軟性がないため冗長になった。

dateコマンドを使う場合

date --date STRING +FORMATでSTRINGで指定された日付を作成し、FORMATで指定指定された形式で出力できる。STRINGに時刻を渡す。FORMATに%sを指定することでepoc timeを出力するので、整数計算に使用する。最後に計算する前にxargs -n2をすることで、計算に使う文字をまとめてawkに渡している。

$ cat time.txt | xargs -I? date --date ? +%s | xargs -n2 | awk '{print ($2 - $1) / 60}'
106.883

次でawkのTime関係の関数を使うので、単なる四則演算だけだとしてもawkを使わずに上のコマンドを書き換えてみる。
初めに計算を簡単にするため、time.txtをcatではなくtacで反対から読む。
sedで計算式を作り、算術コマンドに使う。expr$(())の二パターンで計算してみる。

$ tac time.txt | xargs -I? date --date ? +%s | xargs -n2 | expr \( `sed 's/ / - /'` \) / 60
106
$ tac time.txt | xargs -I? date --date ? +%s | xargs -n2 | echo $(( (`sed 's/ / - /'`)  / 60))
106

awkを使う場合

awkのmktimeを使ってTimeを作り、計算する。mktimeはmktime("yyyy mm dd HH MM SS")で実行できる。mktimeで加減算を行うと、epoc timeとして整数での計算ができるし、printしてもepoc timeが出力される。
はじめは簡単にするため、"yyyy mm dd"の部分を"2017 01 01"固定にする。

$ cat time.txt | awk '{gsub(/:/, " ", $1); print mktime("2017 01 01 "$1)}' | xargs -n2 | awk '{print ($2 - $1) / 60}'
106.883

固定にした日付を現在年月日を使うように書き直してみる。現在時刻をmktimeの引数の"yyyy mm dd "部分に使うにはstrftime([format [, timestamp [, utc-flag] ] ])を使用する。

$ cat time.txt | awk '{gsub(/:/, " ", $1); print mktime(strftime("%Y %m %d ")$1)}' | xargs -n2 | awk '{print ($2 - $1) / 60}'
106.883

分を出すために60で割っているので、小数点が出てしまう場合があり、整数にしたければ、int()を呼ぶ。

$ cat time.txt | awk '{gsub(/:/, " ", $1); print mktime(strftime("%Y %m %d ")$1)}' | xargs -n2 | awk '{print int(($2 - $1) / 60)}'
106

ちなみにxargs -n2を使って計算に使う項をひとまとめに渡さない場合、2行目 - 1行目の計算をしなくてはいけない。三項演算子を使い、1行目であれば負数に、2行目であればそのまま正数にすることで計算しており、少しトリッキーになるが参考のため記載する。

$ cat time.txt | awk '{gsub(/:/, " ", $1); print mktime(strftime("%Y %m %d ")$1)}' | awk '{v += (NR==1 ? -1 : 1) * $1} END{print v / 60}'
106.883

Rubyを使う場合

TimeクラスとTimeライブラリを使う。Time.parse(date, now = Time.now)で文字列を変換してTimeクラスを作成してくれる。
readlinesで標準入力を配列で読んで、各行をパースし、引き算に帰結させている。
2行目から1行目を引かなければいけないので、配列をreverseする方法と、標準入力に渡す時にcatではなくtacであらかじめ逆転させる方法の二通りがある。

$ cat time.txt | ruby -r'time' -e'p readlines.reverse.map{|t| Time.parse(t)}.reduce(:-) / 60'
106.88333333333334
$ tac time.txt | ruby -r'time' -e'p readlines.map{|t| Time.parse(t)}.reduce(:-) / 60'
106.88333333333334

整数にしたい場合はto_iを呼ぶ。to_iを呼ぶ個所は、60で割った計算結果に対してでも問題ないが、そもそも計算結果が浮動小数点になるのは、Timeを加減算した結果が浮動小数点なので、reduce後にto_iしてもいい。

$ cat time.txt | ruby -r'time' -e'p (readlines.reverse.map{|t| Time.parse(t)}.reduce(:-) / 60).to_i'
106
$ cat time.txt | ruby -r'time' -e'p readlines.reverse.map{|t| Time.parse(t)}.reduce(:-).to_i / 60'
106