measured 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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