日々之迷歩

世の中わからんことだらけ

ITが複雑で難しくなっていく様に翻弄される日々です。微力ながら共著させていただいた「シェル・ワンライナー160本ノック」をよろしくお願い申し上げます。

BSD版dateコマンドの落とし穴

プログラミングにおいて日付など時間に関する処理は厄介な代物の一つ。プログラミング言語には、日付に関する関数やクラスが準備されているので、通常はそれを使って時間に関する処理をする。

今回の記事のきっかけはプログラマ兼漫画家の@chomadoさんが作っているTwitter botの日付に関する処理の問題だった。2016年2月21日は2016年の何日目で、2016年の何パーセントが経過したのか?というもの。C#とPHPとで結果が違うということで修正されていた。

先日のシェル芸勉強会福岡サテライトで、BSD版dateコマンドの使い方について質問されたのを思い出した。2016年2月21日が何日目なのか、BSD版dateコマンドを使ったシェル芸で確かめる。

BSD版のdateコマンドの使い方はUECサイトの下記ページが参考になる。

dateコマンドの便利な使い方 : BSD date

シェル芸で調査

実行環境

今回の実行環境はMac。FreeBSDでも同様だが、ワンライナーを実行するシェルはbashを利用。(FreeBSDの標準シェルはtcshなので注意)

BSD版dateコマンドの使い方

BSD版のdateコマンドでは、下記のような使い方をすると指定した日付を指定したフォーマットで出力する。

date -j -f 指定フォーマット 日付文字列 出力フォーマット

出力フォーマットを省略すると、デフォルトの形式で出力される。2016年1月31日を指定すると下記のようになる。時分秒の指定をしていないので、コマンド実行時の時刻になっている。

$ date -j -f '%Y%m%d' 20160131
2016年 1月31日 日曜日 21時20分17秒 JST

日付文字列が不正な(つまり存在しない)日付の場合は、標準エラー出力にエラーメッセージを出力する。例えば2016年1月32日を指定するとエラーになる。

$ date -j -f '%Y%m%d' 20160132
Failed conversion of ``20160132'' using format ``%Y%m%d''
date: illegal time format
usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... 
        [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]

ワンライナーを実行

このことを利用して、下記のワンライナーを実行した。結果の行数が、元旦を含めた日数になる。

$ seq 101 221 | xargs printf "2016%04d\n" | while read d; do date -j -f '%Y%m%d' $d 2> /dev/null; done | wc -l
      53

なるほど、2月21日は2016年の53日目か。念のため他の方法でも確認。Tukubaiコマンドで確認。

$ mdate -e 20160101 20160221 | tarr | gyo
52

あれ?こっちは52だ。行数が違うぞ??なんで???

ということで先ほどのワンライナーの出力をよく見てみると・・・

$ seq 101 221 | xargs printf "2016%04d\n" | while read d; do date -j -f '%Y%m%d' $d 2> /dev/null; done
....
2016年 1月30日 土曜日 21時33分05秒 JST
2016年 1月31日 日曜日 21時33分05秒 JST
2016年 1月31日 日曜日 21時33分06秒 JST
2016年 2月 1日 月曜日 21時33分06秒 JST
....

はあ??1月31日が2行出てるんだけどナニコレ????

変な挙動の確認

デバッグのためset -xし、エラー出力も捨てずに実行してみる。

$ set -x
$ seq 101 221 | xargs printf "2016%04d\n" | while read d; do date -j -f '%Y%m%d' $d; done
....
+ read d
+ date -j -f %Y%m%d 20160199
Failed conversion of ``20160199'' using format ``%Y%m%d''
date: illegal time format
usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS]] ... 
            [-f fmt date | [[[mm]dd]HH]MM[[cc]yy][.ss]] [+format]
+ read d
+ date -j -f %Y%m%d 20160200
2016年 1月31日 日曜日 21時36分29秒 JST
....

なんと20160200つまり2016年2月0日を2016年1月31日として処理しているではないか!?!?

念のため下記コマンドを実行してみると・・・やっぱりエラーになっていないぞ。

$ date -j -f '%Y%m%d' 20160200 2> /dev/null
2016年 1月31日 日曜日 21時48分31秒 JST

$ echo $?
0

いうことで、これはBSD版dateコマンドの不具合?なんだろうか??皆さん要注意。

補足

GNU版dateコマンド

今回はBSD版dateコマンドを使ったが、GNU版dateコマンドでは2016年2月0日は不正な日付として処理される。

$ gdate -d 20160200
gdate: invalid date ‘20160200’

$ echo $?
1

GNU版dateコマンドでは、オプションで-f -を使うと標準入力から日付文字列を入力出来るフィルタモードもあるが、こちらでも不正な日付として処理される。

$ echo 20160200 | gdate -f -
gdate: invalid date ‘20160200’

$ echo $?
1

年の開始日からの日数を出すフォーマット指定

とここまでは日数の計算に出力された行を数えていたが、シェル芸手練れの@ebanさんから下記のようなフォローが。

年の開始日からの日数は、出力フォーマット指定で%jを使えばいいらしい。実際に確認するとあっさりと・・・

$ date -j -f '%Y%m%d' 20160101 '+%j'
001

$ date -j -f '%Y%m%d' 20160221 '+%j'
052

Open usp Tukubaiコマンド

ユニケージ開発手法で利用するTukubaiコマンドには、日付に関する処理が便利なmdate、dayslash、calclockなどのコマンドが揃っており、そちらを使う方が便利な場合が多い。興味がある方は下記のコマンドマニュアルページを参照。

Open usp Tukubaiコマンド 一般コマンドマニュアル mdate

Open usp Tukubaiコマンド 一般コマンドマニュアル calclock

Open usp Tukubaiコマンド 一般コマンドマニュアル dayslash