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
25
26
27
28
# 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
    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
  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



30
31
32
33
# File 'lib/fractional.rb', line 30

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



187
188
189
190
191
192
193
# File 'lib/fractional.rb', line 187

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



195
196
197
198
199
200
201
202
# File 'lib/fractional.rb', line 195

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



204
205
206
# File 'lib/fractional.rb', line 204

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

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



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/fractional.rb', line 115

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



167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/fractional.rb', line 167

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



221
222
223
224
225
# File 'lib/fractional.rb', line 221

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



208
209
210
211
212
213
214
215
216
217
218
219
# File 'lib/fractional.rb', line 208

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

.numeric_to_mixed_number(amount) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
# File 'lib/fractional.rb', line 232

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

Display numbers in human-readable manner.

Examples: 0.5 -> 1/2, 2.333 -> 2 1/3, 0.666 -> 2/3 etc.


251
252
253
# File 'lib/fractional.rb', line 251

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



227
228
229
230
# File 'lib/fractional.rb', line 227

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)


155
156
157
# File 'lib/fractional.rb', line 155

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)


159
160
161
# File 'lib/fractional.rb', line 159

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)


163
164
165
# File 'lib/fractional.rb', line 163

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

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



142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/fractional.rb', line 142

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



78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/fractional.rb', line 78

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



74
75
76
# File 'lib/fractional.rb', line 74

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

#coerce(other) ⇒ Object



91
92
93
94
95
96
97
98
99
100
# File 'lib/fractional.rb', line 91

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



70
71
72
# File 'lib/fractional.rb', line 70

def fractional_part
  @value - whole_part
end

#to_fObject



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

def to_f
  @value.to_f
end

#to_iObject



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

def to_i
  whole_part
end

#to_rObject



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

def to_r
  @value
end

#to_s(options = {}) ⇒ Object



35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/fractional.rb', line 35

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_partObject



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

def whole_part
  @value.truncate
end