unit_soup 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +230 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/unit_soup.rb +8 -0
- data/lib/unit_soup/measurement.rb +57 -0
- data/lib/unit_soup/mix.rb +51 -0
- data/lib/unit_soup/rule.rb +56 -0
- data/lib/unit_soup/soup.rb +88 -0
- data/lib/unit_soup/unit.rb +35 -0
- data/lib/unit_soup/version.rb +3 -0
- data/tmp/test.rb +5 -0
- data/tmp/tmp.rb +143 -0
- data/unit_soup.gemspec +27 -0
- metadata +110 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: e113ee9650c678b102c4788dd2aa7e8f51f9176d
|
4
|
+
data.tar.gz: c614b7fd95cc1b150e1fee659305b80eaf0eb32e
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: a1d5e2ddf493db14064bdb275ce195924658e23b5f280253ea97075ba2a004da198cd4db3191ba4dd71095e6ac9916ca06833a5dd18b3650d6b7caaca4aa1cd7
|
7
|
+
data.tar.gz: ffcb15850fef0a8601bea78c77faa78ca5480cf1edaf5c8b22c71e530de0cf846e96c4d10cfa91fc10e3c8bb6088a069e38945c9323548d86fd8f3bc5ce4a56e
|
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 rutvij
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
# UnitSoup
|
2
|
+
|
3
|
+
Unit Soup is a DRY approach to unit conversion.
|
4
|
+
Specify a set of conversion rules and you can start converting from one unit to another.
|
5
|
+
|
6
|
+
## Analogy
|
7
|
+
- `Mix` is a ready-made set of units and conversion rules.
|
8
|
+
- `Soup` is made from (soup) mixes.
|
9
|
+
- `Measurement` is amount of items of a particular type (unit). (e.g. 3 cars, 4 miles)
|
10
|
+
- `Soup` can convert one `measurement` to another if possible.
|
11
|
+
|
12
|
+
## Usage
|
13
|
+
* Add as many `mixes` to `soup`.
|
14
|
+
* Make the `soup`.
|
15
|
+
* Start converting `measurements` from one unit to another.
|
16
|
+
|
17
|
+
## Examples
|
18
|
+
#### Metric Units
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
require "unit_soup/mix"
|
22
|
+
require "unit_soup/soup"
|
23
|
+
|
24
|
+
metric_units_mix = Mix.new("Metric Units") do |m|
|
25
|
+
m << "1 cm = 10 mm"
|
26
|
+
m << "1 dm = 10 cm"
|
27
|
+
m << "1 m = 100 cm"
|
28
|
+
m << "1 dcm = 10 m"
|
29
|
+
m << "1 hcm = 100 m"
|
30
|
+
m << "1 km = 1000 m"
|
31
|
+
end
|
32
|
+
soup = Soup.new("Metric Soup (mm to km)")
|
33
|
+
soup << metric_units_mix
|
34
|
+
soup.make
|
35
|
+
|
36
|
+
# convert 550 centimeters to meters
|
37
|
+
soup.convert(550, :cm, :m) #=> 5.5
|
38
|
+
# convert 50000 centimeters to kilometers
|
39
|
+
soup.convert(50000, :cm, :km) #=> 0.5
|
40
|
+
```
|
41
|
+
|
42
|
+
#### Simple expenses calculator
|
43
|
+
|
44
|
+
```ruby
|
45
|
+
require "unit_soup/mix"
|
46
|
+
require "unit_soup/soup"
|
47
|
+
|
48
|
+
expenses_per_time_period_mix = Mix.new("Expenses/TimePeriod") do |mix|
|
49
|
+
mix << "1 per_day = 7 per_week"
|
50
|
+
mix << "1 per_week = 4 per_month"
|
51
|
+
mix << "1 per_month = 12 per_year"
|
52
|
+
mix << "1 per_weekend = 1 per_week"
|
53
|
+
mix << "1 per_weekday = 5 per_week"
|
54
|
+
end
|
55
|
+
|
56
|
+
soup = Soup.new("Expense calculator")
|
57
|
+
soup << expenses_per_time_period_mix
|
58
|
+
soup.make
|
59
|
+
|
60
|
+
|
61
|
+
parking_per_week = Measurement.new(50, "per_week")
|
62
|
+
parking_per_month = soup.convert(parking_per_week, :per_month) #=> 200
|
63
|
+
parking_per_year = soup.convert(parking_per_week, :per_year) #=> 2400
|
64
|
+
```
|
65
|
+
|
66
|
+
|
67
|
+
## Details
|
68
|
+
### Unit
|
69
|
+
- Has `name` and `symbol`
|
70
|
+
- `Unit.new("Kilometer", :km)` or `Unit.new(:km)`
|
71
|
+
- Usually inferred from the rules
|
72
|
+
|
73
|
+
### Measurement
|
74
|
+
- Describes things like "3 cars", "4 miles"
|
75
|
+
- Has `amount` and `unit`
|
76
|
+
- m = `Measurement.new("3 car")`
|
77
|
+
- `m.unit` => `:car`
|
78
|
+
- `m.amount` => `3/1 (rational)`
|
79
|
+
- Other ways to initialize
|
80
|
+
- `Measurement.from("4 mile")`
|
81
|
+
- `Measurement.new(4, :mile)`
|
82
|
+
- `Measurement.new(4, Unit.new(:mile))`
|
83
|
+
- `Measurement.new(2.5, :mile)`
|
84
|
+
- `Measurement.new("2.5", :mile)`
|
85
|
+
- `Measurement.new(3/2r, :mile)`
|
86
|
+
- `Measurement.new("3/2", :mile)`
|
87
|
+
- `Measurement.from(another_measurement)`
|
88
|
+
|
89
|
+
### Rule
|
90
|
+
- Describes conversion rule from one unit to another.
|
91
|
+
- Can be specified as equality of two measurements.
|
92
|
+
- `1 km = 1000 m`
|
93
|
+
- `this_measurement` = `1 km`, `that_measurement` = `1000 m`
|
94
|
+
- r = `Rule.new("1 km = 1000 m")` or `Rule.new("1000 m = 1 km")`
|
95
|
+
- `valid?` can be used to check if a string can be parsed into a `Rule`
|
96
|
+
- Can be initialized as
|
97
|
+
- `Rule.new "1 km = 1000 m"`
|
98
|
+
- `Rule.from "1 km = 1000 m"`
|
99
|
+
- `Rule.from another_rule`
|
100
|
+
- `Rule.new measurement_a, measurement_b`
|
101
|
+
- `Rule.new Measurement.new("1 km"), Measurement.new(1000, :m)`
|
102
|
+
|
103
|
+
### Mix
|
104
|
+
- Represents a set of rules
|
105
|
+
- Can be used to group related units together. e.g. Metric Units, time units, etc.
|
106
|
+
- Has a name and a Set of rules
|
107
|
+
- Initializing: time_units = `Mix.new("Time Units")` or `Mix.define("Time Units")`
|
108
|
+
- Adding Rules:
|
109
|
+
- `time_units << "1 minute = 60 second"`
|
110
|
+
- `time_units << Rule.new("1 minute = 60 second")`
|
111
|
+
- `time_units.add "1 minute = 60 second"`
|
112
|
+
- `time_units.add Rule.new("1 minute = 60 second")`
|
113
|
+
- From Rule strings list: `distance << ["1cm = 10mm", "1km = 1.60934mile"]`
|
114
|
+
- From Rule list: `distance << [Rule.new("1cm = 10mm"), Rule.new("1km = 1.60934mile")]`
|
115
|
+
- From mixed list: `distance << ["1cm = 10mm", Rule.new("1km = 1.60934mile")]`
|
116
|
+
- From other mixes:
|
117
|
+
- `my_mix << time_units`
|
118
|
+
- `my_mix << distance_units`
|
119
|
+
|
120
|
+
- Initialize via block:
|
121
|
+
```
|
122
|
+
time_units = Mix.define("Time units") do |m|
|
123
|
+
m << "1 minute = 60 second"
|
124
|
+
m << ["1 hour = 60 minute", "1 millisecond = 1000 second"]
|
125
|
+
end
|
126
|
+
```
|
127
|
+
|
128
|
+
### Soup
|
129
|
+
- Soup allows conversion from one unit to another
|
130
|
+
- Has an optional name
|
131
|
+
- Supports adding rules from strings, lists and mixes exactly like `Mix`
|
132
|
+
- `Soup.new("time_soup") << time_units_mix`
|
133
|
+
- Add as many related/unrelated rules and mixes as you like to the soup
|
134
|
+
- `make`: Make the soup (internally creates a graph of all rules for conversion lookups)
|
135
|
+
- `convert(value, from, to)` => rational representing converted value in target unit
|
136
|
+
- `convert(2, :minute, :second)` => 120/1
|
137
|
+
- `convert(2, Unit.new(:minute), :second)` => 120/1
|
138
|
+
- `convert(50, :foo, :bar)` => `nil` #if conversion was not possible
|
139
|
+
- Return value is a rational to keep precision
|
140
|
+
- `make` needs to be called before `convert`s can work
|
141
|
+
|
142
|
+
#### Conversion
|
143
|
+
- `soup.make` Creates a graph from the rules. So,
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
soup = Soup.new("my_distance_converter")
|
147
|
+
soup << Mix.new("my_distance_mix") do |m|
|
148
|
+
m << "1 centimeter = 10 millimeter"
|
149
|
+
m << "1 decimeter = 10 centimeter"
|
150
|
+
m << "1 centimeter = 100 meter"
|
151
|
+
m << "1 centimeter = 0.39 inch"
|
152
|
+
m << "1 foot = 12 inch"
|
153
|
+
m << "1 kilometer = 1000 meter"
|
154
|
+
m << "1 kilometer = 0.62 mile"
|
155
|
+
end
|
156
|
+
soup.make
|
157
|
+
```
|
158
|
+
|
159
|
+
becomes
|
160
|
+
```
|
161
|
+
+------------+
|
162
|
+
| millimeter |
|
163
|
+
+------------+
|
164
|
+
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
+-----------+ +------------+ +------+ +------+
|
168
|
+
| decimeter | --- | centimeter | --- | inch | --- | foot |
|
169
|
+
+-----------+ +------------+ +------+ +------+
|
170
|
+
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
+-----------+ +------------+
|
174
|
+
| kilometer | --- | meter |
|
175
|
+
+-----------+ +------------+
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
+-----------+
|
180
|
+
| mile |
|
181
|
+
+-----------+
|
182
|
+
```
|
183
|
+
|
184
|
+
- Any rules added after `soup.make` will not be a part of the graph until another call to `soup.make` is made.
|
185
|
+
|
186
|
+
- A `lookup` is initialized internally with all direct conversions inferred from the set of rules. e.g. From "1 foot = 12 inch" we can infer `lookup[:foot][:inch]=12` and `lookup[:inch][:foot]=1/12`
|
187
|
+
|
188
|
+
- `soup.convert(2, :foot, :inch)` - `lookup` has information to convert from `foot` to `inch`, so `convert` returns `2 * (lookup[:foot][:inch]=12) = 24/1`
|
189
|
+
|
190
|
+
- `soup.convert(200, :foot, :meter)` - `lookup` does not have this information, so a `bfs` graph traversal starting from `foot` is performed. It gives us the following chain that can be traversed to get the right fraction for multiplication to perform conversion:
|
191
|
+
|
192
|
+
```
|
193
|
+
+-------+ +------------+ +------+ +------+
|
194
|
+
| meter | <--- | centimeter | <--- | inch | <--- | foot |
|
195
|
+
+-------+ +------------+ +------+ +------+
|
196
|
+
|
197
|
+
200 * lookup[:foot][:inch] * lookup[:inch][:centimeter] * lookup[:centimeter][meter]
|
198
|
+
```
|
199
|
+
|
200
|
+
This conversion fraction is also saved in `lookup[:foot][:meter]` and `lookup[:meter][:foot]` so the next conversion from `foot` to `meter` does not perform a graph traversal.
|
201
|
+
|
202
|
+
## Installation
|
203
|
+
|
204
|
+
Add this line to your application's Gemfile:
|
205
|
+
|
206
|
+
```ruby
|
207
|
+
gem 'unit_soup'
|
208
|
+
```
|
209
|
+
|
210
|
+
And then execute:
|
211
|
+
|
212
|
+
$ bundle
|
213
|
+
|
214
|
+
Or install it yourself as:
|
215
|
+
|
216
|
+
$ gem install unit_soup
|
217
|
+
|
218
|
+
## Development
|
219
|
+
|
220
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
221
|
+
|
222
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
223
|
+
|
224
|
+
## Contributing
|
225
|
+
|
226
|
+
Bug reports and pull requests are welcome.
|
227
|
+
|
228
|
+
## License
|
229
|
+
|
230
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "unit_soup"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/unit_soup.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
require "unit_soup/unit"
|
2
|
+
|
3
|
+
include UnitSoup
|
4
|
+
|
5
|
+
module UnitSoup
|
6
|
+
class Measurement
|
7
|
+
# captures "(num)(symbol)" where num = decimal|integer|fraction
|
8
|
+
@@measurement_format = %r{^(\d+|\d+\.\d+|\d+/[1-9]+|\d+\.\d+/[1-9]+)([a-zA-Z_]+)$}
|
9
|
+
|
10
|
+
def self.valid?(str)
|
11
|
+
str && !str.to_s.gsub("\s", "").match(@@measurement_format).nil?
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from(*args)
|
15
|
+
self.new *args
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :amount, :unit
|
19
|
+
|
20
|
+
def initialize(*args)
|
21
|
+
case args.length
|
22
|
+
when 1
|
23
|
+
if args[0].is_a? Measurement
|
24
|
+
@amount = args[0].amount
|
25
|
+
@unit = args[0].unit
|
26
|
+
else
|
27
|
+
str = args[0]
|
28
|
+
raise ArgumentError.new("No argument provided") unless str
|
29
|
+
str = str.to_s
|
30
|
+
match_data = str.to_s.gsub("\s", "").match(@@measurement_format)
|
31
|
+
raise ArgumentError.new("Format: 12 inch") unless match_data
|
32
|
+
@amount = match_data[1].to_r
|
33
|
+
@unit = Unit.new(match_data[2])
|
34
|
+
end
|
35
|
+
else
|
36
|
+
@amount = args[0].is_a?(String) ? args[0].to_r : args[0].rationalize
|
37
|
+
@unit = Unit.new(args[1].to_sym)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_s
|
42
|
+
"#{amount} #{unit}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def ==(o)
|
46
|
+
amount == o.amount && unit == o.unit
|
47
|
+
end
|
48
|
+
|
49
|
+
def eql?(o)
|
50
|
+
amount == o.amount && unit == o.unit
|
51
|
+
end
|
52
|
+
|
53
|
+
def hash
|
54
|
+
to_s.hash
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "unit_soup/rule"
|
2
|
+
require "unit_soup/unit"
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
module UnitSoup
|
6
|
+
class Mix
|
7
|
+
def self.define(name, &block)
|
8
|
+
mix = Mix.new name
|
9
|
+
if(block)
|
10
|
+
block.arity < 1 ? mix.instance_eval(&block) : block.call(mix)
|
11
|
+
end
|
12
|
+
mix
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :name, :rules
|
16
|
+
|
17
|
+
def initialize(name, &block)
|
18
|
+
@name = name
|
19
|
+
@rules = Set.new
|
20
|
+
if(block)
|
21
|
+
block.arity < 1 ? instance_eval(&block) : block.call(self)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def <<(arg)
|
26
|
+
new_rules = []
|
27
|
+
if arg.is_a? Mix
|
28
|
+
new_rules += arg.rules.to_a
|
29
|
+
elsif arg.is_a? Rule
|
30
|
+
new_rules << arg
|
31
|
+
elsif arg.is_a? Enumerable
|
32
|
+
new_rules += arg.map do |a|
|
33
|
+
if a.is_a? Rule
|
34
|
+
a
|
35
|
+
else
|
36
|
+
Rule.new(a)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
else
|
40
|
+
new_rules << Rule.new(arg)
|
41
|
+
end
|
42
|
+
new_rules.each {|r| @rules.add r}
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :add, :<<
|
46
|
+
|
47
|
+
def add_rules_from_file(file)
|
48
|
+
#TODO: parse rules from file
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "unit_soup/unit"
|
2
|
+
require "unit_soup/measurement"
|
3
|
+
|
4
|
+
include UnitSoup
|
5
|
+
|
6
|
+
module UnitSoup
|
7
|
+
class Rule
|
8
|
+
def self.valid?(str)
|
9
|
+
return false unless str
|
10
|
+
strs = str.split '='
|
11
|
+
strs.length > 1 && Measurement.valid?(strs[0]) && Measurement.valid?(strs[1])
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.from(r)
|
15
|
+
self.new r
|
16
|
+
end
|
17
|
+
|
18
|
+
attr_reader :this_measurement, :that_measurement
|
19
|
+
|
20
|
+
def initialize(*args)
|
21
|
+
case args.length
|
22
|
+
when 1
|
23
|
+
r = args[0]
|
24
|
+
raise ArgumentError.new("No argument provided") unless r
|
25
|
+
if r.is_a? Rule
|
26
|
+
@this_measurement = Measurement.new r.this_measurement
|
27
|
+
@that_measurement = Measurement.new r.that_measurement
|
28
|
+
else
|
29
|
+
strs = r.to_s.split '='
|
30
|
+
raise ArgumentError.new("Format: 12 inch = 1 foot") unless strs.length == 2
|
31
|
+
@this_measurement = Measurement.new strs[0]
|
32
|
+
@that_measurement = Measurement.new strs[1]
|
33
|
+
end
|
34
|
+
else
|
35
|
+
@this_measurement = Measurement.new(args[0])
|
36
|
+
@that_measurement = Measurement.new(args[1])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
"#{this_measurement} = #{that_measurement}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def ==(o)
|
45
|
+
this_measurement == o.this_measurement && that_measurement == o.that_measurement
|
46
|
+
end
|
47
|
+
|
48
|
+
def eql?(o)
|
49
|
+
this_measurement == o.this_measurement && that_measurement == o.that_measurement
|
50
|
+
end
|
51
|
+
|
52
|
+
def hash
|
53
|
+
to_s.hash
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require "unit_soup/rule"
|
2
|
+
require "unit_soup/unit"
|
3
|
+
require "unit_soup/mix"
|
4
|
+
|
5
|
+
include UnitSoup
|
6
|
+
|
7
|
+
module UnitSoup
|
8
|
+
class Soup
|
9
|
+
attr_reader :name
|
10
|
+
def initialize(name="_")
|
11
|
+
@name = name
|
12
|
+
@mix = Mix.new("default")
|
13
|
+
@units = Set.new
|
14
|
+
@rules = Set.new
|
15
|
+
# symbol -> unit
|
16
|
+
@symbols_map = {}
|
17
|
+
@lookup = {}
|
18
|
+
@graph = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(m)
|
22
|
+
@mix << m
|
23
|
+
end
|
24
|
+
|
25
|
+
def rules
|
26
|
+
@mix.rules
|
27
|
+
end
|
28
|
+
|
29
|
+
def make
|
30
|
+
@units.clear
|
31
|
+
@lookup = {}
|
32
|
+
@graph = {}
|
33
|
+
@mix.rules.each do |r|
|
34
|
+
@units << r.this_measurement.unit
|
35
|
+
@units << r.that_measurement.unit
|
36
|
+
this_unit = r.this_measurement.unit
|
37
|
+
that_unit = r.that_measurement.unit
|
38
|
+
|
39
|
+
this_in_that = Measurement.new((r.that_measurement.amount/r.this_measurement.amount).rationalize, that_unit)
|
40
|
+
that_in_this = Measurement.new((r.this_measurement.amount/r.that_measurement.amount).rationalize, this_unit)
|
41
|
+
|
42
|
+
# add direct conversions to lookup
|
43
|
+
@lookup[[r.this_measurement.unit, r.that_measurement.unit]] = this_in_that
|
44
|
+
@lookup[[r.that_measurement.unit, r.this_measurement.unit]] = that_in_this
|
45
|
+
|
46
|
+
# build graph
|
47
|
+
# rule "3 foo = 4 bar" represented as {foo => [4/3 bar], bar => [3/4 foo]}
|
48
|
+
@graph[r.this_measurement.unit] = Set.new unless @graph.include?(r.this_measurement.unit)
|
49
|
+
@graph[r.this_measurement.unit] << this_in_that
|
50
|
+
@graph[r.that_measurement.unit] = Set.new unless @graph.include?(r.that_measurement.unit)
|
51
|
+
@graph[r.that_measurement.unit] << that_in_this
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def convert(value, from, to)
|
56
|
+
from = Unit.new(from)
|
57
|
+
to = Unit.new(to)
|
58
|
+
return nil unless @units.include?(from) && @units.include?(to)
|
59
|
+
return (value * (@lookup[[from, to]]).amount) if @lookup.include? [from, to]
|
60
|
+
# bfs graph from from-to. When found, walk through parent path and multiply all conversion factors.
|
61
|
+
Struct.new("Child", :measurement, :parent)
|
62
|
+
queue = [Struct::Child.new(Measurement.new(value, from), nil)]
|
63
|
+
while !queue.empty? do
|
64
|
+
parent = queue.first
|
65
|
+
# Find all measurement matches
|
66
|
+
# e.g. if graph[:from] = ["3/4 :foo", "4/5 :bar", "3/1 :to", "5/1 :to"]
|
67
|
+
# then matches = ["3/1 :to", "5/1 :to"]
|
68
|
+
# Since there are two valid rules to convert :from -> :to, we pick the first one
|
69
|
+
matches = @graph[parent.measurement.unit].select{|m|m.unit == to}
|
70
|
+
if(matches.size > 0)
|
71
|
+
# create the chain [from, a, b, c, to], starting with 'to' and building back
|
72
|
+
chain_from_to = [Struct::Child.new(matches.first, parent)]
|
73
|
+
curr = chain_from_to.first
|
74
|
+
while(curr.parent) do
|
75
|
+
chain_from_to.unshift curr.parent # add current parent to front of chain
|
76
|
+
curr = curr.parent
|
77
|
+
end
|
78
|
+
return chain_from_to.map{|c|c.measurement.amount}.inject(1){|m1,m2| m1 * m2}
|
79
|
+
else
|
80
|
+
children = @graph[parent.measurement.unit]
|
81
|
+
children_not_in_queue = children.reject{|m| queue.include? m}
|
82
|
+
queue += children_not_in_queue.map{|m| Struct::Child.new(m, parent)}
|
83
|
+
queue.shift
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module UnitSoup
|
2
|
+
|
3
|
+
class Unit
|
4
|
+
attr_reader :name, :symbol
|
5
|
+
|
6
|
+
def initialize(name=nil, symbol)
|
7
|
+
@symbol = symbol.to_sym
|
8
|
+
@name = name.nil? ? @symbol.to_sym : name.to_sym
|
9
|
+
end
|
10
|
+
|
11
|
+
def name=(name)
|
12
|
+
@name = name.to_sym
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
symbol.to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_sym
|
20
|
+
symbol.to_sym
|
21
|
+
end
|
22
|
+
|
23
|
+
def ==(o)
|
24
|
+
symbol == o.symbol
|
25
|
+
end
|
26
|
+
|
27
|
+
def eql?(o)
|
28
|
+
symbol == o.symbol
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
symbol.hash
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/tmp/test.rb
ADDED
data/tmp/tmp.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# trying out api
|
2
|
+
|
3
|
+
pack = UnitPack.create(name: "time periods") do |p|
|
4
|
+
p.name = "time periods"
|
5
|
+
p.desc = "some description"
|
6
|
+
p.rules = [
|
7
|
+
"7 days = 1 week"
|
8
|
+
]
|
9
|
+
p.rules File("conversion.rules")
|
10
|
+
# p.rules [
|
11
|
+
# {days: 7, week: 1}
|
12
|
+
# {weekday: 5, week: 1}
|
13
|
+
# {weekday: 1, days: 1}
|
14
|
+
# {weekend: 1, days: 2}
|
15
|
+
# {weekend: 1, week: 1}
|
16
|
+
# ]
|
17
|
+
p.aliases = {
|
18
|
+
days: %w(day days)
|
19
|
+
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
Rule.new("1 days", "24 hours")
|
25
|
+
Convertible::Unit(:name, %w(aliases), rules)
|
26
|
+
Convertible::Measurement
|
27
|
+
Convertible::Unit
|
28
|
+
Convertible::DefinitionSet
|
29
|
+
Convertible::RuleSet
|
30
|
+
Measurement/Reading
|
31
|
+
Rule = measurementA = measurementB
|
32
|
+
UnitPack.merge()
|
33
|
+
UnitPack.convert measurementA unitB
|
34
|
+
UnitPack.conversionFactor measurementA unitB
|
35
|
+
DefinitionSet/ConversionRuleSet/UnitPack/UnitGroup
|
36
|
+
|
37
|
+
UnitSoup::Unit
|
38
|
+
UnitSoup::Measurement
|
39
|
+
UnitSoup::Mix
|
40
|
+
|
41
|
+
soupmix = Mix.new()
|
42
|
+
soup.make
|
43
|
+
soup.add(Mix::ImperialUnits)
|
44
|
+
soup.make
|
45
|
+
soup.convert(measurement, "cm")
|
46
|
+
soup.conversion_factor("inch", "cm")
|
47
|
+
soup.conversion_path("inch", "cm")
|
48
|
+
soup.rules << Mix | Rule | rules
|
49
|
+
soup.rules = Mix | Rule | rules
|
50
|
+
soup.rules Mix | Rule | rules
|
51
|
+
soup.rule = Rule
|
52
|
+
soup.rule Rule
|
53
|
+
|
54
|
+
BudgetItem < Convertible::Measurement
|
55
|
+
|
56
|
+
SetDefinition
|
57
|
+
|
58
|
+
metric_units = Convertible::Set.define do
|
59
|
+
name "Metric Units"
|
60
|
+
rules [
|
61
|
+
"100 cm = 1 decimeter",
|
62
|
+
"100 decimeter = 1 meter",
|
63
|
+
"1000 meter = 1 km"
|
64
|
+
]
|
65
|
+
rule "2.5 cm = 1 inch"
|
66
|
+
rules_from_file :file_path
|
67
|
+
unit(:km).same_as("KM", :kilo_meters) # aliases
|
68
|
+
unit(:centimeter).use_symbol("cm")
|
69
|
+
unit(:cm).use_plural("centimeters")
|
70
|
+
unit(:cm).use_singular(short: cm, long="centimeter")
|
71
|
+
unit(:cm).use(symbol: :cm, singular: :centimeter, :plural: "centimeters", name: "Centimeter")
|
72
|
+
unit(:cm).rule("2.5cm", "1 inch")
|
73
|
+
unit(:cm).rule("2.5", 1, unit(:inch))
|
74
|
+
unit(:cm).rule("2.5", unit(1, :inch))
|
75
|
+
end
|
76
|
+
|
77
|
+
Convertible::Set.from/with/union (Convertible::SetDefinition::Metric, dollar_units)
|
78
|
+
my_set.add(dollar_units).
|
79
|
+
|
80
|
+
|
81
|
+
# algo
|
82
|
+
@lookup @graph
|
83
|
+
convert(measurement, from, to)
|
84
|
+
return from lookup if lookup has from, to factor
|
85
|
+
bfssearch from to - when found, take path, collect factors
|
86
|
+
return converted or show error
|
87
|
+
|
88
|
+
when rules change
|
89
|
+
reset lookup and graph
|
90
|
+
build graph from rules
|
91
|
+
rules.each do
|
92
|
+
graph[this][that]=factor
|
93
|
+
graph[that][this]=1/factor
|
94
|
+
|
95
|
+
# classes
|
96
|
+
Rule = Measurement - Measurement
|
97
|
+
Measurement = Amount.to_r Unit
|
98
|
+
Unit(to_sym, symbols)
|
99
|
+
Amount(to_r)
|
100
|
+
Soup = set<rule>
|
101
|
+
|
102
|
+
Soup
|
103
|
+
@mix = Mix.new(name)
|
104
|
+
@units = []
|
105
|
+
@rules = []
|
106
|
+
@symbols_map = {:symbol => unit}
|
107
|
+
@lookup
|
108
|
+
@graph
|
109
|
+
|
110
|
+
|
111
|
+
# brainstorming unit singletons
|
112
|
+
mix << rules
|
113
|
+
mix[:unit].symbols << :cms
|
114
|
+
soup.prepare
|
115
|
+
soup.unit[:unit].same_as :longer_name
|
116
|
+
soup.make
|
117
|
+
soup = Soup.new("mysoup", mix1, mix2)
|
118
|
+
soup << mix3
|
119
|
+
soup << rule1, rule2, mix
|
120
|
+
soup.unit(:unit).field
|
121
|
+
soup.units[:unit].symbols
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
# unit additional functionality
|
126
|
+
:singular, :plural,
|
127
|
+
def initialize(options={})
|
128
|
+
@symbols = []
|
129
|
+
@name = options[:name].to_sym if options[:name]
|
130
|
+
@singular = options[:singular].to_sym if options[:singular]
|
131
|
+
@plural = options[:plural].to_sym if options[:plural]
|
132
|
+
@symbol = options[:use].to_sym if options[:use]
|
133
|
+
@symbol = options[:symbol].to_sym if @symbol.nil? && options[:symbol]
|
134
|
+
@symbol = options[:preffered].to_sym if @symbol.nil? && options[:preffered]
|
135
|
+
@symbols += options[:aliases] if options[:aliases]
|
136
|
+
@symbols += options[:symbols] if options[:symbols]
|
137
|
+
end
|
138
|
+
|
139
|
+
def symbols
|
140
|
+
(@symbols + [@name, @singular, @plural, @symbol]).reject{|s|s.blank?}.sort.uniq
|
141
|
+
end
|
142
|
+
|
143
|
+
alias_method :preffered, :symbol
|
data/unit_soup.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path("../lib", __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require "unit_soup/version"
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "unit_soup"
|
8
|
+
spec.version = UnitSoup::VERSION
|
9
|
+
spec.authors = ["Rutvij"]
|
10
|
+
spec.email = ["code@rutvijshah.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{A DRY approach to unit conversion.}
|
13
|
+
spec.description = %q{A DRY approach to unit conversion. Define rules, make soup, convert.}
|
14
|
+
spec.homepage = "http://www.rutvijshah.com"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = "exe"
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ["lib"]
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.15"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "rspec", '~> 3.5', '>= 3.5.0'
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: unit_soup
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Rutvij
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-10-02 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.15'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.15'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.5'
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: 3.5.0
|
51
|
+
type: :development
|
52
|
+
prerelease: false
|
53
|
+
version_requirements: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3.5'
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 3.5.0
|
61
|
+
description: A DRY approach to unit conversion. Define rules, make soup, convert.
|
62
|
+
email:
|
63
|
+
- code@rutvijshah.com
|
64
|
+
executables: []
|
65
|
+
extensions: []
|
66
|
+
extra_rdoc_files: []
|
67
|
+
files:
|
68
|
+
- ".gitignore"
|
69
|
+
- ".rspec"
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- bin/console
|
75
|
+
- bin/setup
|
76
|
+
- lib/unit_soup.rb
|
77
|
+
- lib/unit_soup/measurement.rb
|
78
|
+
- lib/unit_soup/mix.rb
|
79
|
+
- lib/unit_soup/rule.rb
|
80
|
+
- lib/unit_soup/soup.rb
|
81
|
+
- lib/unit_soup/unit.rb
|
82
|
+
- lib/unit_soup/version.rb
|
83
|
+
- tmp/test.rb
|
84
|
+
- tmp/tmp.rb
|
85
|
+
- unit_soup.gemspec
|
86
|
+
homepage: http://www.rutvijshah.com
|
87
|
+
licenses:
|
88
|
+
- MIT
|
89
|
+
metadata: {}
|
90
|
+
post_install_message:
|
91
|
+
rdoc_options: []
|
92
|
+
require_paths:
|
93
|
+
- lib
|
94
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
requirements: []
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.6.13
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: A DRY approach to unit conversion.
|
110
|
+
test_files: []
|