POSIXシェルスクリプトでの日時とタイムゾーンの取得

GNOME Terminal

この記事はPOSIX Advent Calendarの22日目だ。

シェルスクリプトを作成した際に,作成日や更新日などの日時情報をスクリプト内や--helpなどに記述することがある。このときの日時の形式はどうすべきか?という議論がある。この記事では採用すべき日時形式とタイムゾーンの取得方法について説明する。

結論としては,ISO 8601の拡張形式 (YYYY-MM-DDThh:mm±hh:mm) を採用し,以下のコマンドでタイムゾーンを取得すればよい。

tz() {
	set $(date -u '+%Y %j %H %M'); U_Y=$1 U_D=${2#0} U_D=${U_D#0} U_H=${3#0} U_M=${4#0}
	set $(date    '+%Y %j %H %M'); L_Y=$1 L_D=${2#0} L_D=${L_D#0} L_H=${3#0} L_M=${4#0}

	# Fix if year is crossed.
	U_D=$((U_Y == L_Y ? U_D : L_D + (U_Y - L_Y)))

	# Calculate delta minutes.
	dm=$(((L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M)))
	DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

	# Fix if minute is changed during running date command.
	[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
	[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

	printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
}


tz # +09:00
echo "$(date +%Y-%m-%dT%H:%M:%S)$(tz)" # 2016-12-27T21:57:32+09:00

導入

プログラムにおいて,そのバージョンがいつ作られたかという情報が大事になることがある。例えば,依存ソフトウェアのバージョンや,開発の活発さの指標として参考になることがある。

そこで,ソースコードやREADMEなどに,直接そのプログラムの作成日などや更新日時を記述することがある。しかし,日付や時刻の形式は国や地域,人によっていくつかの書き方がある。

以下に比較的よく見かける日時の表記例を示す。

日付の形式
形式
YYYY-MM-DD 2016-12-25
YYYYMMDD 20161225
YYYY/MM/DD 2016/12/25
DD/MM/YYYY 25/12/2016
MM/DD/YYYY 12/25/2016
YYMMDD 161225
YY/MM/DD 16/12/26
YYYY/MMM/DD 2016/Dec/25
MMM/DD/YYYY Dec/25/2016
DD/MMM/YYYY 25/Dec/2016
時刻の形式
形式
hh:mm:ss 20:30:40
hhmmss 203040
TT hh:mm:ss PM 08:30:40
hh:mm:ss TT 08:30:40 PM
T.T. hh:mm:ss P.M. 08:30:40
hh:mm:ss T.T. 08:30:40 P.M.
t.t. hh:mm:ss p.m. 08:30:40
hh:mm:ss t.t. 08:30:40 p.m.
tt hh:mm:ss pm 08:30:40
hh:mm:ss tt 08:30:40 pm

この他にも年,月,日を空白区切りで並べる方法などもあり,実に多くの日時の表記方法があることがわかる。特に,この中ではDD/MM/YYYYMM/DD/YYYYの表記が紛らわしい。実際にこれらはそれぞれイギリスとアメリカで使われる日付の標識であり,2016年09月07日などお互いに1-12の範囲の月日であるときにそれぞれ07/09/201609/07/2016と表記され見分けがつかない

さらに,これとは別にタイムゾーンもある。例えば,13:00という時刻をアメリカと日本とでみると,それぞれの国では時差があるので,実際にはそれぞれ別の時間をみていることになる。例えば,誰が世界で一番最初に公開したかなどのように世界中で時間を競う場合に,この時差を考慮する必要がある。時差を考慮するには,13:00 JSTや13:00+09:00などのようにタイムゾーンも明記しなければ特定できない

こうした日時の表記方法に対して,どのような形式を採用するべきだろうか?

POSIX標準日時形式

XBD 1.3 Normative ReferencesでPOSIX規格に含んでいる国際規格が掲載されている。この中に,日時形式の国際規格であるISO 8601が存在している。

実際に,POSIX規格内でも以下の場所でISO 8601が参照されている。

また,POSIX以外にもW3CのHTML 5.1のtime要素でもISO 8601の形式しか日時の形式として認められていない。さらに,ECMAScript 2016でも日時として受け付ける文字列にもISO 8601の形式のみが採用されている。

したがって,ISO 8601の日時形式に従うべきだろう。日時形式の国際標準としてはISO 8601しか存在しないので順当な判断だ。

ISO 8601の日時の形式は以下の形式となる。

ISO 8601の書式
項目 書式
ISO 8601 YYYYMMDDThhmmss±hhmm 20161226T230000+0900
ISO 8601拡張形式 YYYY-MM-DDThh:mm:ss±hh:mm 2016-12-26T23:00:00+09:00

ISO 8601の形式は日付と時刻を文字Tで区切り,タイムゾーンを末尾に付けることで日時を表記する。

1個目の形式は年月日時刻を詰めた形式となっており,可読性は悪いが,Windowsなどでファイル名として利用不可である:がないことから,ファイル名やデータ名などに適した形式である。

2個目の形式は,ISO 8601の拡張形式 (extended format) と呼ばれており,各項目の間に-:といった区切り文字を入れることで可読性に優れた形式となっている。そのため,通常の文書ではこちらの形式が適している。

常に日時+タイムゾーンを明記する必要はなく,必要に応じて後ろの部分を省略することが許されている。例えば,2016-12-26のように日付だけ表記したり,23:00といった具合に時刻の部分だけ表記してもよい。また,タイムゾーンを指定しない場合は現地時間を意味する

しかし,ここで一つ疑問が起きる。それは,現在日時を表示するPOSIX準拠コマンドであるdateコマンドの標準の出力形式がISO 8601と異なる点だ。dateコマンドの出力形式は以下となっており,ISO 8601とは異なっている。

When no formatting operand is specified, the output in the POSIX locale shall be equivalent to specifying:
date “+%a %b %e %H:%M:%S %Z %Y”

date – XCU

実際にコマンドを実行すると以下のように出力される。

date
Mon Dec 26 22:39:45 JST 2016

POSIX文書に書かれている以上,これも一つの標準とみなすことができるかもしれない。ただ,この原因は,憶測だが以下のように考えることもできる。

  1. POSIX前のUNIXの標準がそうなっていた。
  2. 対話的な用途として視認性がよいフォーマットが優先された。

1点目の理由として,元々の形式がこのようになっていたので,差し障りがないように標準の出力はそのままにしたのではないかと考えることができる。

2点目の理由だが,現在日時の把握のためにdateの4文字を入力した結果としては,視認性のよいものがよいという思想が働いた可能性がある。例えば,ISO 8601の形式だと曜日は月曜日から始まる1-7の数字で表記されることとなり,現在が何曜日かぱっとわかりにくい。その他,カレンダーを表示するcalコマンドも,標準出力は機械向けというよりは視認性を優先した出力結果となっている。

cal
   December 2016      
Su Mo Tu We Th Fr Sa  
             1  2  3  
 4  5  6  7  8  9 10  
11 12 13 14 15 16 17  
18 19 20 21 22 23 24  
25 26 27 28 29 30 31

dateコマンドの標準出力結果がISO 8601でないことについて考察した。この考察した結果としても,やはり基本はISO 8601に従うべきだろう。ISO 8601の拡張形式 (YYYY-MM-DD) を使えば,視認性を損なわずに機械可読な日時にできるからだ。

タイムゾーンの取得

方針

ここまでで,日時の形式にはISO 8601を採用すべきだと結論づけた。実際にdateコマンドでISO 8601の形式で出力するには,以下のように変換指定子を組み合わせる。

date +%Y-%m-%dT%H:%M:%S
2016-12-26T23:06:48

ただし,POSIXのdateコマンドのオプションでは現在のタイムゾーンを取得できないという問題がある。変換指定子に%Zというのがあるのだが,これはJSTというようなタイムゾーン名が表示されるだけで,残念ながら協定世界時 (UTC) からの時差 (オフセット) を数字で表示できない。POSIXで規定されるC言語のstrftime関数には%zという変換指定子があり,これでタイムゾーンが±hhmmの形式で取得できる。dateコマンドにも%zの変換指定子が存在すれば簡単だったのだが,ないならばしかたない。

タイムゾーンを省略する場合は,現地時間と解釈できる。この場合,同じ文書に国や地域名を含める必要があり,これはこれで煩雑になる。より汎用性をあげるには日時にタイムゾーンも明記したほうがよいだろう。そこで,タイムゾーンを取得する方法を検討する。

なお,GNU dateであれば,-Iオプションを使えば簡単にタイムゾーンも含めてISO 8601による現在日時を表示できる。当然ながら,GNUの独自拡張に依存すれば移植性が落ちるので使ってはいけない。

date -I'seconds'
2016-12-26T23:11:06+09:00

タイムゾーンを取得するにあたって,環境変数TZを使えば簡単にできるかと思ったが,TZ環境変数は定義されていない環境もあり,これに頼ることができない。

dateコマンドはTZ環境変数が存在すれば,このタイムゾーンに基づいて日時を表示する。dateコマンドのTZ環境変数の説明をみればわかる通り,TZ環境変数が存在しなければ,システム標準のタイムゾーンが使われることになっている。

システム標準のタイムゾーンとは,実装依存になるのだが,例えばGNU C Libraryでは/etc/localtime/usr/etc/localtimeが参照され,Ubuntuでは/etc/timezoneが参照される。

システム標準のタイムゾーンの参照元

しかし,当然ながらこれらのファイルはPOSIXでは未定義であり,これらに依存すれば移植性がなくなるので使うことはできない。

POSIX規格を調べたが,現在のタイムゾーンを数値で取得する方法はstrftime以外に存在しない。ではシェルスクリプトではどうするか?

date -uで常にUTC-0での日時が表示されることを利用して,datedate -uの差分をとり,タイムゾーンを取得する。

基本実装


具体的には,以下のような関数により現在のタイムゾーンを取得できるようになる。

#/bin/sh
## \file get_tz.sh

get_tz()( L_D=$(date +%j); U_D=$(date -u +%j) is_crossed_year="[ $L_D -eq 1 -o $U_D -eq 1 ] && [ $((L_D+U_D)) -ne 3 ]" eval $is_crossed_year && [ $L_D -eq 1 ] && U_D=0 || L_D=0 LOCAL_MIN=$(echo "$L_D*24*60 + $(date +%H)*60 + $(date +%M)" | bc) UTC_0_MIN=$(echo "$U_D*24*60 + $(date -u +%H)*60 + $(date -u +%M)" | bc) DELTA_MIN=$((LOCAL_MIN - UTC_0_MIN)) printf '%+03d:%+03d\n' $((DELTA_MIN/60)) $((DELTA_MIN%60)) | sed 's/:[+-]/:/' )

get_tz

上記のget_tz関数を実行すると,日本であれば+09:00と表示される。

get_tzの仕組みを解説する。

まず,dateコマンドの-uオプションでは,常にUTC-0での日時が表示される。一方,dateコマンドはTZ環境変数が設定されていればそのタイムゾーンに従い,TZ環境変数がなければシステム標準のタイムゾーンに従い日時を表示する。つまり,date -udateコマンドの結果の差分で現在のタイムゾーンを取得できる。

ただし,そのまま差分を取ると問題が起こる。時間は12進数であり,分は60進数である。そのまま単純に減算すれば,10進数での減算となってしまい,値が想定と異なる。例えば,現在が日本時間の01:00である場合,単純に協定世界時との差分を取ると,01:00-14:00=-13となり,期待する9と異なってしまう。

この問題を回避するため,日時の単位を分に統一させる。分に単位を統一して減算を行い,得られた差分を時間と分に戻すことでタイムゾーンを取得している。通常であれば,シェルスクリプトでの日時の演算は煩雑な作業であり,1970-01-01からの経過秒であるエポックタイム(UNIX時間)に変換して行うのが汎用的だ。しかし,この変換自体複雑である。幸い今回は2時刻の差分をとるだけで済むので,素直に日時を分に変換することで対応できた。

最後のsedの処理について説明する。イギリスより西の地域など現地時間とUTC-0との差分がマイナスのとき,時間と分に換算するときにもマイナスの符号が付く。このままだと,出力するときに-09:-30のように分の部分に符号が付くため,これをsedで除去した。

なお,当初はLOCAL_MINUTC_0_MINで日時を分に換算する際に,算術展開(Arithmetic Expansion)($(()))を使っていたのだが,これだと問題が起きたのでbcコマンドに切り替えた。算術展開だと数字が0から始まる場合に8進数とみなされてしまう。現在日時の取得に使用しているdateコマンドの%d%H%M変換指定子では,1桁の数字を常に先頭に0を付けて出力する。そのため,00-07までは10進数と同じであるので問題ないが,08-09に関しては問題が起きる。具体的には以下のようなエラーが出る。

sh
echo "$((08*2))"
sh: 1: arithmetic expression: expecting EOF: "08*2"

bcコマンドであれば数字を常に10進数として扱うのでこの問題を回避できる。同様の問題に対応するのに,exprコマンドも利用できるのだが,こちらは演算子の前後を空白で区切る必要があり,記述量が長くなってしまうので今回は避けた。

年またぎの処理

2016-12-29追記

当初掲載していたコードでは,月や年をまたぐ場合に日数が不連続になり (例:12/31と1/1),タイムゾーンを算出できていなかった。ユリウス日 (通算日) をdate +jで取得すれば,月またぎは問題ないが,やはり年をまたぐときにタイムゾーンを算出できない。そこで,年をまたぐかどうかの判定を入れた。判定は以下の手順で行った。

年またぎの判定
  1. 確実に年をまたぐことを判定。
    1. 現地時間とUTC-0のどちらかが1/1で,もう片方が最終日であるかを判定。
    2. うるう年では最終通算日366がありえるので,残りの日にちが1/1か1/2でないという条件,つまり両方の日数の合計が3以上であるで判定。
  2. 差分を取れるように,日を0か366か367に更新。

これで,年をまたぐ場合であってもタイムゾーンを算出できるようになった。以下のコマンドで現在の端末でだけ日付を1/1に変更してget_tzを実行すれば,きちんと現在のタイムゾーンを取得できることを確認できる。

sudo date 01010000
./get_tz.sh # +09:00
コマンド実行中の分またぎ

2017-01-07追記

関数内で1回目のdateを実行してから2回目のdateコマンドを実行するまでの間に,万が一でも分をまたいでしまうと1分ずれてしまい,タイムゾーンが08:59や09:01となってしまう。幸いなことに,タイムゾーンの分は00,30,45しか存在しないので,差分をとった後の分の1桁目が1,6,4,9なら±1することで誤差を調整する。現在のタイムゾーンがUTC+00:00より西か東かで符号が変わるのでその処理を入れている。

また,当初は日時の先頭に0が登場するために,bcコマンドを使って計算していた。今回の対応で,setコマンドで日時分を個別の変数に代入するように処理を変えたので,ついでに先頭の0を削除するようにして,算術展開$(())で計算できるようにした。

get_tz()(
	set $(date -u '+%j %H %M'); U_D=${1#0}; U_D=${U_D#0}; U_H=${2#0}; U_M=${3#0}
	set $(date    '+%j %H %M'); L_D=${1#0}; L_D=${L_D#0}; L_H=${2#0}; L_M=${3#0}

	# Fix if year is crossed
	IS_CROSSED_YEAR="[ $L_D = 1 -o $U_D = 1 ] && [ $((L_D+U_D)) -gt 3 ]"
	eval "$IS_CROSSED_YEAR" && U_D=$((L_D == 1 ? L_D-1 : L_D+1))

	dm=$(( (L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M) ))
	DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

	# Fix if minute is changed during running date command
	[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
	[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

	printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
)

setコマンドの後に以下の日時を強制的にセットしてちゃんと算出できていることを確認できる。

	# +5:45 Nepal
	# U_D=365; U_H=23; U_M=59
	# L_D=1;   L_H=5;  L_M=45

	# U_D=1;   U_H=5;  U_M=45
	# L_D=365; L_H=23; L_M=59

	# -3:30 Canada
	# U_D=1;   U_H=3;  U_M=28
	# L_D=365; L_H=23; L_M=59

	# L_D=1;   L_H=3;  L_M=28
	# U_D=365; U_H=23; U_M=59

これでコマンド実行中に分をまたいでも大丈夫だ。

年またぎの処理の改良

2020-03-29 Sun追記

年またぎの処理について改良案をコメントでいただいた。2016-12-29の実装では,通算日を使って年またぎを判定していた。よくよく考えれば,dateコマンド実行時に%Yで年を取得できるので,この年の比較 (U_Y, L_Y) で簡単に年またぎを判定できる。

加えて,年またぎ時の日付の処理では,$U_Y-$L_Y == -1の場合,地域標準時が未来で,$U_Y-$L_Y == 1の場合,協定世界時が未来となる。従って,以下のようにローカル日 (L_D) にU_Y-L_Yを足したものを世界日 (U_D) にしてやれば,日付の連続性を維持できる。

U_D=$((U_Y == L_Y ? U_D : L_D + (U_Y - L_Y)))

最終的なコードは以下の通りとなる。

tz() {
	set $(date -u '+%Y %j %H %M'); U_Y=$1 U_D=${2#0} U_D=${U_D#0} U_H=${3#0} U_M=${4#0}
	set $(date    '+%Y %j %H %M'); L_Y=$1 L_D=${2#0} L_D=${L_D#0} L_H=${3#0} L_M=${4#0}

	# Fix if year is crossed.
	U_D=$((U_Y == L_Y ? U_D : L_D + (U_Y - L_Y)))

	# Calculate delta minutes.
	dm=$(((L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M)))
	DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

	# Fix if minute is changed during running date command.
	[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
	[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

	printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
}

この修正で処理が短くわかりやすくなった。その他,コマンド名やコメント,余分な区切り (;) を削除するなど若干調整した。

結論

POSIXにおける日時の表示形式とタイムゾーンの取得方法について説明した。

現在日時は一時ファイルを作る時などで重宝するので,以下のように関数にして~/.bashrcなどに書いておけば,現在日時を即座にISO 8601形式で出力できるので便利だ。

tz() {
	set $(date -u '+%Y %j %H %M'); U_Y=$1 U_D=${2#0} U_D=${U_D#0} U_H=${3#0} U_M=${4#0}
	set $(date    '+%Y %j %H %M'); L_Y=$1 L_D=${2#0} L_D=${L_D#0} L_H=${3#0} L_M=${4#0}

	# Fix if year is crossed.
	U_D=$((U_Y == L_Y ? U_D : L_D + (U_Y - L_Y)))

	# Calculate delta minutes.
	dm=$(((L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M)))
	DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

	# Fix if minute is changed during running date command.
	[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
	[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

	printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
}


now() { dt=$(date +%Y%m%dT%H%M%S) OPTSTR=':hlst-:' for opt in $(echo $OPTSTR | sed 's/[:-]//g' | fold -w 1); do eval opt_$opt='false' done while getopts $OPTSTR opt; do case "$opt${OPTARG:-}" in l|-long) opt_l='true' dt=$(date +%Y-%m-%dT%H:%M:%S);; s|-short) dt=$(date +%Y%m%dT%H%M%S);; t|-time-zone) opt_t='true';; h|-help) cat <<-EOT Usage: now [OPTION]... Print current date time with ISO 8601 format (YYYYmmddThhmmss). -l, --long print date by ISO 8601 extended format (YYYY-mm-ddThh:mm:ss). -s, --short print date by ISO 8601 format (YYYYmmddThhmmss) [default]. -t, --time-zone append time zone (-t=[+-]hh:mm, -s=[+-]hhmm). -h, --help display this help and exit. EOT exit ;; \?*) echo "$EXE_NAME: invalid option -- '$OPTARG'" >&2; exit 1;; *) echo "$EXE_NAME: unrecognized option '-$opt$OPTARG'" >&2; exit 1;; esac done if $opt_t; then EXE_DIR=$(dirname "$0") [ -r "$EXE_DIR/tz.sh" ] && . "$EXE_DIR/tz.sh" command -v tz.sh >/dev/null && . tz.sh if ! command -v tz >/dev/null; then echo "tz command was not found. Time zone option (-t) is not enabled!" >&2 exit 1 fi TIME_ZONE=$(tz) ! $opt_l && TIME_ZONE=$(echo "$TIME_ZONE" | sed 's/://') dt="$dt$TIME_ZONE" fi echo "$dt" }

上記のnow関数は以下のように使う。

now     # 20161227T232101          Same as now -s or now --short
now -t # 20161227T232119+0900 With time zone, same as now -st or now --time-zone
now -l # 2016-12-27T23:22:07 ISO 8601 extended format. same as now --long now -tl # 2016-12-27T23:22:36+09:00 Same as now --long --time-zone

上記のように,関数化までする必要がないと感じるならalias程度にしておくのもよいだろう。こちらだと,タイムゾーンや表示形式の自由度はないが,1行で書けるのですっきりしている。

alias now='date +%Y%m%dT%H%M%S'

このnow関数は,例えば,以下のようにコマンド置換 (Command Substitution) $()で現在日時をファイル名として使ったりできる。

ls > $(now).log

今回紹介したtznowGitHub上にも格納している。

POSIX規格だけでなく,ISOなどの国際規格に準拠することでも移植性を担保できる。日頃からこうしたデジュレ標準を意識して,高品質で高寿命なコードや文書の作成を心がけよう。

POSIXシェルスクリプトでの日時とタイムゾーンの取得” に対して6件のコメントがあります。

  1. cmplstofB より:

    こんにちは。

    年跨ぎの判定についてですが,%Y (year with century as a decimal number) を用いるのもいいかなと思うのですが,もしかして%Yには何か問題があって採用なさっていないのでしょうか。

    個人的には,
    date -u +’%Y’とdate +’%Y’の値について,仮に$U_Y,$L_Yという名前の変数に格納するとして,$(($U_Y – $L_Y))の値をそのまま判定に利用するのがいいかなと思いますが,なにぶんド素人なので重篤な欠点があるやもしれません……。

    1. senooken より:

      コメントありがとうございます!おっしゃるとおりです。冒頭の`set`で`%Y`を `U_Y`, `L_Y` に年を保存して,これを比較すれば処理が簡単になりますね。

      この記事を書いたときは,アドベントカレンダーを集中的に書いていて,後から不都合がいくつか見つかったため,そこまで気がつかなかったようです。

      あるいはsetの行が80列を超えることを嫌がってU_D, L_Dを使ったものにした可能性があります。が,%Yを使ったほうが処理も理解も簡単になるので,こちらに差し替えて記事を更新します。

      1. senooken より:

        以下のコードに差し替えることにします。何か問題あればコメントいただけると幸いです。

        get_tz()(
        	set $(date -u '+%Y %j %H %M'); U_Y=$1 U_D=${2#0} U_D=${U_D#0} U_H=${3#0} U_M=${4#0}
        	set $(date    '+%Y %j %H %M'); L_Y=$1 L_D=${2#0} L_D=${L_D#0} L_H=${3#0} L_M=${4#0}
        
        	# Fix if year is crossed
        	[ $U_Y != $L_Y ] && U_D=$((L_D == 1 ? L_D-1 : L_D+1))
        
        	dm=$(( (L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M) ))
        	DIGIT_1=$((dm<0 ? -dm%10 : dm%10))
        
        	# Fix if minute is changed during running date command
        	[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
        	[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))
        
        	printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
        )
        
        1. cmplstofB より:

          返信ありがとうございます!

          いつも(特にPOSIXシェルスクリプト関連に関して)参考にさせていただいているので,少しでもお役に立てたなら嬉しいです。

          $U_Yと$L_Yの比較ですが,
          どんな場合でも両者の差は1年を越すことはなく,
          $U_Y – $L_Y = -1の場合は地域標準時の方が進んでいる,
          つまり$L_D = 1であり,
          $U_Y – $L_Y = -1の場合は協定世界時の方が進んでいる,
          つまり$U_D = 1(従って$L_D != 1)であるということですよね。
          (間違ってるかもしれないです…)

          もし↑上述の理論が正当ならば
          $U_D変数に施す年跨ぎ処理は,
          “`
          U_D=$(( U_Y == L_Y ? U_D : L_D + (U_Y – L_Y) ))
          “`
          このようにすると(微々たるものですが)短かく済むのではないでしょうか。

          1. senooken より:

            なるほど。その論理で問題なさそうです。

            testコマンド ([) はbashやzshなど多くの場合,組み込みコマンドになっていますが,POSIX上は組み込みではないので,できれば避けたかったのですが,ぱっと思いつきませんでした。

            細かいところなので,なかなかやっかいです。算術展開 (Arithmetic Expansion) はサブシェルじゃないので,パフォーマンス上も問題ありません。

            こちらで後ほど訂正することにします。ご指摘ありがとうございました。

            今はいろいろあってPOSIXの探求をやめましたが,参考になったようで幸いです。また間違いがあるかもしれませんので,気付いたらご指摘お願いします。

            1. senooken より:

              細かいところが気になって対応が遅れてしまいました。

              記事を更新しました。
              「[年またぎの処理の改良](https://senooken.jp/post/2016/12/27/#20200329T1318_%E5%B9%B4%E3%81%BE%E3%81%9F%E3%81%8E%E3%81%AE%E5%87%A6%E7%90%86%E3%81%AE%E6%94%B9%E8%89%AF)」を追加して,ここでのコメントのやり取りを整理して記述しました。

              ありがとうございました。

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です