23
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Ruby 3.2 で導入された Integer#ceildiv とは

Last updated at Posted at 2023-01-03

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 だけに?

divquoremainder の類が 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#ceildivRational#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 が出るのは一瞬ギョッとするし,わけが分かっても面倒だなとは思う。

23
3
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?