GASのシフト表で「24時」がなぜか翌日の昼を指した話
GASのシフト表で「24時」がなぜか翌日の昼を指した話
結論から言う。
時刻の計算は、入力を受け取った直後に正規化する。
24以上の値を、そのままDateへ渡してはいけない。
しろまえ殿の現場では、この一点を怠ったせいで、シフトの開始時刻がまるごと数時間ずれる不具合が起きた。
シフト表の予定が、大きくずれていた
Google Apps Scriptで、シフトの開始時刻をカレンダーへ登録する処理を動かしていたときのことだ。
24時開始として登録したはずの予定が、翌日の昼近くまでずれ込んで表示された。
色クルーたちは、まず入力値そのものを疑った。
だが、入力は正しかった。
疑いの目は、JavaScriptのDateオブジェクトへ向かった。
setHours(24, 0, 0, 0)は、エラーにならない。
指定日の翌日0時として、自動的に繰り上がる仕様だった。
「親切な自動変換」が、二重に働いていた
原因を追ううちに、設計そのものに手が入っていたことがわかった。
このアプリでは、24時を「翌日の0時」として扱う設計だった。
そのために、日付側へあらかじめ翌日分を足す処理が入っていた。
ところが、その後の処理で、24という値そのものもDateへ渡していた。
つまり、アプリ側の繰り上げと、Dateの自動繰り上げが、同じ入力に対して二重に働いていた。
たとえば、荷物を隣の部屋へ移す係が二人いて、同じ荷物をそれぞれ一回ずつ移したようなものだ。
予定は本来の翌日0時ではなく、さらに先へずれていった。
境界値では、こうした「親切な自動変換」がそのまま落とし穴になる。
色クルーたちは、原因の正体を掴んだ瞬間、思わず声を上げた。
日数と時刻を、最初に分けてしまう
修正の方針は決まった。
入力値を、日数と当日の時刻に分けてからDateへ渡す。
function applyClockValue(calendar, rawHour) {
const wholeDays = Math.floor(rawHour / 24);
const withinDay = rawHour - wholeDays * 24;
const minutePart = Math.round((withinDay % 1) * 60);
const shiftedDay = calendar.getDate() + wholeDays;
calendar.setDate(shiftedDay);
calendar.setHours(Math.floor(withinDay), minutePart, 0, 0);
return calendar;
}
Math.floor(decimalHour / 24)で、日オフセットをまず取り出す。
残りを0〜23時の値に整えてから、Dateへ渡す。
この形なら、24は日オフセット1と時刻0に分解される。
setHours()へ、24を渡すことは二度とない。
日付の変更は、setDate()だけが担当する。
処理の責任が一つに決まったことで、二重補正は起きなくなった。
読者へ:境界値を直すときに気をつけたいこと
同じような境界値のバグに出会ったら、次の点を踏まえておくとよい。
開始時刻と終了時刻の比較にも、同じ問題が潜んでいることがある。
開始が24、終了が翌日1時を表す場合、開始値の24をそのまま比較すると判定が崩れる。
比較用の時刻は、startHour % 24で当日の値へ戻してから使う。
function isNextDay(firstClock, lastClock) {
const clockAtDayStart = firstClock - Math.floor(firstClock / 24) * 24;
return lastClock <= clockAtDayStart;
}
24時は、日中の時計では0時と同じ位置にある。
ただし、日付の情報まで消してよいわけではない。
「表示上の時刻」と「何日目か」を、別々に持つのが安全だ。
時刻だけを比較するのか、絶対的な日時を比較するのかも、関数ごとに明示しておく。
もう一つ、直しのついでに踏みやすい罠がある。
今回の修正では、全角数字を半角数字へ変換する正規表現も見直しの対象になった。
全角の文字クラスは[0-9]だ。
これをうっかり半角の[0-9]へ書き換えると、全角入力を拾えなくなる。
通常の時刻表記まで解析できなくなる、二次バグを招く。
コードを直すときは、関連して見える箇所を勢いで変更しない。
変更した行と、変更していない行を、差分で必ず確認する。
たとえば時計の針を直す作業で、文字盤まで交換する必要はない。
境界値のテストには、少なくとも次を含めておく。
- 0時と通常の勤務時間
- 23時台
- 24時開始
- 24時を超える終了時刻
- 全角数字を含む入力
- 開始と終了が同じに見える日跨ぎの入力
小さな正規化が、予定表全体のずれを止める
24時のように、同じ瞬間へ複数の表現がある値には、特に注意がいる。
入力を正規化してから、比較と加算を行う。
ライブラリの自動変換に判断を預けず、コード側で日付の進み方を一つに決める。
境界を越える処理ほど、明示的な分解が効く。
色クルーたちは、この一件を胸に、時刻以外の単位変換にも同じ目で気を配るようになった。