class Fractional

Constants

MIXED_FRACTION
SINGLE_FRACTION

Public Class Methods

find_after_decimal( decimal ) click to toggle source
# File lib/fractional.rb, line 185
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
find_before_decimal( decimal ) click to toggle source
# File lib/fractional.rb, line 193
def self.find_before_decimal( decimal )
  numeric = decimal.to_f.truncate.to_i
  if numeric == 0
    decimal.to_f < 0 ? "-0" : "0"
  else
    numeric.to_s
  end
end
find_repeat( decimal ) click to toggle source
# File lib/fractional.rb, line 202
def self.find_repeat( decimal )
  return largest_repeat( decimal.to_s.reverse, 0 ).reverse
end
float_to_fraction( value, options={} ) click to toggle source
# File lib/fractional.rb, line 113
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

  if options[:to_nearest]
    return self.round_to_nearest_fraction( value, options[:to_nearest] )
  end

  if options[:to_human]
    return self.round_to_human_fraction( value )
  end

  # 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
float_to_rational_repeat(base_value) click to toggle source
# File lib/fractional.rb, line 165
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
fractional_from_parts(before_decimal, after_decimal, repeat) click to toggle source
# File lib/fractional.rb, line 219
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
largest_repeat( string, i ) click to toggle source
# File lib/fractional.rb, line 206
def self.largest_repeat( string, i )
  if i * 2 > string.length
    return ""
  end
  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
new( value, options={} ) click to toggle source
# File lib/fractional.rb, line 10
def initialize( value, options={} )
  case value
  when Rational
    @value = value
  when String
    @value = Fractional.string_to_fraction( value, options )
  when Numeric
    if @value == @value.to_i
      @value = Rational(value)
    else # It's still Rational if it's a natural number
      @value = Fractional.float_to_fraction( value.to_f, options )
    end
  else
    raise TypeError, "Cannot instantiate Fractional from #{value.class}"
  end

end
numeric_to_mixed_number(amount) click to toggle source
# File lib/fractional.rb, line 230
def self.numeric_to_mixed_number(amount)
  sign_prefix = ( amount < 0 )? '-' : ''
  amount = amount.abs
  amount_as_integer = amount.to_i
  if (amount_as_integer != amount.to_f) && (amount_as_integer > 0)
    fraction = amount - amount_as_integer
    "#{sign_prefix}#{amount_as_integer} #{fraction}"
  else
    if amount.denominator == 1
      "#{sign_prefix}#{amount_as_integer}"
    else
      sign_prefix + amount.to_s
    end
  end
end
round_to_human_fraction(value) click to toggle source

Display numbers in human-readable manner.

Examples: 0.5 -> 1/2, 2.333 -> 2 1/3, 0.666 -> 2/3 etc.
# File lib/fractional.rb, line 249
def self.round_to_human_fraction(value)
  numeric_to_mixed_number value.rationalize(Rational('0.01'))
end
round_to_nearest_fraction(value, to_nearest_fraction) click to toggle source
# File lib/fractional.rb, line 225
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
string_is_fraction?( value ) click to toggle source
# File lib/fractional.rb, line 153
def self.string_is_fraction?( value )
  value.is_a? String and (value.match(SINGLE_FRACTION) or value.match(MIXED_FRACTION))
end
string_is_mixed_fraction?( value ) click to toggle source
# File lib/fractional.rb, line 157
def self.string_is_mixed_fraction?( value )
  string_is_fraction?(value) and value.match(MIXED_FRACTION)
end
string_is_single_fraction?( value ) click to toggle source
# File lib/fractional.rb, line 161
def self.string_is_single_fraction?( value )
  string_is_fraction?(value) and value.match(SINGLE_FRACTION)
end
string_to_fraction( value, options={} ) click to toggle source
# File lib/fractional.rb, line 140
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

Public Instance Methods

<=>(other) click to toggle source
# File lib/fractional.rb, line 76
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
    nil
  end
end
==( other_num ) click to toggle source
# File lib/fractional.rb, line 72
def ==( other_num )
  @value == other_num
end
coerce(other) click to toggle source
# File lib/fractional.rb, line 89
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
fractional_part() click to toggle source
# File lib/fractional.rb, line 68
def fractional_part
  @value - whole_part
end
method_missing(name, *args, &blk) click to toggle source
# File lib/fractional.rb, line 28
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
to_f() click to toggle source
# File lib/fractional.rb, line 52
def to_f
  @value.to_f
end
to_i() click to toggle source
# File lib/fractional.rb, line 60
def to_i
  whole_part
end
to_r() click to toggle source
# File lib/fractional.rb, line 56
def to_r
  @value
end
to_s( options={} ) click to toggle source
# File lib/fractional.rb, line 33
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
      if whole_part != 0
        to_join << fractional_part.abs.to_s
      else
        to_join << fractional_part.to_s
      end
    end
    to_join.join(" ")
  else
    @value.to_s
  end
end
whole_part() click to toggle source
# File lib/fractional.rb, line 64
def whole_part
  @value.truncate
end