CodeIQ「中学入試から:概数と計算」

CodeIQ の鍋谷武典さん出題の問題。
問題と解説はこちら。
CodeIQ に出した「中学入試から:概数と計算」の 解説解題

下が提出したコード。言語は Ruby

浮動小数点で計算するとまずそうなので、有理数で計算するようにした。
コメントで '下限' を次の意味で使っている('上限'も同様)。
  実数 x が a≦x の範囲で動くとき、x の'下限'は (a, true)
  a<x の範囲で動くとき、x の'下限'は (a, false)
  (つまり、普通の意味での下限と境界値が範囲に含まれるかどうかを組にしたもの)

r_to_s のところはいい加減に書いてしまって、これだと有効数字16桁あたりまでしか扱えない。
この点はフィードバックでも触れて戴いていて、Ruby2.2.0 だと「("%.20f" % q).gsub(/0*$/,'')」等と浮動小数点を経由せずに精度が指定できるらしい。
古い Ruby でも同じように書けるけど、('%.20g' % 1.001r).to_r != 1.001r - Qiita
を見ると、結局内部的には浮動小数点に一度変換するらしく、望む結果にならないということのようだ。

問題データをつけてすぐに実行できるものを http://ideone.com/ClflvH に置いておいた。
ただし、問題データを切り縮めて、標準入力から読み込むように修正してある。

#! ruby -Ku

# 問題データは data.utf8.txt から読み込む

class Round
  @@rev = {'切り捨て'=>'切り上げ', '切り上げ'=>'切り捨て', '四捨五入'=>'五捨五超入'}

  # data は設問の【あ】〜【か】に対応
  def initialize(data)
    @round1, @round2 = data[1], data[4]
    @pow1, @pow2 = [0, 3].map {|i| 10 ** (data[i].to_i - 1)}
    @mul, @x = [2, 5].map {|i| Rational(data[i])}
  end
  
  # ある数の小数第 p 位を round で指定された方法で丸めたときの'下限'(x, eql) が与えられたとき、
  # ある数の'下限'を返す
  def infimum(pow, round, x, eql)
    y = x * pow # pow == 10 ** (p - 1)
    z = y.denominator == 1 && !eql ? y + 1 : y.ceil
    z -= round == '切り上げ' ? 1 : 1.quo(2) unless round == '切り捨て'
    [z.quo(pow), ['切り捨て', '四捨五入'].member?(round)]
  end

  # ある数の小数第 p1 位を round1 で指定された方法で丸めた結果を @mul 倍してから
  # 小数第 p2 位を round2 で指定された方法で丸めると x になるとき、ある数の'下限'を返す
  def solve_lower(round1, round2, x)
    # @pow1 == 10 ** (p1 - 1), @pow2 == 10 ** (p2 - 1)
    inf, eql = infimum(@pow2, round2, x, true)
    infimum(@pow1, round1, inf / @mul, eql)
  end

  # 有理数 q を小数の文字列に変換
  def r_to_s(q)
    # to_f.to_s がちょっといい加減だけど、これでうまくいくようなので
    q.to_f.to_s.sub(/.0$/, '')
  end

  def solve
    inf, eql_i = solve_lower(@round1, @round2, @x) # ある数の'下限'を求める
    # 数直線を逆に見て'下限'を求めて、符号を逆転させれば'上限'になる
    s, eql_s = solve_lower(@@rev[@round1], @@rev[@round2], -@x)
    sup = -s
    # この時点で、ある数の'下限'、'上限'は (inf, eql_i), (sup, eql_s)
    # 今回の設問では eql_i, eql_s の片方は false なので、inf == sup のときも 'なし' を答としてよい
    return 'なし' if inf >= sup
    r_to_s(inf) + (eql_i ? '以上' : 'より大きく') + r_to_s(sup) + (eql_s ? '以下' : '未満')
  end
end

open('data.utf8.txt') do |file|
  file.each do |d|
    data = d.chomp.split(/\t/)
    id, expected = data[0], data[7]
    answer = Round.new(data[1..6]).solve
    # 計算した答と用意された答が異なるとき、問題の番号, 用意された答, 計算した答 を出力
    puts "#{id}  #{expected}  #{answer}" unless answer == expected
  end
end


(追記)
r_to_s のところを組み込みの文字列化に頼らず自前で作ってみた。

  def r_to_s(q)
    exp = 0.upto(Float::INFINITY).find {|e| 10 ** e % q.denominator == 0}
    a = (q.numerator * 10 ** exp / q.denominator).to_s
    return a if exp == 0
    a = '0' * (exp + 1 - a.size) + a if a.size <= exp
    a[0...-exp] + '.' + a[-exp..-1]
  end