module UnitConversion class NumberFormatter def self.for(parsed_value, parsed_unit) case when parsed_unit && parsed_unit.metric? DecimalFormatter when parsed_value.rational? RationalFormatter else DecimalFormatter end.new end def initialize end # Converts a Numeric into a cooking-friendly Rational def rationalize(value) # NOTE: this algorithm seems stupid. if value.floor == value return value end useful_denominators = [2, 3, 4, 8, 16] if value.is_a?(Rational) && useful_denominators.include?(value.denominator) return value end whole = value.floor fraction = value - whole approximations = useful_denominators.map do |d| best_n = (1...d).each.map { |n| {delta: (fraction - Rational(n, d)).abs, numerator: n} }.sort { |a, b| a[:delta] <=> b[:delta] }.first {denominator: d, numerator: best_n[:numerator], delta: best_n[:delta]} end # Add 0 and 1 approximations << { denominator: 1, numerator: 0, delta: (fraction - Rational(0,1)).abs } approximations << { denominator: 1, numerator: 1, delta: (fraction - Rational(1,1)).abs } best = approximations.sort { |a, b| [a[:delta], a[:denominator]] <=> [b[:delta], b[:denominator]] }.first Rational((whole * best[:denominator]) + best[:numerator], best[:denominator]) end end class RationalFormatter < NumberFormatter def format(value) rational_val = rationalize(value) if rational_val.denominator == 1 rational_val.to_i.to_s elsif rational_val.denominator < rational_val.numerator.abs whole = rational_val.floor fraction = rational_val - whole "#{whole} #{fraction}" else rational_val.to_s end end end class DecimalFormatter < NumberFormatter def format(value) "%g" % ("%.3f" % value) end end end