Skip to content

Commit a8627b3

Browse files
committed
Parse fraction characters
1 parent 0112fe6 commit a8627b3

File tree

18 files changed

+703
-634
lines changed

18 files changed

+703
-634
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
language: ruby
2+
sudo: false
23

34
rvm:
45
- 1.9.3

lib/ruby-measurement/core_ext/string.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ class String
44
def to_measurement
55
Measurement.parse(self)
66
end
7-
7+
88
def to_unit
99
Measurement::Unit[self] or raise ArgumentError, "Invalid unit: #{self}"
1010
end

lib/ruby-measurement/measurement.rb

Lines changed: 95 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
# encoding: UTF-8
2+
13
require 'ruby-measurement/unit'
24
require 'ruby-measurement/version'
35

@@ -7,28 +9,46 @@ class Measurement
79
SCIENTIFIC_REGEX = /\A#{SCIENTIFIC_NUMBER}\s*#{UNIT_REGEX}?\z/.freeze
810
RATIONAL_REGEX = /\A([+-]?\d+\s+)?((\d+)\/(\d+))?\s*#{UNIT_REGEX}?\z/.freeze
911
COMPLEX_REGEX = /\A#{SCIENTIFIC_NUMBER}?#{SCIENTIFIC_NUMBER}i\s*#{UNIT_REGEX}?\z/.freeze
10-
12+
13+
RATIOS = {
14+
'¼' => '1/4',
15+
'½' => '1/2',
16+
'¾' => '3/4',
17+
'⅓' => '1/3',
18+
'⅔' => '2/3',
19+
'⅕' => '1/5',
20+
'⅖' => '2/5',
21+
'⅗' => '3/5',
22+
'⅘' => '4/5',
23+
'⅙' => '1/6',
24+
'⅚' => '5/6',
25+
'⅛' => '1/8',
26+
'⅜' => '3/8',
27+
'⅝' => '5/8',
28+
'⅞' => '7/8',
29+
}.freeze
30+
1131
attr_reader :quantity, :unit
12-
32+
1333
def initialize(quantity, unit_name = :count)
1434
unit = unit_name
1535
unit = Unit[unit_name.to_s] if unit_name.kind_of?(Symbol) || unit_name.kind_of?(String)
16-
36+
1737
raise ArgumentError, "Invalid quantity: #{quantity}" unless quantity.kind_of?(Numeric)
1838
raise ArgumentError, "Invalid unit: #{unit_name}" unless unit.kind_of?(Unit)
19-
39+
2040
@quantity = quantity
2141
@unit = unit
2242
end
23-
43+
2444
def inspect
2545
to_s
2646
end
27-
47+
2848
def to_s
2949
"#{quantity} #{unit}"
3050
end
31-
51+
3252
%w(+ - * /).each do |operator|
3353
class_eval <<-END, __FILE__, __LINE__ + 1
3454
def #{operator}(obj)
@@ -47,7 +67,7 @@ def #{operator}(obj)
4767
end
4868
END
4969
end
50-
70+
5171
def **(obj)
5272
case obj
5373
when Numeric
@@ -56,82 +76,96 @@ def **(obj)
5676
raise ArgumentError, "Invalid arithmetic: #{self} ** #{obj}"
5777
end
5878
end
59-
79+
6080
def ==(obj)
6181
obj.kind_of?(self.class) && quantity == obj.quantity && unit == obj.unit
6282
end
63-
83+
6484
def convert_to(unit_name)
6585
unit = Unit[unit_name]
6686
raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
67-
87+
6888
return dup if unit == @unit
69-
89+
7090
conversion = @unit.conversion(unit.name)
7191
raise ArgumentError, "Invalid conversion: '#@unit' to '#{unit.name}'" unless conversion
72-
92+
7393
self.class.new(conversion.call(@quantity), unit.name)
7494
end
75-
95+
7696
def convert_to!(unit_name)
7797
measurement = convert_to(unit_name)
7898
@unit, @quantity = measurement.unit, measurement.quantity
7999
self
80100
end
81-
82-
def self.parse(str = '0')
83-
str = str.strip
84-
85-
case str
86-
when COMPLEX_REGEX then unit_name, quantity = parse_complex(str)
87-
when SCIENTIFIC_REGEX then unit_name, quantity = parse_scientific(str)
88-
when RATIONAL_REGEX then unit_name, quantity = parse_rational(str)
89-
else raise ArgumentError, "Unable to parse: '#{str}'"
101+
102+
class << self
103+
def parse(str = '0')
104+
str = normalize(str)
105+
106+
case str
107+
when COMPLEX_REGEX then unit_name, quantity = parse_complex(str)
108+
when SCIENTIFIC_REGEX then unit_name, quantity = parse_scientific(str)
109+
when RATIONAL_REGEX then unit_name, quantity = parse_rational(str)
110+
else raise ArgumentError, "Unable to parse: '#{str}'"
111+
end
112+
113+
unit_name ||= 'count'
114+
unit = Unit[unit_name.strip.downcase]
115+
raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
116+
117+
new(quantity, unit)
90118
end
91-
92-
unit_name ||= 'count'
93-
unit = Unit[unit_name.strip.downcase]
94-
raise ArgumentError, "Invalid unit: '#{unit_name}'" unless unit
95-
96-
new(quantity, unit)
97-
end
98-
99-
def self.define(unit_name, &block)
100-
Unit.define(unit_name, &block)
101-
end
102-
103-
private
104-
105-
def self.parse_complex(str)
106-
real, imaginary, unit_name = str.scan(COMPLEX_REGEX).first
107-
quantity = Complex(real.to_f, imaginary.to_f).to_f
108-
return unit_name, quantity
109-
end
110-
111-
def self.parse_scientific(str)
112-
whole, unit_name = str.scan(SCIENTIFIC_REGEX).first
113-
quantity = whole.to_f
114-
return unit_name, quantity
115-
end
116-
117-
def self.parse_rational(str)
118-
whole, _, numerator, denominator, unit_name = str.scan(RATIONAL_REGEX).first
119-
120-
if numerator && denominator
121-
numerator = numerator.to_f + (denominator.to_f * whole.to_f)
122-
denominator = denominator.to_f
123-
quantity = Rational(numerator, denominator).to_f
124-
else
119+
120+
def define(unit_name, &block)
121+
Unit.define(unit_name, &block)
122+
end
123+
124+
private
125+
126+
def normalize(str)
127+
str.dup.tap do |str|
128+
if str =~ Regexp.new(/(#{RATIOS.keys.join('|')})/)
129+
RATIOS.each do |search, replace|
130+
str.gsub!(search) { " #{replace}" }
131+
end
132+
end
133+
134+
str.strip!
135+
end
136+
end
137+
138+
def parse_complex(str)
139+
real, imaginary, unit_name = str.scan(COMPLEX_REGEX).first
140+
quantity = Complex(real.to_f, imaginary.to_f).to_f
141+
return unit_name, quantity
142+
end
143+
144+
def parse_scientific(str)
145+
whole, unit_name = str.scan(SCIENTIFIC_REGEX).first
125146
quantity = whole.to_f
147+
return unit_name, quantity
148+
end
149+
150+
def parse_rational(str)
151+
whole, _, numerator, denominator, unit_name = str.scan(RATIONAL_REGEX).first
152+
153+
if numerator && denominator
154+
numerator = numerator.to_f + (denominator.to_f * whole.to_f)
155+
denominator = denominator.to_f
156+
quantity = Rational(numerator, denominator).to_f
157+
else
158+
quantity = whole.to_f
159+
end
160+
161+
return unit_name, quantity
126162
end
127-
128-
return unit_name, quantity
129163
end
130-
164+
131165
define(:count) do |unit|
132166
unit.convert_to(:dozen) { |value| value / 12.0 }
133167
end
134-
168+
135169
define(:doz) do |unit|
136170
unit.alias :dozen
137171
unit.convert_to(:count) { |value| value * 12.0 }

lib/ruby-measurement/unit.rb

Lines changed: 31 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -3,85 +3,85 @@
33
class Measurement
44
class Unit
55
attr_reader :name, :aliases, :conversions
6-
6+
77
@definitions = {}
8-
8+
99
def initialize(name)
1010
@name = name.to_s
1111
@aliases = Set.new
1212
@conversions = {}
1313
add_alias(name)
1414
end
15-
15+
1616
def add_alias(*args)
1717
args.each do |unit_alias|
1818
@aliases << unit_alias.to_s
1919
self.class[unit_alias] = self
2020
end
2121
end
22-
22+
2323
def add_conversion(unit_name, &block)
2424
@conversions[unit_name.to_s] = block
2525
end
26-
26+
2727
def conversion(unit_name)
2828
unit = self.class[unit_name]
2929
return nil unless unit
30-
30+
3131
unit.aliases.each do |unit_alias|
3232
conversion = @conversions[unit_alias.to_s]
3333
return conversion if conversion
3434
end
35-
35+
3636
nil
3737
end
38-
38+
3939
def inspect
4040
to_s
4141
end
42-
42+
4343
def to_s
4444
name
4545
end
46-
46+
4747
def ==(obj)
4848
obj.kind_of?(self.class) && name == obj.name && aliases == obj.aliases && conversions.all? do |key, proc|
4949
[-2.5, -1, 0, 1, 2.5].all? { |n| proc.call(n) == obj.conversions[key].call(n) }
5050
end
5151
end
52-
53-
def self.define(unit_name, &block)
54-
Builder.new(unit_name, &block)
55-
end
56-
57-
def self.[](unit_name)
58-
@definitions[unit_name.to_s.downcase]
59-
end
60-
61-
def self.names
62-
@definitions.keys
63-
end
64-
65-
private
66-
67-
def self.[]=(unit_name, unit)
68-
@definitions[unit_name.to_s.downcase] = unit
52+
53+
class << self
54+
def define(unit_name, &block)
55+
Builder.new(unit_name, &block)
56+
end
57+
58+
def [](unit_name)
59+
@definitions[unit_name.to_s.downcase]
60+
end
61+
62+
def []=(unit_name, unit)
63+
@definitions[unit_name.to_s.downcase] = unit
64+
end
65+
66+
def names
67+
@definitions.keys
68+
end
6969
end
70-
70+
7171
class Builder
7272
def initialize(unit_name, &block)
7373
@unit = Unit.new(unit_name)
7474
block.call(self) if block_given?
7575
end
76-
76+
7777
def alias(*args)
7878
@unit.add_alias(*args)
7979
end
80-
80+
8181
def convert_to(unit_name, &block)
8282
@unit.add_conversion(unit_name, &block)
8383
end
84-
84+
8585
def to_unit
8686
@unit
8787
end

spec/ruby-measurement/core_ext/string_spec.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,29 +6,29 @@
66
subject { '3' }
77
specify { expect(subject.to_measurement).to eq Measurement.new(3) }
88
end
9-
9+
1010
describe 'with valid quantity and unit' do
1111
subject { '3 dozen' }
1212
specify { expect(subject.to_measurement).to eq Measurement.new(3, :dozen) }
1313
end
14-
14+
1515
describe 'with valid quantity and invalid unit' do
1616
subject { '3 people' }
1717
specify { expect { subject.to_measurement }.to raise_error }
1818
end
19-
19+
2020
describe 'with invalid input' do
2121
subject { 'foobar' }
2222
specify { expect { subject.to_measurement }.to raise_error }
2323
end
2424
end
25-
25+
2626
describe '#to_unit' do
2727
describe 'with valid unit' do
2828
subject { 'dozen' }
2929
specify { expect(subject.to_unit).to eq Measurement::Unit[:dozen] }
3030
end
31-
31+
3232
describe 'with invalid unit' do
3333
subject { 'person' }
3434
specify { expect { subject.to_unit }.to raise_error }

0 commit comments

Comments
 (0)