measured 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1534c3781494b704e57f0a027deed911ee1bd826
4
+ data.tar.gz: 182ba934b6b9f2eb9ab75cd15f1465f7e8e568e3
5
+ SHA512:
6
+ metadata.gz: ccf9e081f0e5bba182c1dac65b3fe25d77cb709fb4a32257f9bdc58bd2b0be7239363c51839509ba4fa1bf09d40e72ec0ae5dbfe97a9105585723600d4759b8e
7
+ data.tar.gz: fd9cfc068c0d6157f62dda09420ba9adda1f4135cdf7abbbd2cfd2ab6c50d9802983db3791116cbedc9df1fe18712e1da0726eac736475f2b4c874c09cc3502d
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Kevin McPhillips
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 all
13
+ 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 THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,208 @@
1
+ # Measured
2
+
3
+ Encapsulates measruements with their units. Provides easy conversion between units.
4
+
5
+ Light weight and easily extensible to include other units and conversions. Conversions done with `BigDecimal` for precision.
6
+
7
+ ## Installation
8
+
9
+ Using bundler, add to the Gemfile:
10
+
11
+ ```ruby
12
+ gem 'measured'
13
+ ```
14
+
15
+ Or stand alone:
16
+
17
+ $ gem install measured
18
+
19
+ ## Usage
20
+
21
+ Initialize a measurement:
22
+
23
+ ```ruby
24
+ Measured::Weight.new("12", "g")
25
+ ```
26
+
27
+ Convert to return a new measurement:
28
+
29
+ ```ruby
30
+ Measured::Weight.new("12", "g").convert_to("kg")
31
+ ```
32
+
33
+ Or convert inline:
34
+
35
+ ```ruby
36
+ Measured::Weight.new("12", "g").convert_to!("kg")
37
+ ```
38
+
39
+ Agnostic to symbols/strings:
40
+
41
+ ```ruby
42
+ Measured::Weight.new(1, "kg") == Measured::Weight.new(1, :kg)
43
+ ```
44
+
45
+ Seamlessly handles aliases:
46
+
47
+ ```ruby
48
+ Measured::Weight.new(12, :oz) == Measured::Weight.new("12", :ounce)
49
+ ```
50
+
51
+ Raises on unknown units:
52
+
53
+ ```ruby
54
+ begin
55
+ Measured::Weight.new(1, :stone)
56
+ rescue Measured::UnitError
57
+ puts "Unknown unit"
58
+ end
59
+ ```
60
+
61
+ Perform mathematical operations against other units, all represented internally as `BigDecimal`:
62
+
63
+ ```ruby
64
+ Measured::Weight.new(1, :g) + Measured::Weight.new(2, :g)
65
+ > #<Measured::Weight 3 g>
66
+ Measured::Weight.new(2, :g) - Measured::Weight.new(1, :g)
67
+ > #<Measured::Weight 1 g>
68
+ Measured::Weight.new(10, :g) / Measured::Weight.new(2, :g)
69
+ > #<Measured::Weight 5 g>
70
+ Measured::Weight.new(2, :g) * Measured::Weight.new(3, :g)
71
+ > #<Measured::Weight 6 g>
72
+ ```
73
+
74
+ In cases of differing units, the left hand side takes precedence:
75
+
76
+ ```ruby
77
+ Measured::Weight.new(1000, :g) + Measured::Weight.new(1, :kg)
78
+ > #<Measured::Weight 2000 g>
79
+ ```
80
+
81
+ Also perform mathematical operations against `Numeric` things:
82
+
83
+ ```ruby
84
+ Measured::Weight.new(3, :g) * 2
85
+ > #<Measured::Weight 6 g>
86
+ ```
87
+
88
+ Extract the unit and the value:
89
+
90
+ ```ruby
91
+ weight = Measured::Weight.new("1.2", "grams")
92
+ weight.value
93
+ > #<BigDecimal 1.2>
94
+ weight.unit
95
+ > "g"
96
+ ```
97
+
98
+ See all valid units:
99
+
100
+ ```ruby
101
+ Measured::Weight.units
102
+ > ["g", "kg", "lb", "oz"]
103
+ ```
104
+
105
+ See all valid units with their aliases:
106
+
107
+ ```ruby
108
+ Measured::Weight.units_with_aliases
109
+ > ["g", "gram", "grams", "kg", "kilogram", "kilograms", "lb", "lbs", "ounce", "ounces", "oz", "pound", "pounds"]
110
+ ```
111
+
112
+ ## Units and conversions
113
+
114
+ ### Bundled unit conversion
115
+
116
+ * `Measured::Weight`
117
+ * g, gram, grams
118
+ * kg, kilogram, kilograms
119
+ * lb, lbs, pound, pounds
120
+ * oz, ounce, ounces
121
+ * `Measured::Length`
122
+ * m, meter, metre, meters, metres
123
+ * cm, centimeter, centimetre, centimeters, centimetres
124
+ * mm, millimeter, millimetre, millimeters, millimetres
125
+ * in, inch, inches
126
+ * ft, foot, feet
127
+ * yd, yard, yards
128
+
129
+ You can skip these and only define your own units by doing:
130
+
131
+ ```ruby
132
+ gem 'measured', require: 'measured/base'
133
+ ```
134
+
135
+ ### Adding new units
136
+
137
+ Extending this library to support other units is simple. To add a new conversion, subclass `Measured::Measurable`, define your base units, then add your conversion units.
138
+
139
+ ```ruby
140
+ class Measured::Thing < Measured::Measurable
141
+ conversion.set_base :base_unit, # Define the basic unit for the system
142
+ aliases: [:bu] # Allow it to be aliased to other names/symbols
143
+
144
+ conversion.add :another_unit, # Add a second unit to the system
145
+ aliases: [:au], # All units allow aliases, as long as they are unique
146
+ value: ["1.5 base_unit"] # The conversion rate to another unit
147
+
148
+ conversion.add :different_unit
149
+ aliases: [:du],
150
+ value: [Rational(2/3), "another_unit"] # Conversion rate can be Rational, otherwise it is coerced to BigDecimal
151
+ end
152
+ ```
153
+
154
+ The base unit takes no value. Values for conversion units can be defined as a string with two tokens `"number unit"` or as an array with two elements. The numbers must be `Rational` or `BigDecimal`, else they will be coerced to `BigDecimal`. Conversion paths don't have to be direct as a conversion table will be built for all possible conversions using tree traversal.
155
+
156
+ You can also open up the existing classes and add a new conversion:
157
+
158
+ ```ruby
159
+ class Measured::Length
160
+ conversion.add :dm,
161
+ aliases: [:decimeter, :decimetre, :decimeters, :decimetres],
162
+ value: "0.1 m"
163
+ end
164
+ ```
165
+
166
+ ### Namespaces
167
+
168
+ All units and classes are namespaced by default, but can be aliased in your application.
169
+
170
+ ```ruby
171
+ Weight = Measured::Weight
172
+ Length = Measured::Length
173
+ ```
174
+
175
+ ## Alternatives
176
+
177
+ Existing alternatives which were considered:
178
+
179
+ ### Gem: [ruby-units](https://github.com/olbrich/ruby-units)
180
+ * **Pros**
181
+ * Accurate math and conversion factors.
182
+ * Includes nearly every unit you could ask for.
183
+ * **Cons**
184
+ * Opens up and modifies `Array`, `Date`, `Fixnum`, `Math`, `Numeric`, `String`, `Time`, and `Object`, then depends on those changes internally.
185
+ * Lots of code to solve a relatively simple problem.
186
+ * No ActiveRecord adapter.
187
+
188
+ ### Gem: [quantified](https://github.com/Shopify/quantified)
189
+ * **Pros**
190
+ * Light weight.
191
+ * Included with ActiveShipping/ActiveUtils.
192
+ * **Cons**
193
+ * All math done with floats making it highly lossy.
194
+ * All units assumed to be pluralized, meaning using unit abbreviations is not possible.
195
+ * Not actively maintained.
196
+ * No ActiveRecord adapter.
197
+
198
+ ## Contributing
199
+
200
+ 1. Fork it ( https://github.com/Shopify/measured/fork )
201
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
202
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
203
+ 4. Push to the branch (`git push origin my-new-feature`)
204
+ 5. Create a new Pull Request
205
+
206
+ ## Authors
207
+
208
+ * [Kevin McPhillips](https://github.com/kmcphillips) at [Shopify](http://shopify.com/careers)
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ $LOAD_PATH.unshift File.expand_path("../lib", __FILE__)
5
+ require "measured/version"
6
+
7
+ task default: :test
8
+
9
+ desc 'Run the test stuite'
10
+ Rake::TestTask.new do |t|
11
+ t.libs << "test"
12
+ t.libs << "lib/**/*"
13
+ t.test_files = FileList['test/**/*_test.rb']
14
+ t.verbose = true
15
+ end
16
+
17
+ task tag: :build do
18
+ system "git commit -m'Released version #{ Measured::VERSION }' --allow-empty"
19
+ system "git tag -a v#{ Measured::VERSION } -m 'Tagging #{ Measured::VERSION }'"
20
+ system "git push --tags"
21
+ end
@@ -0,0 +1,37 @@
1
+ module Measured::Arithmetic
2
+ def +(other)
3
+ arithmetic_operation(other, :+)
4
+ end
5
+
6
+ def -(other)
7
+ arithmetic_operation(other, :-)
8
+ end
9
+
10
+ def *(other)
11
+ arithmetic_operation(other, :*)
12
+ end
13
+
14
+ def /(other)
15
+ arithmetic_operation(other, :/)
16
+ end
17
+
18
+ def -@
19
+ self.class.new(-self.value, self.unit)
20
+ end
21
+
22
+ def coerce(other)
23
+ [self, other]
24
+ end
25
+
26
+ private
27
+
28
+ def arithmetic_operation(other, operator)
29
+ if other.is_a?(self.class)
30
+ self.class.new(self.value.send(operator, other.convert_to(self.unit).value), self.unit)
31
+ elsif other.is_a?(Numeric)
32
+ self.class.new(self.value.send(operator, other), self.unit)
33
+ else
34
+ raise TypeError, "Invalid operation #{ operator } between #{ self.class } to #{ other.class }"
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,13 @@
1
+ require "measured/version"
2
+ require "active_support"
3
+ require "bigdecimal"
4
+
5
+ module Measured
6
+ class UnitError < StandardError ; end
7
+ end
8
+
9
+ require "measured/arithmetic"
10
+ require "measured/unit"
11
+ require "measured/conversion"
12
+ require "measured/conversion_table"
13
+ require "measured/measurable"
@@ -0,0 +1,91 @@
1
+ class Measured::Conversion
2
+ def initialize
3
+ @base_unit = nil
4
+ @units = []
5
+ end
6
+
7
+ attr_reader :base_unit, :units
8
+
9
+ def set_base(unit_name, aliases: [])
10
+ add_new_unit(unit_name, aliases: aliases, base: true)
11
+ end
12
+
13
+ def add(unit_name, aliases: [], value:)
14
+ add_new_unit(unit_name, aliases: aliases, value: value)
15
+ end
16
+
17
+ def unit_names_with_aliases
18
+ @units.map{|u| u.names}.flatten.sort
19
+ end
20
+
21
+ def unit_names
22
+ @units.map{|u| u.name}.sort
23
+ end
24
+
25
+ def unit_or_alias?(name)
26
+ unit_names_with_aliases.include?(name.to_s)
27
+ end
28
+
29
+ def unit?(name)
30
+ unit_names.include?(name.to_s)
31
+ end
32
+
33
+ def to_unit_name(name)
34
+ unit_for(name).name
35
+ end
36
+
37
+ def convert(value, from:, to:)
38
+ raise Measured::UnitError, "Source unit #{ from } does not exits." unless unit?(from)
39
+ raise Measured::UnitError, "Converted unit #{ to } does not exits." unless unit?(to)
40
+
41
+ from_unit = unit_for(from)
42
+ to_unit = unit_for(to)
43
+
44
+ raise Measured::UnitError, "Cannot find conversion entry from #{ from } to #{ to }" unless conversion = conversion_table[from][to]
45
+
46
+ value * conversion
47
+ end
48
+
49
+ def conversion_table
50
+ @conversion_table ||= Measured::ConversionTable.new(@units).to_h
51
+ end
52
+
53
+ private
54
+
55
+ def add_new_unit(unit_name, aliases:, value: nil, base: false)
56
+ if base && @base_unit
57
+ raise Measured::UnitError, "Can only have one base unit. Adding #{ unit_name } but already defined #{ @base_unit }."
58
+ elsif !base && !@base_unit
59
+ raise Measured::UnitError, "A base unit has not yet been set."
60
+ end
61
+
62
+ check_for_duplicate_unit_names([unit_name] + aliases)
63
+
64
+ unit = Measured::Unit.new(unit_name, aliases: aliases, value: value)
65
+ @units << unit
66
+ @base_unit = unit if base
67
+
68
+ clear_conversion_table
69
+
70
+ unit
71
+ end
72
+
73
+ def check_for_duplicate_unit_names(names)
74
+ names.each do |name|
75
+ raise Measured::UnitError, "Unit #{ name } has already been added." if unit_or_alias?(name)
76
+ end
77
+ end
78
+
79
+ def unit_for(name)
80
+ @units.each do |unit|
81
+ return unit if unit.names.include?(name.to_s)
82
+ end
83
+
84
+ raise Measured::UnitError, "Cannot find unit for #{ name }."
85
+ end
86
+
87
+ def clear_conversion_table
88
+ @conversion_table = nil
89
+ end
90
+
91
+ end
@@ -0,0 +1,67 @@
1
+ class Measured::ConversionTable
2
+
3
+ def initialize(units)
4
+ @units = units
5
+ end
6
+
7
+ def to_h
8
+ table = {}
9
+
10
+ @units.map{|u| u.name}.each do |to_unit|
11
+ to_table = {to_unit => BigDecimal("1")}
12
+
13
+ table.each do |from_unit, from_table|
14
+ to_table[from_unit] = find_conversion(to: from_unit, from: to_unit)
15
+ from_table[to_unit] = find_conversion(to: to_unit, from: from_unit)
16
+ end
17
+
18
+ table[to_unit] = to_table
19
+ end
20
+
21
+ table
22
+ end
23
+
24
+ private
25
+
26
+ def find_conversion(to:, from:)
27
+ conversion = find_direct_conversion(to: to, from: from) || find_tree_traversal_conversion(to: to, from: from)
28
+
29
+ raise Measured::UnitError, "Cannot find conversion path from #{ from } to #{ to }." unless conversion
30
+
31
+ conversion
32
+ end
33
+
34
+ def find_direct_conversion(to:, from:)
35
+ @units.each do |unit|
36
+ return unit.conversion_amount if unit.name == from && unit.conversion_unit == to
37
+ end
38
+
39
+ @units.each do |unit|
40
+ return unit.inverse_conversion_amount if unit.name == to && unit.conversion_unit == from
41
+ end
42
+
43
+ nil
44
+ end
45
+
46
+ def find_tree_traversal_conversion(to:, from:)
47
+ traverse(from: from, to: to, unit_names: @units.map{|u| u.name }, amount: BigDecimal("1"))
48
+ end
49
+
50
+ def traverse(from:, to:, unit_names:, amount:)
51
+ unit_names = unit_names - [from]
52
+
53
+ unit_names.each do |name|
54
+ if conversion = find_direct_conversion(from: from, to: name)
55
+ if name == to
56
+ return amount * conversion
57
+ else
58
+ result = traverse(from: name, to: to, unit_names: unit_names, amount: amount * conversion)
59
+ return result if result
60
+ end
61
+ end
62
+ end
63
+
64
+ nil
65
+ end
66
+
67
+ end