unitwise-193 1.0.4

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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +21 -0
  5. data/CHANGELOG.md +44 -0
  6. data/Gemfile +10 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +297 -0
  9. data/Rakefile +21 -0
  10. data/data/base_unit.yaml +43 -0
  11. data/data/derived_unit.yaml +3542 -0
  12. data/data/prefix.yaml +121 -0
  13. data/lib/unitwise.rb +70 -0
  14. data/lib/unitwise/atom.rb +121 -0
  15. data/lib/unitwise/base.rb +58 -0
  16. data/lib/unitwise/compatible.rb +60 -0
  17. data/lib/unitwise/errors.rb +7 -0
  18. data/lib/unitwise/expression.rb +35 -0
  19. data/lib/unitwise/expression/composer.rb +41 -0
  20. data/lib/unitwise/expression/decomposer.rb +68 -0
  21. data/lib/unitwise/expression/matcher.rb +47 -0
  22. data/lib/unitwise/expression/parser.rb +58 -0
  23. data/lib/unitwise/expression/transformer.rb +37 -0
  24. data/lib/unitwise/ext.rb +2 -0
  25. data/lib/unitwise/ext/numeric.rb +45 -0
  26. data/lib/unitwise/functional.rb +117 -0
  27. data/lib/unitwise/measurement.rb +198 -0
  28. data/lib/unitwise/prefix.rb +24 -0
  29. data/lib/unitwise/scale.rb +139 -0
  30. data/lib/unitwise/search.rb +46 -0
  31. data/lib/unitwise/standard.rb +29 -0
  32. data/lib/unitwise/standard/base.rb +73 -0
  33. data/lib/unitwise/standard/base_unit.rb +21 -0
  34. data/lib/unitwise/standard/derived_unit.rb +49 -0
  35. data/lib/unitwise/standard/extras.rb +17 -0
  36. data/lib/unitwise/standard/function.rb +35 -0
  37. data/lib/unitwise/standard/prefix.rb +17 -0
  38. data/lib/unitwise/standard/scale.rb +25 -0
  39. data/lib/unitwise/term.rb +142 -0
  40. data/lib/unitwise/unit.rb +181 -0
  41. data/lib/unitwise/version.rb +3 -0
  42. data/test/support/scale_tests.rb +117 -0
  43. data/test/test_helper.rb +19 -0
  44. data/test/unitwise/atom_test.rb +129 -0
  45. data/test/unitwise/base_test.rb +6 -0
  46. data/test/unitwise/expression/decomposer_test.rb +45 -0
  47. data/test/unitwise/expression/matcher_test.rb +42 -0
  48. data/test/unitwise/expression/parser_test.rb +109 -0
  49. data/test/unitwise/ext/numeric_test.rb +54 -0
  50. data/test/unitwise/functional_test.rb +17 -0
  51. data/test/unitwise/measurement_test.rb +233 -0
  52. data/test/unitwise/prefix_test.rb +25 -0
  53. data/test/unitwise/scale_test.rb +7 -0
  54. data/test/unitwise/search_test.rb +18 -0
  55. data/test/unitwise/term_test.rb +55 -0
  56. data/test/unitwise/unit_test.rb +87 -0
  57. data/test/unitwise_test.rb +35 -0
  58. data/unitwise.gemspec +33 -0
  59. metadata +246 -0
@@ -0,0 +1,25 @@
1
+ module Unitwise::Standard
2
+ class Scale
3
+ attr_accessor :nori
4
+
5
+ def initialize(nori)
6
+ @nori = nori
7
+ end
8
+
9
+ def value
10
+ nori.attributes["value"].to_f
11
+ end
12
+
13
+ def primary_unit_code
14
+ nori.attributes["Unit"]
15
+ end
16
+
17
+ def secondary_unit_code
18
+ nori.attributes["UNIT"]
19
+ end
20
+
21
+ def to_hash
22
+ {:value => value, :unit_code => primary_unit_code}
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,142 @@
1
+ module Unitwise
2
+ # A Term is the combination of an atom, prefix, factor and annotation.
3
+ # Not all properties have to be present. Examples: 'g', 'mm', 'mi2', '4[pi]',
4
+ # 'kJ{Electric Potential}'
5
+ class Term < Liner.new(:atom, :prefix, :factor, :exponent, :annotation)
6
+ include Compatible
7
+
8
+ # Set the atom.
9
+ # @param value [String, Atom] Either a string representing an Atom, or an
10
+ # Atom
11
+ # @api public
12
+ def atom=(value)
13
+ value.is_a?(Atom) ? super(value) : super(Atom.find(value.to_s))
14
+ end
15
+
16
+ # Set the prefix.
17
+ # @param value [String, Prefix] Either a string representing a Prefix, or
18
+ # a Prefix
19
+ def prefix=(value)
20
+ value.is_a?(Prefix) ? super(value) : super(Prefix.find(value.to_s))
21
+ end
22
+
23
+ # Is this term special?
24
+ # @return [true, false]
25
+ def special?
26
+ atom.special? rescue false
27
+ end
28
+
29
+ # Determine how far away a unit is from a base unit.
30
+ # @return [Integer]
31
+ # @api public
32
+ def depth
33
+ atom ? atom.depth + 1 : 0
34
+ end
35
+ memoize :depth
36
+
37
+ # Determine if this is the last term in the scale chain
38
+ # @return [true, false]
39
+ # @api public
40
+ def terminal?
41
+ depth <= 3
42
+ end
43
+
44
+ # The multiplication factor for this term. The default value is 1.
45
+ # @return [Numeric]
46
+ # @api public
47
+ def factor
48
+ super || 1
49
+ end
50
+
51
+ # The exponent for this term. The default value is 1.
52
+ # @return [Numeric]
53
+ # @api public
54
+ def exponent
55
+ super || 1
56
+ end
57
+
58
+ # The unitless scalar value for this term.
59
+ # @param magnitude [Numeric] The magnitude to calculate the scalar for.
60
+ # @return [Numeric] The unitless linear scalar value.
61
+ # @api public
62
+ def scalar(magnitude = 1.0)
63
+ calculate(atom ? atom.scalar(magnitude) : magnitude)
64
+ end
65
+
66
+ # Calculate the magnitude for this term
67
+ # @param scalar [Numeric] The scalar for which you want the magnitude
68
+ # @return [Numeric] The magnitude on this scale.
69
+ # @api public
70
+ def magnitude(scalar = scalar())
71
+ calculate(atom ? atom.magnitude(scalar) : 1.0)
72
+ end
73
+
74
+ # The base units this term is derived from
75
+ # @return [Array] An array of Unitwise::Term
76
+ # @api public
77
+ def root_terms
78
+ if terminal?
79
+ [self]
80
+ else
81
+ atom.scale.root_terms.map do |t|
82
+ self.class.new(:atom => t.atom, :exponent => t.exponent * exponent)
83
+ end
84
+ end
85
+ end
86
+ memoize :root_terms
87
+
88
+ # Term multiplication. Multiply by a Unit, another Term, or a Numeric.
89
+ # params other [Unit, Term, Numeric]
90
+ # @return [Term]
91
+ def *(other)
92
+ operate('*', other) ||
93
+ fail(TypeError, "Can't multiply #{ self } by #{ other }.")
94
+ end
95
+
96
+ # Term division. Divide by a Unit, another Term, or a Numeric.
97
+ # params other [Unit, Term, Numeric]
98
+ # @return [Term]
99
+ def /(other)
100
+ operate('/', other) ||
101
+ fail(TypeError, "Can't divide #{ self } by #{ other }.")
102
+ end
103
+
104
+
105
+ # Term exponentiation. Raise a term to a numeric power.
106
+ # params other [Numeric]
107
+ # @return [Term]
108
+ def **(other)
109
+ if other.is_a?(Numeric)
110
+ self.class.new(to_hash.merge(:exponent => exponent * other))
111
+ else
112
+ fail TypeError, "Can't raise #{self} to #{other}."
113
+ end
114
+ end
115
+
116
+ def to_s(mode = :primary_code)
117
+ [(factor if factor != 1), (prefix.send(mode) if prefix),
118
+ (atom.send(mode) if atom), (exponent if exponent != 1)].compact.join('')
119
+ end
120
+
121
+ private
122
+
123
+ # @api private
124
+ def calculate(value)
125
+ (factor * (prefix ? prefix.scalar : 1) * value) ** exponent
126
+ end
127
+
128
+ # Multiply or divide a term
129
+ # @api private
130
+ def operate(operator, other)
131
+ exp = operator == '/' ? -1 : 1
132
+ if other.respond_to?(:terms)
133
+ Unit.new(other.terms.map { |t| t ** exp } << self)
134
+ elsif other.respond_to?(:atom)
135
+ Unit.new([self, other ** exp])
136
+ elsif other.is_a?(Numeric)
137
+ self.class.new(to_hash.merge(:factor => factor.send(operator, other)))
138
+ end
139
+ end
140
+
141
+ end
142
+ end
@@ -0,0 +1,181 @@
1
+ module Unitwise
2
+ # A Unit is essentially a collection of Unitwise::Term. Terms can be combined
3
+ # through multiplication or division to create a unit. A unit does not have
4
+ # a magnitude, but it does have a scale.
5
+ class Unit
6
+ liner :expression, :terms
7
+ include Compatible
8
+
9
+ # Create a new unit. You can send an expression or a collection of terms
10
+ # @param input [String, Unit, [Term]] A string expression, a unit, or a
11
+ # collection of tems.
12
+ # @api public
13
+ def initialize(input)
14
+ case input
15
+ when Compatible
16
+ @expression = input.expression
17
+ when String, Symbol
18
+ @expression = input.to_s
19
+ else
20
+ @terms = input
21
+ end
22
+ end
23
+
24
+ # The collection of terms used by this unit.
25
+ # @return [Array]
26
+ # @api public
27
+ def terms
28
+ unless frozen?
29
+ unless @terms
30
+ decomposer = Expression.decompose(@expression)
31
+ @mode = decomposer.mode
32
+ @terms = decomposer.terms
33
+ end
34
+ freeze
35
+ end
36
+ @terms
37
+ end
38
+
39
+ # Build a string representation of this unit by it's terms.
40
+ # @param mode [Symbol] The mode to use to stringify the atoms
41
+ # (:primary_code, :names, :secondary_code).
42
+ # @return [String]
43
+ # @api public
44
+ def expression(mode=nil)
45
+ if @expression && (mode.nil? || mode == self.mode)
46
+ @expression
47
+ else
48
+ Expression.compose(terms, mode || self.mode)
49
+ end
50
+ end
51
+
52
+ # The collection of atoms that compose this unit. Essentially delegated to
53
+ # terms.
54
+ # @return [Array]
55
+ # @api public
56
+ def atoms
57
+ terms.map(&:atom)
58
+ end
59
+ memoize :atoms
60
+
61
+ # Is this unit special (meaning on a non-linear scale)?
62
+ # @return [true, false]
63
+ # @api public
64
+ def special?
65
+ terms.count == 1 && terms.all?(&:special?)
66
+ end
67
+ memoize :special?
68
+
69
+ # A number representing this unit's distance from it's deepest terminal atom.
70
+ # @return [Integer]
71
+ # @api public
72
+ def depth
73
+ terms.map(&:depth).max + 1
74
+ end
75
+ memoize :depth
76
+
77
+ # A collection of the deepest terms, or essential composition of the unit.
78
+ # @return [Array]
79
+ # @api public
80
+ def root_terms
81
+ terms.map(&:root_terms).flatten
82
+ end
83
+ memoize :root_terms
84
+
85
+ # Get a scalar value for this unit.
86
+ # @param magnitude [Numeric] An optional magnitude on this unit's scale.
87
+ # @return [Numeric] A scalar value on a linear scale
88
+ # @api public
89
+ def scalar(magnitude = 1.0)
90
+ terms.reduce(1.0) do |prod, term|
91
+ prod * term.scalar(magnitude)
92
+ end
93
+ end
94
+
95
+ # Get a magnitude for this unit based on a linear scale value.
96
+ # Should only be used by units with special atoms in it's hierarchy.
97
+ # @param scalar [Numeric] A linear scalar value
98
+ # @return [Numeric] The equivalent magnitude on this scale
99
+ # @api public
100
+ def magnitude(scalar = scalar())
101
+ terms.reduce(1.0) do |prod, term|
102
+ prod * term.magnitude(scalar)
103
+ end
104
+ end
105
+
106
+ # Multiply this unit by another unit, term, or number.
107
+ # @param other [Unitwise::Unit, Unitwise::Term, Numeric]
108
+ # @return [Unitwise::Unit]
109
+ # @api public
110
+ def *(other)
111
+ operate('*', other) ||
112
+ fail(TypeError, "Can't multiply #{ self } by #{ other }.")
113
+ end
114
+
115
+ # Divide this unit by another unit,term, or number.
116
+ # @param other [Unitwise::Unit, Unitwise::Term, Numeric]
117
+ # @return [Unitwise::Unit]
118
+ # @api public
119
+ def /(other)
120
+ operate('/', other) ||
121
+ fail(TypeError, "Can't divide #{ self } by #{ other }.")
122
+ end
123
+
124
+
125
+ # Raise this unit to a numeric power.
126
+ # @param other [Numeric]
127
+ # @return [Unitwise::Unit]
128
+ # @api public
129
+ def **(other)
130
+ if other.is_a?(Numeric)
131
+ self.class.new(terms.map { |t| t ** other })
132
+ else
133
+ fail TypeError, "Can't raise #{self} to #{other}."
134
+ end
135
+ end
136
+
137
+ # A string representation of this unit.
138
+ # @param mode [:symbol] The mode used to represent the unit
139
+ # (:primary_code, :names, :secondary_code)
140
+ # @return [String]
141
+ # @api public
142
+ def to_s(mode = nil)
143
+ expression(mode)
144
+ end
145
+
146
+ # A collection of the possible string representations of this unit.
147
+ # Primarily used by Unitwise::Search.
148
+ # @return [Array]
149
+ # @api public
150
+ def aliases
151
+ [:names, :primary_code, :secondary_code, :symbol].map do |mode|
152
+ to_s(mode)
153
+ end.uniq
154
+ end
155
+ memoize :aliases
156
+
157
+ # The default mode to use for inspecting and printing.
158
+ # @return [Symbol]
159
+ # @api semipublic
160
+ def mode
161
+ terms
162
+ @mode || :primary_code
163
+ end
164
+
165
+ private
166
+
167
+ # Multiply or divide units
168
+ # @api private
169
+ def operate(operator, other)
170
+ exp = operator == '/' ? -1 : 1
171
+ if other.respond_to?(:terms)
172
+ self.class.new(terms + other.terms.map { |t| t ** exp })
173
+ elsif other.respond_to?(:atom)
174
+ self.class.new(terms << other ** exp)
175
+ elsif other.is_a?(Numeric)
176
+ self.class.new(terms.map { |t| t.send(operator, other) })
177
+ end
178
+ end
179
+
180
+ end
181
+ end
@@ -0,0 +1,3 @@
1
+ module Unitwise
2
+ VERSION = '1.0.4'
3
+ end
@@ -0,0 +1,117 @@
1
+ # Shared examples for Unitwise::Scale and Unitwise::Measurement
2
+ module ScaleTests
3
+ def self.included(base)
4
+ base.class_eval do
5
+ subject { described_class.new(4, "J") }
6
+
7
+ let(:mph) { described_class.new(60, '[mi_i]/h') }
8
+ let(:kmh) { described_class.new(100, 'km/h') }
9
+ let(:mile) { described_class.new(3, '[mi_i]') }
10
+ let(:hpm) { described_class.new(6, 'h/[mi_i]') }
11
+ let(:cui) { described_class.new(12, "[in_i]3") }
12
+ let(:cel) { described_class.new(22, 'Cel') }
13
+ let(:k) { described_class.new(373.15, 'K') }
14
+ let(:f) { described_class.new(98.6, '[degF]')}
15
+ let(:r) { described_class.new(491.67, '[degR]') }
16
+
17
+ describe "#new" do
18
+ it "must set attributes" do
19
+ subject.value.must_equal(4)
20
+ subject.unit.to_s.must_equal('J')
21
+ end
22
+ end
23
+
24
+ describe "#unit" do
25
+ it "must be a unit" do
26
+ subject.must_respond_to(:unit)
27
+ subject.unit.must_be_instance_of(Unitwise::Unit)
28
+ end
29
+ end
30
+
31
+ describe "#root_terms" do
32
+ it "must be a collection of terms" do
33
+ subject.must_respond_to(:root_terms)
34
+ subject.root_terms.must_be_kind_of Enumerable
35
+ subject.root_terms.first.must_be_instance_of(Unitwise::Term)
36
+ end
37
+ end
38
+
39
+ describe "#terms" do
40
+ it "must return an array of terms" do
41
+ subject.terms.must_be_kind_of(Enumerable)
42
+ subject.terms.first.must_be_kind_of(Unitwise::Term)
43
+ end
44
+ end
45
+
46
+ describe "#atoms" do
47
+ it "must return an array of atoms" do
48
+ subject.atoms.must_be_kind_of(Enumerable)
49
+ subject.atoms.first.must_be_kind_of(Unitwise::Atom)
50
+ end
51
+ end
52
+
53
+ describe "#scalar" do
54
+ it "must return value relative to terminal atoms" do
55
+ subject.scalar.must_equal 4000
56
+ mph.scalar.must_almost_equal 26.8224
57
+ cel.scalar.must_equal 295.15
58
+ end
59
+ end
60
+
61
+ describe "#magnitude" do
62
+ it "must return the magnitude" do
63
+ mph.magnitude.must_equal(60)
64
+ cel.magnitude.must_equal(22)
65
+ end
66
+ end
67
+
68
+ describe "#special?" do
69
+ it "must return true when unit is special, false otherwise" do
70
+ subject.special?.must_equal false
71
+ cel.special?.must_equal true
72
+ end
73
+ end
74
+
75
+ describe "#depth" do
76
+ it "must return a number indicating how far down the rabbit hole goes" do
77
+ subject.depth.must_equal 11
78
+ k.depth.must_equal 3
79
+ end
80
+ end
81
+
82
+ describe "#frozen?" do
83
+ it "must be frozen" do
84
+ subject.frozen?.must_equal true
85
+ end
86
+ end
87
+
88
+ describe "#simplified_value" do
89
+ describe "when the value is equivalent to an Integer" do
90
+ it "should convert from a Rational" do
91
+ result = described_class.new(Rational(20,2), 'foot').simplified_value
92
+ result.must_equal 10
93
+ result.must_be_kind_of(Integer)
94
+ end
95
+ it "should convert from a Float" do
96
+ result = described_class.new(4.0, 'foot').simplified_value
97
+ result.must_equal 4
98
+ result.must_be_kind_of(Integer)
99
+ end
100
+ it "should convert from a BigDecimal" do
101
+ result = described_class.new(BigDecimal("4.5"), "volt").simplified_value
102
+ result.must_equal 4.5
103
+ result.must_be_kind_of(Float)
104
+ end
105
+ end
106
+ describe "when the value is equivalent to a Float" do
107
+ it "should convert from a BigDecimal" do
108
+ result = described_class.new(BigDecimal("1.5"), 'foot').simplified_value
109
+ result.must_equal 1.5
110
+ result.must_be_kind_of(Float)
111
+ end
112
+ end
113
+ end
114
+
115
+ end
116
+ end
117
+ end