分数を循環小数に直すメソッドを作った(Ruby)

※注記
のちに全面的に書き直しました。



 
割り算をするとき、場合によっては割り切れず、小数部分が循環して無限に続くことがあります。これが「循環小数(recurring decimal)」です。これを扱うメソッドを作ってみました。


Rational クラスのインスタンス・メソッド Rational#to_rec_decimal です。String へ変換します。 循環小数でない場合は整数または小数を String で返します。
Rational#rec_decimal? はオブジェクトが循環小数に変換されるなら true、それ以外は false を返します。

いずれも負数をサポートします。

Rational#abs はオブジェクトの絶対値を与えます。Rational#no_minus? はオブジェクトが 0 または正ならば true を、そうでなければ false を返します。これらはおまけです。

class Rational
  def to_rec_decimal
    st = main(self.abs)
    self.no_minus? ? st : "-" + st
  end
  
  def rec_decimal?
    slf = self.abs
    @nume = slf.numerator
    @deno = slf.denominator
    return false if @deno == 1
    get_rcycle.size == 1 ? false : true
  end

  def abs
    self.no_minus? ? self : self * (-1)
  end
  
  def no_minus?
    self.numerator >= 0
  end
  
  private
  def main(slf)
    @nume = slf.numerator
    @deno = slf.denominator
    return "#{@nume}" if @deno == 1
    n = @nume / @deno
    ar = get_rcycle
    repetition_num = {}
    s = {}
    return "#{n}.#{ar[0][1..-1].join}" if ar.size == 1
    idx = repetition_num[@deno] = ar[1].size - ar[2]
    output_st(ar, idx, n)
  end
  
  def get_rcycle
    quotient = []
    remainder = []
    divided = @nume - (@nume / @deno) * @deno
    loop do
      quotient << divided / @deno
      a = divided % @deno
      return [quotient] if a.zero?
      if (b = remainder.index(a))
        return [quotient, remainder, b]
      end
      remainder << a
      divided = a * 10
    end
  end

  def output_st(ar, idx, n)
    st = ar[0].join[1..-1]
    if (ln = st.length) > idx
      idx = ln - idx
      "#{n}.#{st[0..(idx - 1)]}(#{st[idx..-1]})"
    else
      "#{n}.(#{st})"
    end
  end
end

実行例と結果。

for i in 1..20
  a = Rational(10, i)
  puts "#{10}/#{i} = " + a.to_rec_decimal
end

10/1 = 10
10/2 = 5
10/3 = 3.(3)
10/4 = 2.5
10/5 = 2
10/6 = 1.(6)
10/7 = 1.(428571)
10/8 = 1.25
10/9 = 1.(1)
10/10 = 1
10/11 = 0.(90)
10/12 = 0.8(3)
10/13 = 0.(769230)
10/14 = 0.(714285)
10/15 = 0.(6)
10/16 = 0.625
10/17 = 0.(5882352941176470)
10/18 = 0.(5)
10/19 = 0.(526315789473684210)
10/20 = 0.5


循環小数を分数に直すメソッドも作った

String#to_r をオーバーライドしました。上の書式の循環小数を、Rational オブジェクトに直します。循環小数でなければ普通に今までの String#to_r を実行します。

class String
  alias :__to_r__ :to_r
  def to_r
    s = self
    sign = 1
    if s[0] == "-"
      sign = - 1
      s = s[1..-1]
    end
    m = /(\d+)\.(\d*)\((\d+)\)$/.match(s)
    unless m
      __to_r__
    else
      a = (m[1] + "." + m[2]).__to_r__
      b = ((m[1] + m[2] + m[3]).to_i * 10 ** (- m[2].length)).to_r
      (sign * (b - a) / (10 ** m[3].length - 1)).to_r
    end
  end
  protected :__to_r__
end

実行例。

irb(main):014:0> "0".to_r
=> (0/1)
irb(main):015:0> "12".to_r
=> (12/1)
irb(main):016:0> "12.2".to_r
=> (61/5)
irb(main):017:0> "12.2(325)".to_r
=> (122203/9990)
irb(main):018:0> "0.(5882352941176470)".to_r
=> (10/17)
irb(main):019:0> "3.(3)".to_r
=> (10/3)
irb(main):020:0> "1.(428571)".to_r + "0.(714285)".to_r
=> (15/7)



サンプルです。Rational#to_rec_decimal と String#to_r が正しく動いていれば、エラーは出ない筈です。

rn = Random.new
begin
  a = (rn.rand * 1000).to_i
  b = (rn.rand * 1000).to_i + 1
  sign = rn.rand < 0.5 ? -1 : 1
  r = Rational(sign * a, b)
  print "\e[2J\e[1;1H\e[1G#{r} = #{r.to_rec_decimal}\n#{r.rec_decimal?}"
  sleep(1)
end while r.to_rec_decimal.to_r == r
puts "\nerror"



Rational#to_rec_decimal と String#to_r とを組み合わせれば、循環小数どうしの厳密な計算が可能になります。

p ("10.(952)".to_r + "5.26(3)".to_r).to_rec_decimal    #=>"16.21(628)"