measured 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,67 @@
1
+ class Measured::Measurable
2
+ include Comparable
3
+ include Measured::Arithmetic
4
+
5
+ attr_reader :unit, :value
6
+
7
+ def initialize(value, unit)
8
+ raise Measured::UnitError, "Unit #{ unit } does not exits." unless self.class.conversion.unit_or_alias?(unit)
9
+
10
+ @value = value
11
+ @value = BigDecimal(@value) unless @value.is_a?(BigDecimal)
12
+
13
+ @unit = self.class.conversion.to_unit_name(unit)
14
+ end
15
+
16
+ def convert_to(new_unit)
17
+ new_unit_name = self.class.conversion.to_unit_name(new_unit)
18
+ value = self.class.conversion.convert(@value, from: @unit, to: new_unit_name)
19
+
20
+ self.class.new(value, new_unit)
21
+ end
22
+
23
+ def convert_to!(new_unit)
24
+ converted = convert_to(new_unit)
25
+
26
+ @value = converted.value
27
+ @unit = converted.unit
28
+
29
+ self
30
+ end
31
+
32
+ def to_s
33
+ [value.to_f.to_s.gsub(/\.0\Z/, ""), unit].join(" ")
34
+ end
35
+
36
+ def inspect
37
+ "#<#{ self.class }: #{ value } #{ unit }>"
38
+ end
39
+
40
+ def <=>(other)
41
+ if other.is_a?(self.class) && unit == other.unit
42
+ value <=> other.value
43
+ end
44
+ end
45
+
46
+ def ==(other)
47
+ !!(other.is_a?(self.class) && unit == other.unit && value == other.value)
48
+ end
49
+
50
+ alias_method :eql?, :==
51
+
52
+ class << self
53
+
54
+ def conversion
55
+ @conversion ||= Measured::Conversion.new
56
+ end
57
+
58
+ def units
59
+ conversion.unit_names
60
+ end
61
+
62
+ def units_with_aliases
63
+ conversion.unit_names_with_aliases
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,59 @@
1
+ class Measured::Unit
2
+ include Comparable
3
+
4
+ def initialize(name, aliases: [], value: nil)
5
+ @name = name.to_s
6
+ @names = ([@name] + aliases.map{|n| n.to_s }).sort
7
+
8
+ @conversion_amount, @conversion_unit = parse_value(value) if value
9
+ end
10
+
11
+ attr_reader :name, :names, :conversion_amount, :conversion_unit
12
+
13
+ def to_s
14
+ if conversion_string
15
+ "#{ @name } (#{ conversion_string })"
16
+ else
17
+ @name
18
+ end
19
+ end
20
+
21
+ def inspect
22
+ "#<Measured::Unit: #{ @name } (#{ @names.join(", ") }) #{ conversion_string }>"
23
+ end
24
+
25
+ def <=>(other)
26
+ if self.class == other.class
27
+ if other.names != @names
28
+ other.names <=> @names
29
+ else
30
+ other.conversion_amount <=> @conversion_amount
31
+ end
32
+ else
33
+ @name <=> other
34
+ end
35
+ end
36
+
37
+ def inverse_conversion_amount
38
+ if conversion_amount.is_a?(Rational)
39
+ Rational(conversion_amount.denominator, conversion_amount.numerator)
40
+ else
41
+ BigDecimal(1) / conversion_amount
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def conversion_string
48
+ "#{ conversion_amount } #{ conversion_unit }" if @conversion_amount || @conversion_unit
49
+ end
50
+
51
+ def parse_value(tokens)
52
+ tokens = tokens.split(" ") if tokens.is_a?(String)
53
+ raise Measured::UnitError, "Cannot parse 'number unit' or [number, unit] formatted tokens from #{ tokens }." unless tokens.size == 2
54
+
55
+ tokens[0] = BigDecimal(tokens[0]) unless tokens[0].is_a?(BigDecimal) || tokens[0].is_a?(Rational)
56
+
57
+ tokens
58
+ end
59
+ end
@@ -0,0 +1,26 @@
1
+ class Measured::Length < Measured::Measurable
2
+
3
+ conversion.set_base :m,
4
+ aliases: [:meter, :metre, :meters, :metres]
5
+
6
+ conversion.add :cm,
7
+ aliases: [:centimeter, :centimetre, :centimeters, :centimetres],
8
+ value: "0.01 m"
9
+
10
+ conversion.add :mm,
11
+ aliases: [:millimeter, :millimetre, :millimeters, :millimetres],
12
+ value: "0.001 m"
13
+
14
+ conversion.add :in,
15
+ aliases: [:inch, :inches],
16
+ value: "0.0254 m"
17
+
18
+ conversion.add :ft,
19
+ aliases: [:foot, :feet],
20
+ value: "0.3048 m"
21
+
22
+ conversion.add :yd,
23
+ aliases: [:yard, :yards],
24
+ value: "0.9144 m"
25
+
26
+ end
@@ -0,0 +1,18 @@
1
+ class Measured::Weight < Measured::Measurable
2
+
3
+ conversion.set_base :g,
4
+ aliases: [:gram, :grams]
5
+
6
+ conversion.add :kg,
7
+ aliases: [:kilogram, :kilograms],
8
+ value: "1000 g"
9
+
10
+ conversion.add :lb,
11
+ aliases: [:lbs, :pound, :pounds],
12
+ value: [Rational(45359237,1e8), "kg"]
13
+
14
+ conversion.add :oz,
15
+ aliases: [:ounce, :ounces],
16
+ value: [Rational(1,16), "lb"]
17
+
18
+ end
@@ -0,0 +1,3 @@
1
+ module Measured
2
+ VERSION = "0.0.1"
3
+ end
data/lib/measured.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "measured/base"
2
+
3
+ require "measured/units/length"
4
+ require "measured/units/weight"
data/measured.gemspec ADDED
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'measured/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "measured"
8
+ spec.version = Measured::VERSION
9
+ spec.authors = ["Kevin McPhillips"]
10
+ spec.email = ["github@kevinmcphillips.ca"]
11
+ spec.summary = %q{Encapsulate measurements with their units in Ruby}
12
+ spec.description = %q{Wrapper objects which encapsulate measurments and their associated units in Ruby.}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_runtime_dependency "activesupport", ">= 4.0"
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.7"
24
+ spec.add_development_dependency "rake", "~> 10.0"
25
+ spec.add_development_dependency "minitest", "~> 5.5.1"
26
+ spec.add_development_dependency "mocha", "~> 1.1.0"
27
+ spec.add_development_dependency "pry"
28
+ end
@@ -0,0 +1,133 @@
1
+ require "test_helper"
2
+
3
+ class Measured::ArithmeticTest < ActiveSupport::TestCase
4
+ setup do
5
+ @two = Magic.new(2, :magic_missile)
6
+ @three = Magic.new(3, :magic_missile)
7
+ @four = Magic.new(4, :magic_missile)
8
+ end
9
+
10
+ test "#+ should add together same units" do
11
+ assert_equal Magic.new(5, :magic_missile), @two + @three
12
+ assert_equal Magic.new(5, :magic_missile), @three + @two
13
+ end
14
+
15
+ test "#+ should add a number to the value" do
16
+ assert_equal Magic.new(5, :magic_missile), @two + 3
17
+ assert_equal Magic.new(5, :magic_missile), 2 + @three
18
+ end
19
+
20
+ test "#+ should raise if different unit system" do
21
+ assert_raises TypeError do
22
+ OtherFakeSystem.new(1, :other_fake_base) + @two
23
+ end
24
+
25
+ assert_raises TypeError do
26
+ @two + OtherFakeSystem.new(1, :other_fake_base)
27
+ end
28
+ end
29
+
30
+ test "#+ should raise if adding something nonsense" do
31
+ assert_raises TypeError do
32
+ @two + "thing"
33
+ end
34
+
35
+ assert_raises TypeError do
36
+ "thing" + @two
37
+ end
38
+ end
39
+
40
+ test "#- should subtract same units" do
41
+ assert_equal Magic.new(-1, :magic_missile), @two - @three
42
+ assert_equal Magic.new(1, :magic_missile), @three - @two
43
+ end
44
+
45
+ test "#- should subtract a number from the value" do
46
+ assert_equal Magic.new(-1, :magic_missile), @two - 3
47
+ assert_equal Magic.new(1, :magic_missile), 2 - @three
48
+ end
49
+
50
+ test "#- should raise if different unit system" do
51
+ assert_raises TypeError do
52
+ OtherFakeSystem.new(1, :other_fake_base) - @two
53
+ end
54
+
55
+ assert_raises TypeError do
56
+ @two - OtherFakeSystem.new(1, :other_fake_base)
57
+ end
58
+ end
59
+
60
+ test "#- should raise if subtracting something nonsense" do
61
+ assert_raises TypeError do
62
+ @two - "thing"
63
+ end
64
+
65
+ assert_raises NoMethodError do
66
+ "thing" - @two
67
+ end
68
+ end
69
+
70
+ test "#* should multiply together same units" do
71
+ assert_equal Magic.new(6, :magic_missile), @two * @three
72
+ assert_equal Magic.new(6, :magic_missile), @three * @two
73
+ end
74
+
75
+ test "#* should multiply a number to the value" do
76
+ assert_equal Magic.new(6, :magic_missile), @two * 3
77
+ assert_equal Magic.new(6, :magic_missile), 2 * @three
78
+ end
79
+
80
+ test "#* should raise if different unit system" do
81
+ assert_raises TypeError do
82
+ OtherFakeSystem.new(1, :other_fake_base) * @two
83
+ end
84
+
85
+ assert_raises TypeError do
86
+ @two * OtherFakeSystem.new(1, :other_fake_base)
87
+ end
88
+ end
89
+
90
+ test "#* should raise if multiplying something nonsense" do
91
+ assert_raises TypeError do
92
+ @two * "thing"
93
+ end
94
+
95
+ assert_raises TypeError do
96
+ "thing" * @two
97
+ end
98
+ end
99
+
100
+ test "#/ should divide together same units" do
101
+ assert_equal Magic.new("0.5", :magic_missile), @two / @four
102
+ assert_equal Magic.new(2, :magic_missile), @four / @two
103
+ end
104
+
105
+ test "#/ should divide a number to the value" do
106
+ assert_equal Magic.new("0.5", :magic_missile), @two / 4
107
+ assert_equal Magic.new(2, :magic_missile), 2 / @four
108
+ end
109
+
110
+ test "#/ should raise if different unit system" do
111
+ assert_raises TypeError do
112
+ OtherFakeSystem.new(1, :other_fake_base) / @two
113
+ end
114
+
115
+ assert_raises TypeError do
116
+ @two / OtherFakeSystem.new(1, :other_fake_base)
117
+ end
118
+ end
119
+
120
+ test "#/ should raise if dividing something nonsense" do
121
+ assert_raises TypeError do
122
+ @two / "thing"
123
+ end
124
+
125
+ assert_raises NoMethodError do
126
+ "thing" / @two
127
+ end
128
+ end
129
+
130
+ test "#-@ returns the negative version" do
131
+ assert_equal Magic.new(-2, :magic_missile), -@two
132
+ end
133
+ end
@@ -0,0 +1,19 @@
1
+ require "test_helper"
2
+
3
+ class Measured::ConversionTableTest < ActiveSupport::TestCase
4
+ setup do
5
+ @unit = Measured::Unit.new(:test)
6
+ end
7
+
8
+ test "#initialize accepts a list of units and a base unit" do
9
+ Measured::ConversionTable.new([@unit])
10
+ end
11
+
12
+ test "#to_h should return a hash for the simple case" do
13
+ expected = {
14
+ "test" => {"test" => BigDecimal("1")}
15
+ }
16
+
17
+ assert_equal expected, Measured::ConversionTable.new([@unit]).to_h
18
+ end
19
+ end
@@ -0,0 +1,206 @@
1
+ require "test_helper"
2
+
3
+ class Measured::ConversionTest < ActiveSupport::TestCase
4
+ setup do
5
+ @conversion = Measured::Conversion.new
6
+ end
7
+
8
+ test "#base sets the base unit" do
9
+ @conversion.set_base :m, aliases: [:metre]
10
+ assert_equal ["m", "metre"], @conversion.base_unit.names
11
+ end
12
+
13
+ test "#base doesn't allow a second base to be added" do
14
+ @conversion.set_base :m, aliases: [:metre]
15
+
16
+ assert_raises Measured::UnitError do
17
+ @conversion.set_base :in
18
+ end
19
+ end
20
+
21
+ test "#add adds a new unit" do
22
+ @conversion.set_base :m
23
+ @conversion.add :in, aliases: [:inch], value: "0.0254 meter"
24
+
25
+ assert_equal 2, @conversion.units.count
26
+ end
27
+
28
+ test "#add cannot add duplicate unit names" do
29
+ @conversion.set_base :m
30
+ @conversion.add :in, aliases: [:inch], value: "0.0254 meter"
31
+
32
+ assert_raises Measured::UnitError do
33
+ @conversion.add :in, aliases: [:thing], value: "123 m"
34
+ end
35
+
36
+ assert_raises Measured::UnitError do
37
+ @conversion.add :inch, value: "123 m"
38
+ end
39
+ end
40
+
41
+ test "#add does not allow you to add a unit before the base" do
42
+ assert_raises Measured::UnitError do
43
+ @conversion.add :in, aliases: [:inch], value: "0.0254 meter"
44
+ end
45
+ end
46
+
47
+ test "#unit_names_with_aliases lists all allowed unit names" do
48
+ @conversion.set_base :m
49
+ @conversion.add :in, aliases: [:inch], value: "0.0254 meter"
50
+ @conversion.add :ft, aliases: [:feet, :foot], value: "0.3048 meter"
51
+
52
+ assert_equal ["feet", "foot", "ft", "in", "inch", "m"], @conversion.unit_names_with_aliases
53
+ end
54
+
55
+ test "#unit_names lists all base unit names without aliases" do
56
+ @conversion.set_base :m
57
+ @conversion.add :in, aliases: [:inch], value: "0.0254 meter"
58
+ @conversion.add :ft, aliases: [:feet, :foot], value: "0.3048 meter"
59
+
60
+ assert_equal ["ft", "in", "m"], @conversion.unit_names
61
+ end
62
+
63
+ test "#unit? checks if the unit is part of the units and aliases" do
64
+ @conversion.set_base :m
65
+ @conversion.add :inch, aliases: [:in], value: "0.0254 meter"
66
+
67
+ assert @conversion.unit?(:inch)
68
+ assert @conversion.unit?("m")
69
+ refute @conversion.unit?("in")
70
+ refute @conversion.unit?(:yard)
71
+ end
72
+
73
+ test "#unit_or_alias? checks if the unit is part of the units but not aliases" do
74
+ @conversion.set_base :m
75
+ @conversion.add :inch, aliases: [:in], value: "0.0254 meter"
76
+
77
+ assert @conversion.unit_or_alias?(:inch)
78
+ assert @conversion.unit_or_alias?("m")
79
+ assert @conversion.unit_or_alias?("in")
80
+ refute @conversion.unit_or_alias?(:yard)
81
+ end
82
+
83
+ test "#to_unit_name converts a unit name to its base unit" do
84
+ assert_equal "fireball", Magic.conversion.to_unit_name("fire")
85
+ end
86
+
87
+ test "#to_unit_name does not care about string or symbol" do
88
+ assert_equal "fireball", Magic.conversion.to_unit_name(:fire)
89
+ end
90
+
91
+ test "#to_unit_name passes through if already base unit name" do
92
+ assert_equal "fireball", Magic.conversion.to_unit_name("fireball")
93
+ end
94
+
95
+ test "#to_unit_name raises if not found" do
96
+ assert_raises Measured::UnitError do
97
+ Magic.conversion.to_unit_name("thunder")
98
+ end
99
+ end
100
+
101
+ test "#convert raises if either unit is not found" do
102
+ assert_raises Measured::UnitError do
103
+ Magic.conversion.convert(1, from: "fire", to: "doesnt_exist")
104
+ end
105
+
106
+ assert_raises Measured::UnitError do
107
+ Magic.conversion.convert(1, from: "doesnt_exist", to: "fire")
108
+ end
109
+ end
110
+
111
+ test "#convert converts betwen two known units" do
112
+ @conversion.set_base :m
113
+ @conversion.add :cm, value: "0.01 m"
114
+
115
+ assert_equal BigDecimal("10"), @conversion.convert(BigDecimal("1000"), from: "cm", to: "m")
116
+ assert_equal BigDecimal("250"), @conversion.convert(BigDecimal("2.5"), from: "m", to: "cm")
117
+ end
118
+
119
+ test "#convert handles the same unit" do
120
+ @conversion.set_base :m
121
+ @conversion.add :cm, value: "0.01 m"
122
+
123
+ assert_equal BigDecimal("2"), @conversion.convert(BigDecimal("2"), from: "cm", to: "cm")
124
+ end
125
+
126
+ test "#conversion_table returns expected nested hashes with BigDecimal conversion factors in a tiny data set" do
127
+ @conversion.set_base :m
128
+ @conversion.add :cm, value: "0.01 m"
129
+
130
+ expected = {
131
+ "m" => {
132
+ "m" => BigDecimal("1"),
133
+ "cm" => BigDecimal("100")
134
+ },
135
+ "cm" => {
136
+ "cm" => BigDecimal("1"),
137
+ "m" => BigDecimal("0.01")
138
+ }
139
+ }
140
+
141
+ assert_equal expected, @conversion.conversion_table
142
+ end
143
+
144
+ test "#conversion_table returns expected nested hashes with BigDecimal conversion factors" do
145
+ @conversion.set_base :m
146
+ @conversion.add :cm, value: "0.01 m"
147
+ @conversion.add :mm, value: "0.001 m"
148
+
149
+ expected = {
150
+ "m" => {
151
+ "m" => BigDecimal("1"),
152
+ "cm" => BigDecimal("100"),
153
+ "mm" => BigDecimal("1000")
154
+ },
155
+ "cm" => {
156
+ "cm" => BigDecimal("1"),
157
+ "m" => BigDecimal("0.01"),
158
+ "mm" => BigDecimal("10")
159
+ },
160
+ "mm" => {
161
+ "mm" => BigDecimal("1"),
162
+ "m" => BigDecimal("0.001"),
163
+ "cm" => BigDecimal("0.1")
164
+ }
165
+ }
166
+
167
+ assert_equal expected, @conversion.conversion_table
168
+ end
169
+
170
+ test "#conversion_table returns expected nested hashes with BigDecimal conversion factors in an indrect path" do
171
+ @conversion.set_base :mm
172
+ @conversion.add :cm, value: "10 mm"
173
+ @conversion.add :dm, value: "10 cm"
174
+ @conversion.add :m, value: "10 dm"
175
+
176
+ expected = {
177
+ "m" => {
178
+ "m" => BigDecimal("1"),
179
+ "cm" => BigDecimal("100"),
180
+ "dm" => BigDecimal("10"),
181
+ "mm" => BigDecimal("1000")
182
+ },
183
+ "cm" => {
184
+ "cm" => BigDecimal("1"),
185
+ "dm" => BigDecimal("0.1"),
186
+ "m" => BigDecimal("0.01"),
187
+ "mm" => BigDecimal("10")
188
+ },
189
+ "dm" => {
190
+ "dm" => BigDecimal("1"),
191
+ "cm" => BigDecimal("10"),
192
+ "m" => BigDecimal("0.1"),
193
+ "mm" => BigDecimal("100")
194
+ },
195
+ "mm" => {
196
+ "mm" => BigDecimal("1"),
197
+ "m" => BigDecimal("0.001"),
198
+ "dm" => BigDecimal("0.01"),
199
+ "cm" => BigDecimal("0.1")
200
+ }
201
+ }
202
+
203
+ assert_equal expected, @conversion.conversion_table
204
+ end
205
+
206
+ end