GruffがZeroDivisionErrorで動かない

原因判明(2012年5月17日)

Ruby 1.9.3のパッチのどこかの段階でZeroDivisionErrorがでる基準が変わったみたい。

% ruby -v
ruby 1.9.3p194 (2012-04-20 revision 35410) [i686-linux]
% irb
1.9.3p194 :001 > 3.0%0.0
ZeroDivisionError: divided by 0
	from (irb):1
	from /usr/local/rvm/rubies/ruby-1.9.3-p194/bin/irb:16:in `<main>'
% ruby -v
ruby 1.9.3p0 (2011-10-30 revision 33570) [i486-linux]
% irb 
irb(main):001:0> 3.0%0.0
=> NaN

これはよく知られている話らしい。

また、0による割り算はIntergerクラスではエラーとなりますが、FloatクラスではInfinity(無限大)やNaN(Not a Number)を返します。これらの値を使った演算はInfinityかNaNにしかなりません。入力をそのまま使って演算する場合など、0で割り算を行う可能性がある場合は注意してください。
たのしいRuby 第3版, 第10章 数値(Numeric)クラス p. 177より)

このため、いままでは以下の判定が通っていたけど、例外が飛ぶようになったので動かなくなったみたい。

label = if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?

教訓としては、ゼロ割まわりの値を利用して判定を組んではいけないということか。

解決法

usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:1067のlabelというメソッドを以下のように書き換えればよい。

   def label(value)
     if !@y_axis_increment.nil?
       label = value.to_i.to_s
     elsif !@marker_count.nil? && @spread.to_f % @marker_count.to_f == 0
       label = value.to_i.to_s
     elsif @spread > 10.0
       label = sprintf("%0i", value)
     elsif @spread >= 3.0
       label = sprintf("%0.2f", value)
     else
       label = value.to_s
     end

     parts = label.split('.')
     parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{THOUSAND_SEPARATOR}")
     parts.join('.')
    end

環境

Ubuntu 12.04

% ruby -v
ruby 1.9.3p194 (2012-04-20 revision 35410) [i686-linux]

% gem list | grep rmagick
rmagick (2.13.1)

% gem list | grep gruff  
gruff (0.3.6)

% dpkg -l | grep magick
ii  imagemagick                            8:6.6.9.7-5ubuntu3.1                    image manipulation programs
ii  imagemagick-common                     8:6.6.9.7-5ubuntu3.1                    image manipulation programs -- infrastructure
rc  libmagick++3                           8:6.6.0.4-3ubuntu1.1                    object-oriented C++ interface to ImageMagick
ii  libmagick++4                           8:6.6.9.7-5ubuntu3.1                    object-oriented C++ interface to ImageMagick
ii  libmagickcore-dev                      8:6.6.9.7-5ubuntu3.1                    low-level image manipulation library - development files
rc  libmagickcore3                         8:6.6.0.4-3ubuntu1.1                    low-level image manipulation library
ii  libmagickcore4                         8:6.6.9.7-5ubuntu3.1                    low-level image manipulation library
ii  libmagickcore4-extra                   8:6.6.9.7-5ubuntu3.1                    low-level image manipulation library - extra codecs
ii  libmagickwand-dev                      8:6.6.9.7-5ubuntu3.1                    image manipulation library - development files
rc  libmagickwand3                         8:6.6.0.4-3ubuntu1.1                    image manipulation library
ii  libmagickwand4                         8:6.6.9.7-5ubuntu3.1                    image manipulation library
ii  librmagick-ruby                        2.13.1-5build1                          Transitional package for ruby-imagemagick
ii  perlmagick                             8:6.6.9.7-5ubuntu3.1                    Perl interface to the ImageMagick graphics routines
ii  ruby-rmagick                           2.13.1-5build1                          ImageMagick API for Ruby (documentation)

状況

今まで動いていたのだけど、Ruby 1.9系に切り替えたあたりから動かなくなった(?)。Gruff Graphs for RubyのCode Sampleをそのままコピーし、実行すると次のエラーがでる。

% ruby test_gruff.rb
/usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:1067:in `label': divided by 0 (ZeroDivisionError)
	from /usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:590:in `setup_graph_measurements'
	from /usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:532:in `setup_drawing'
	from /usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:508:in `draw'
	from /usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/line.rb:53:in `draw'
	from /usr/local/rvm/gems/ruby-1.9.3-p194@rails3/gems/gruff-0.3.6/lib/gruff/base.rb:487:in `write'
	from test_gruff.rb:14:in `<main>'

該当コードをみてみると draw()というメソッドから呼び出されるlabel()というメソッドがありこいつの以下の部分が問題らしい。

label = if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?

どうも@marker_countが怪しい。以下のように puts を入れて確認してみる。

puts "debug marker_count = #{@marker_count.class}"
label = if (@spread.to_f % @marker_count.to_f == 0) || !@y_axis_increment.nil?

すると

debug marker_count = NilClass

とでる。@marker_countは、initialize_ivars()で初期化されている。

def initialize_ivars
      # Internal for calculations                                               
      @raw_columns = 800.0
      @raw_rows = 800.0 * (@rows/@columns)
      @column_count = 0
      @marker_count = nil
      @maximum_value = @minimum_value = nil
      @has_data = false
      @data = Array.new
      @labels = Hash.new
      @labels_seen = Hash.new
      @sort = true
      @title = nil
〜以下略〜

このあと、draw_line_markers()で値がいれられている。label()もdraw_line_markers()で呼び出されているので、そちらの場合は問題なのだけど、がdraw_line_markers()よりも前に呼び出されているsetup_graph_measurementsの中でも呼び出されている。こちらで呼び出された場合 @marker_count は空なので、ゼロ割の例外が飛ぶ。

gruff-0.3.6は2009年から更新されていないので、これまではこれでもちゃんと動いていた。何が変わってエラーがでるようになったのだろう?とりあえず、ここまで。

追記

不思議なことにこちらの環境だと同じソースでもきっちり動く。なぜ?

Debian GNU/Linux Testing
% ruby -v
ruby 1.9.3p0 (2011-10-30 revision 33570) [i486-linux]

% gem list | grep rmagick
rmagick (2.13.1)

% gem list | grep gruff  
gruff (0.3.6)

% dpkg -l | grep magick
ii  imagemagick                          8:6.7.4.0-5                  image manipulation programs
ii  imagemagick-common                   8:6.7.4.0-5                  image manipulation programs -- infrastructure
rc  libgraphicsmagick++3                 1.3.12-1.1+b1                format-independent image processing - C++ shared library
rc  libgraphicsmagick3                   1.3.12-1.1+b1                format-independent image processing - C shared library
ii  libmagickcore-dev                    8:6.7.4.0-5                  low-level image manipulation library - development files
rc  libmagickcore4                       8:6.6.9.7-7                  low-level image manipulation library
ii  libmagickcore5:i386                  8:6.7.4.0-5                  low-level image manipulation library
ii  libmagickcore5-extra:i386            8:6.7.4.0-5                  low-level image manipulation library - extra codecs
ii  libmagickwand-dev                    8:6.7.4.0-5                  image manipulation library - development files
rc  libmagickwand4                       8:6.6.9.7-7                  image manipulation library
ii  libmagickwand5:i386                  8:6.7.4.0-5                  image manipulation library