Ruby 3.2 で Integer#ceildiv
なるメソッドが導入された。これはどういうものか。
用途
テニスボールが 15 個ある。これを「4 個入るケース」に詰めて保管しておきたい。ケースはいくつ必要か。
15 を 4 で割ると 3 余り 2。ということは,3 ケースに 4 個ずつ詰めて,半端の 2 個を別の 1 ケースに入れることになるので,ケースは計 4 個必要になる。
この計算は,Ruby でやるなら
p 15 / 4 # => 3
# あるいは
p 15.div(4) # => 3
で,3 に 1 を足せばよいと分かるわけだが,整商(整数の商)にいつも 1 を足せばよいわけではない。
ボールが 12 個なら半端が出ないので,12 / 4
がそのまま答えになる。
つまり,整商と剰余の両方を divmod
で求めて
p 15.divmod(4) # => [3, 3] # この場合,3 に 1 を足す
p 12.divmod(4) # => [3, 0] # この場合,3 がそのまま答え
と考えなければならない。
えっ,剰余が 0
かどうかで場合分けするの? めんどくさ。
いやいや,そんなことをする必要はなくて,単に商(整商ではない普通の商)を ceil
で切り上げればよいのだ。つまり
p 15.quo(4).ceil # => 4
p 12.quo(4).ceil # => 3
でいいのである。
Integer#ceildiv
はこれを一発で行うメソッドだ:
p 15.ceildiv(4) # => 4
p 12.ceildiv(4) # => 3
こういう計算はときどき必要になるので,メソッド一つでできるのは確かにありがたい。
補足
ここまでの議論について少し補っておきたい。
quo について
さきほど,商を求めるのに Numeric#quo を用いた。これは文字通り商(quotient)を得るためのメソッドである。
被除数・除数がともに Integer の場合,quo
による商は Rational で返る:
p 6.quo(4) # => (3/2)
p 6.quo(2) # => (3/1)
読者の中には「fdiv
でいいんじゃね?」と思った人もいるだろう。確かに,
p 15.fdiv(4).ceil # => 4
p 12.fdiv(4).ceil # => 3
でテニスボールケースの必要数が分かるので,一見よさそうだが,一般にはダメなんである。
Float に落としたところで,丸め誤差が生じうるので,正しい答えが得られないことがある。実際,
p (10**23).quo(1).ceil
# => 100000000000000000000000 # 正確
p (10**23).fdiv(1).ceil
# => 99999999999999991611392 # 誤差を含んでいる
のようなことになる(環境によって結果は違いうる)。
ceil について
さきほど,
商を
ceil
で切り上げればよいのだ
と書いたが,ceil
の働きを「切り上げ」と表現するのは,一般には妥当でない。
正の数(やゼロ)については「切り上げ」だが,負数の場合,例えば
p -1.99.ceil # => -1
となる。これは「切り上げ」とは呼ばない。
ceil
は切り上げではなく,「正の無限大への丸め」なのである。
なお,ceil は ceiling(天井)の略で,[siːl] と読む。だからもし ceildiv
をカタカナ発音で口にする必要が出てきたら「シールディブ」みたいに読むことになるのだろう(知らんけど)。
なぜ Integer だけに?
div
や quo
や remainder
の類が Float や Rational でも使えるのに対し,ceildiv
は Ruby 3.2.0 では Integer
でしか使えない。
除数は Integer でなくてもいい
p 1.ceildiv(2/3r) # => 2
p 1.ceildiv(0.3) # => 4
のだが,被除数(レシーバー)は Integer でなければならない:
1.0.ceildiv(1) # => NoMethodError
なぜなのだろうか。
Integer#ceildiv
導入の 議論 を見ると,
We do not introduce
Numeric#ceildiv
until we see the need.
とあるので,Integer 以外での需要を誰も訴えなかったのだろう。
しかし,次に述べる二つの用途を考えれば,Float#ceildiv
や Rational#ceildiv
があってもよかったのではと思う。
液体の小分け
3.5 L のバーボンを 0.75 L のボトルに入れていくと,ボトルは何本必要になるか?
Float#ceildiv
があれば
p 3.5.ceildiv(0.75) # => 5
で 5 本と求められたはずだ。
まあ Float では丸め誤差が出る可能性があるので,数学的に厳密な計算をするには
p 3.5r.ceildiv(0.75r) # => 5
のように Rational オブジェクトでやったほうがよいが。
カレンダーの行数
2023 年 2 月のカレンダーは何行か? ただし日曜始まりのカレンダーとする。
まず当該月の日数を求め,そいつに,1 日の曜日に応じた数を足してやり,そいつを ceildiv(7)
してやれば行数が出る。
つまり
require "date"
d1 = Date.new(2023, 2, 1) # ついたち
d2 = Date.new(2023, 2, -1) # 末日
p (d2 - d1 + 1 + d1.wday).ceildiv(7)
でよさそうなのだが,このコードは NoMethodError を出す。
なぜかというと,Date オブジェクト同士の差が Rational オブジェクトになるからだ。
もちろん整数なので分母が 1 の Rational オブジェクトなのだが。
整数なので ceildiv
の前に to_i
をかませば済む話ではある。しかし,上のコードで NoMethodError が出るのは一瞬ギョッとするし,わけが分かっても面倒だなとは思う。