負の数を入れたら、Go は止まり、JavaScript は NaN を返した ― 同じ仕様、違う答え③

Go
B!

干支は「(西暦 − 4) を 10 と 12 で割った余り」で決まります。西暦3年の干支を求めるために、4言語で同じ1行を書きました。

(year - 4) % 12

西暦3年なら 3 − 4 = −1 です。結果は4通りに割れました。

言語(3 - 4) % 12干支の配列の −1 番目を読むと返した干支
Python11正しく「亥」癸亥(正解)
Go−1index out of range [-1] でパニックなし(処理が止まる)
JavaScript−1undefinedNaN
PHP−1警告(Undefined array key -1)と null空文字

余り(%)の符号の決め方が違う

割り算の余りを、負の数でどう決めるかが言語で違います。

  • Python: 余りは割る数と同じ符号になる。12 で割れば、必ず 0〜11
  • Go・JavaScript・PHP: 余りは割られる数と同じ符号になる。−1 を 12 で割った余りは −1
(3 - 4) % 12   # 11(Python)
(3 - 4) % 12   // -1(JavaScript。Go・PHP も -1)

この題材では、Python の書き方がそのまま正解でした。ほかの3言語は、負の余りを配列の番号に使ったことで、それぞれの言語らしい壊れ方をしています。

  • Go は止まります。間違った答えは返しません
  • JavaScript は止まらず、undefined + undefined を計算して NaN を返します
  • PHP は警告を出しつつ、空の答えを返します

一番困るのは、黙って続く JavaScript と PHP です。画面に「NaN」や空欄が出ても、エラーとして記録されません。

西暦1〜3年は生まれ年としては現れませんが、「◯年前の干支」や紀元前をさかのぼる計算では、同じ形で負の数が入ります。

どう直したか

余りを、数学の剰余(0 以上)にそろえる関数を通してから使いました。

const mod = (n, m) => ((n % m) + m) % m;

mod(3 - 4, 12);  // 11

直した後は、西暦1〜9999年の全9,999通りで、4言語とも正解と一致しました。

返品の「切り捨て」が、逆向きになった

負の数で困るのは、余りだけではありません。請求書の消費税の仕様では、返品のマイナスの金額で、4言語がそろって切り捨てと切り上げを逆にしました。

返品 −1,055円・10%の消費税は −105.5円です。日常の意味での切り捨ては −105円、切り上げは −106円 です(同じ額の請求をちょうど打ち消す値)。

正解4言語の答え
切り捨て−105円−106円
切り上げ−106円−105円

各言語の floor(切り捨て)と ceil(切り上げ)は、数学の定義どおりに動きます。

  • floor は負の無限大の方へ丸める:−105.5 → −106
  • ceil は正の無限大の方へ丸める:−105.5 → −105

正の数では、日常の切り捨て・切り上げと同じ結果になります。そのため、プラスの金額だけで確かめていると気づけません。

どう直したか

日常の意味は「0 の方へ切り捨てる」「0 から遠い方へ切り上げる」です。これに合う丸め方を名前で指定しました。

言語切り捨て切り上げ
PHPRoundingMode::TowardsZeroRoundingMode::AwayFromZero
Pythondecimal の ROUND_DOWNdecimal の ROUND_UP
Go・JavaScript0 の方へ切り捨てる整数の割り算その結果に、符号を見て 1 を足す

Python の ROUND_DOWN と ROUND_UP は、名前に DOWN・UP とありますが、どちらも絶対値を基準に丸めます(−105.5 → −105、−106)。

まとめ

  • 余り(%)の符号は、Python は割る数に、Go・JavaScript・PHP は割られる数にそろう。配列の番号に使う前に 0 以上にそろえる
  • 負の数で壊れたとき、Go は止まり、JavaScript は NaN、PHP は空文字を返した。黙って続く方が見つけにくい
  • floor と ceil は負の数で、日常の切り捨て・切り上げと逆向きになる。マイナスの金額をテストに入れる

この記事のケースの正解表・4言語の結果・直す前と後のコードの違いは、次のページで見られます。

次回は、大きな整数で PHP が黙って上限に張りつき、JavaScript の桁が化けたケースを取り上げます。

連載「同じ仕様、違う答え」(全6回)
  1. 第1回:「四捨五入」と書いたのに、4言語で答えが割れた
  2. 第2回:1,100 ÷ 1.1 が 1,000 にならない
  3. 第3回:負の数を入れたら、Go は止まり、JavaScript は NaN を返した(この記事)
  4. 第4回:大きな整数で、PHP は黙って上限に張りつき、JavaScript は桁が化けた
  5. 第5回:日本で動かしたときだけ、JavaScript の日付が1日ずれた
  6. 第6回:1月31日の「1か月後」はいつか
B!
← 一覧へ戻る