Class: Fractional

Inherits:
Numeric
  • Object
show all
Extended by:
DeprecatedFractionalMethods
Defined in:
lib/fractional.rb

Constant Summary collapse

SINGLE_FRACTION =
/^\s*(\-?\d+)\/(\-?\d+)\s*$/
MIXED_FRACTION =
/^\s*(\-?\d*)\s+(\d+)\/(\d+)\s*$/

Class Method Summary collapse

Instance Method Summary collapse

Methods included from DeprecatedFractionalMethods

fraction?, mixed_fraction?, single_fraction?

Constructor Details

#initialize(value, options = {}) ⇒ Fractional

Returns a new instance of Fractional.



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 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 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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, &blk) ⇒ Object



26
27
28
29
# File 'lib/fractional.rb', line 26

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

Class Method Details

.find_after_decimal(decimal) ⇒ Object



175
176
177
178
179
180
181
# File 'lib/fractional.rb', line 175

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) ⇒ Object



183
184
185
186
187
188
189
190
# File 'lib/fractional.rb', line 183

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) ⇒ Object



192
193
194
# File 'lib/fractional.rb', line 192

def self.find_repeat( decimal )
  return largest_repeat( decimal.to_s.reverse, 0 ).reverse
end

.float_to_fraction(value, options = {}) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/fractional.rb', line 107

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

  # 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) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/fractional.rb', line 155

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) ⇒ Object



209
210
211
212
213
# File 'lib/fractional.rb', line 209

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) ⇒ Object



196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/fractional.rb', line 196

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

.round_to_nearest_fraction(value, to_nearest_fraction) ⇒ Object



215
216
217
218
# File 'lib/fractional.rb', line 215

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) ⇒ Boolean

Returns:

  • (Boolean)


143
144
145
# File 'lib/fractional.rb', line 143

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) ⇒ Boolean

Returns:

  • (Boolean)


147
148
149
# File 'lib/fractional.rb', line 147

def self.string_is_mixed_fraction?( value )
  string_is_fraction?(value) and value.match(MIXED_FRACTION)
end

.string_is_single_fraction?(value) ⇒ Boolean

Returns:

  • (Boolean)


151
152
153
# File 'lib/fractional.rb', line 151

def self.string_is_single_fraction?( value )
  string_is_fraction?(value) and value.match(SINGLE_FRACTION)
end

.string_to_fraction(value, options = {}) ⇒ Object



130
131
132
133
134
135
136
137
138
139
140
141
# File 'lib/fractional.rb', line 130

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

Instance Method Details

#<=>(other) ⇒ Object



70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/fractional.rb', line 70

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) ⇒ Object



66
67
68
# File 'lib/fractional.rb', line 66

def ==( other_num )
  @value == other_num
end

#coerce(other) ⇒ Object



83
84
85
86
87
88
89
90
91
92
# File 'lib/fractional.rb', line 83

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_partObject



62
63
64
# File 'lib/fractional.rb', line 62

def fractional_part
  @value - whole_part
end

#to_fObject



46
47
48
# File 'lib/fractional.rb', line 46

def to_f
  @value.to_f
end

#to_iObject



54
55
56
# File 'lib/fractional.rb', line 54

def to_i
  whole_part
end

#to_rObject



50
51
52
# File 'lib/fractional.rb', line 50

def to_r
  @value
end

#to_s(options = {}) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/fractional.rb', line 31

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

#whole_partObject



58
59
60
# File 'lib/fractional.rb', line 58

def whole_part
  @value.truncate
end