lib/fractional.rb in fractional-0.1.0 vs lib/fractional.rb in fractional-1.0.1

- old
+ new

@@ -1,119 +1,220 @@ require 'rational' +require 'deprecated' -class Fractional +class Fractional < Numeric + extend DeprecatedFractionalMethods + SINGLE_FRACTION = /^\s*(\-?\d+)\/(\-?\d+)\s*$/ MIXED_FRACTION = /^\s*(\-?\d*)\s+(\d+)\/(\d+)\s*$/ - def initialize(value) - @value = value + def initialize( value, options={} ) + case value + when Rational + @value = value + when String + @value = Fractional.string_to_fraction( value, options ) + when Fixnum + @value = Rational(value) + when Numeric + @value = Fractional.float_to_fraction( value.to_f, options ) + else + raise TypeError, "Cannot instantiate Fractional from #{value.class}" + end + end - def to_s - @value + def method_missing(name, *args, &blk) + return_value = @value.send(name, *args, &blk) + return_value.is_a?(Rational) ? Fractional.new(return_value) : return_value end + def to_s( options={} ) + if options[:mixed_fraction] or options[:mixed_number] + to_join = [] + if whole_part != 0 + to_join << whole_part.to_s + end + if fractional_part != 0 + to_join << fractional_part.abs.to_s + end + to_join.join(" ") + else + @value.to_s + end + end + def to_f - Fractional.to_f(@value) + @value.to_f end - [:+, :-, :*, :/].each do |math_operator| - define_method(math_operator) do |another_fractional| - Fractional.new(Fractional.to_s(self.to_f.send(math_operator, another_fractional.to_f))) - end + def to_r + @value end - def self.to_f(value) - result = 0 + def to_i + whole_part + end - if mixed_fraction?(value) - whole, numerator, denominator = value.scan(MIXED_FRACTION).flatten + def whole_part + @value.truncate + end - result = (numerator.to_f / denominator.to_f) + whole.to_f.abs + def fractional_part + @value - whole_part + end - result = whole.to_f > 0 ? result : -result - elsif single_fraction?(value) - numerator, denominator = value.split("/") - result = numerator.to_f / denominator.to_f + def ==( other_num ) + @value == other_num + end + + def <=>(other) + case other + when Fractional, Rational + self.to_r <=> other.to_r + when Numeric + @value <=> other + when String + @value <=> Fractional.new(other).to_r else - result = value.to_f + nil end + end - result + def coerce(other) + case other + when Numeric + return Fractional.new(other), self + when String + return Fractional.new(other), self + else + raise TypeError, "#{other.class} cannot be coerced into #{Numeric}" + end end - def self.to_s(value, args={}) - whole_number = value.to_f.truncate.to_i - if whole_number == 0 # Single fraction - fractional_part_to_string(value, args[:to_nearest]) - else # Mixed fraction - decimal_point_value = get_decimal_point_value(value.to_f) - return whole_number.to_s if decimal_point_value == 0 - - fractional_part = fractional_part_to_string(decimal_point_value.abs, args[:to_nearest]) - - if (fractional_part == "1") || (fractional_part == "0") - (whole_number + fractional_part.to_i).to_s + [:+, :-, :*, :/, :**].each do |math_operator| + define_method(math_operator) do |another_fractional| + if another_fractional.is_a? Fractional or another_fractional.is_a? Rational + Fractional.new(@value.send(math_operator, another_fractional.to_r)) + elsif another_fractional.is_a? Numeric + self.send(math_operator, Fractional.new(another_fractional)) else - whole_number.to_s + " " + fractional_part + Fractional.new(self.to_r.send(math_operator, another_fractional)) end end end - def self.round_to_nearest_fraction(value, to_nearest_fraction) - if value.is_a? String - to_nearest_float = to_f(to_nearest_fraction) + def self.float_to_fraction( value, options={} ) + if value.to_f.nan? + return Rational(0,0) # Div by zero error + elsif value.to_f.infinite? + return Rational(value<0 ? -1 : 1,0) # Div by zero error + end - to_s((self.to_f(value) / to_nearest_float).round * to_nearest_float) - else - to_nearest_float = to_f(to_nearest_fraction) + if options[:to_nearest] + return self.round_to_nearest_fraction( value, options[:to_nearest] ) + end - (value / to_nearest_float).round * to_nearest_float + # first try to convert a repeating decimal unless guesstimate is forbidden + unless options[:exact] + repeat = float_to_rational_repeat(value) + return repeat unless repeat.nil? end + # finally assume a simple decimal + # The to_s helps with float rounding issues + return Rational(value.to_s) + end - private + def self.string_to_fraction( value, options={} ) + if string_is_mixed_fraction?(value) + whole, numerator, denominator = value.scan(MIXED_FRACTION).flatten + return Rational( (whole.to_i.abs * denominator.to_i + numerator.to_i) * + whole.to_i / whole.to_i.abs, denominator.to_i ) + elsif string_is_single_fraction?(value) + numerator, denominator = value.split("/") + return Rational(numerator.to_i, denominator.to_i) + else + return float_to_fraction(value.to_f, options) + end + end - def self.fraction?(value) + def self.string_is_fraction?( value ) value.is_a? String and (value.match(SINGLE_FRACTION) or value.match(MIXED_FRACTION)) end - def self.single_fraction?(value) - fraction?(value) and value.match(SINGLE_FRACTION) + def self.string_is_mixed_fraction?( value ) + string_is_fraction?(value) and value.match(MIXED_FRACTION) end - def self.mixed_fraction?(value) - fraction?(value) and value.match(MIXED_FRACTION) + def self.string_is_single_fraction?( value ) + string_is_fraction?(value) and value.match(SINGLE_FRACTION) end - def self.get_decimal_point_value(value) - value - value.truncate + def self.float_to_rational_repeat(base_value) + normalized_value = base_value.to_f + repeat = find_repeat( normalized_value ) + + if repeat.nil? or repeat.length < 1 + # try again chomping off the last number (fixes float rounding issues) + normalized_value = normalized_value.to_s[0...-1].to_f + repeat = find_repeat(normalized_value.to_s) + end + + if !repeat or repeat.length < 1 + return nil + else + return fractional_from_parts( + find_before_decimal(normalized_value), + find_after_decimal(normalized_value), + repeat) + end end - def self.fractional_part_to_string(value, round) - if round - round_to_nearest_fraction(float_to_rational(value.to_f).to_s, round) + def self.find_after_decimal( decimal ) + s_decimal = decimal.to_s + regex = /(#{find_repeat(s_decimal)})+/ + last = s_decimal.index( regex ) + first = s_decimal.index( '.' ) + 1 + s_decimal[first...last] + end + + def self.find_before_decimal( decimal ) + numeric = decimal.to_f.truncate.to_i + if numeric == 0 + decimal.to_f < 0 ? "-0" : "0" else - float_to_rational(value.to_f).to_s + numeric.to_s end end - # Whoa this method is crazy - # I nicked it from Jannis Harder at http://markmail.org/message/nqgrsmaixwbrvsno - def self.float_to_rational(value) - if value.nan? - return Rational(0,0) # Div by zero error - elsif value.infinite? - return Rational(value<0 ? -1 : 1,0) # Div by zero error + def self.find_repeat( decimal ) + return largest_repeat( decimal.to_s.reverse, 0 ).reverse + end + + def self.largest_repeat( string, i ) + if i * 2 > string.length + return "" end - s,e,f = [value].pack("G").unpack("B*").first.unpack("AA11A52") - s = (-1)**s.to_i - e = e.to_i(2) - if e.nonzero? and e<2047 - Rational(s)* Rational(2)**(e-1023)*Rational("1#{f}".to_i(2),0x10000000000000) - elsif e.zero? - Rational(s)* Rational(2)**(-1024)*Rational("0#{f}".to_i(2),0x10000000000000) + repeat_string = string[0..i] + next_best = largest_repeat( string, i + 1) + if repeat_string == string[i+1..2*i + 1] + repeat_string.length > next_best.length ? repeat_string : next_best + else + next_best end end + + def self.fractional_from_parts(before_decimal, after_decimal, repeat) + numerator = "#{before_decimal}#{after_decimal}#{repeat}".to_i - "#{before_decimal}#{after_decimal}".to_i + denominator = 10 ** (after_decimal.length + repeat.length) - 10 ** after_decimal.length + return Rational( numerator, denominator ) + end + + def self.round_to_nearest_fraction(value, to_nearest_fraction) + to_nearest_float = Fractional.new(to_nearest_fraction).to_f + Fractional.new((Fractional.new(value).to_f / to_nearest_float).round * to_nearest_float) + end + end