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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -0
- data/CHANGELOG.md +44 -0
- data/Gemfile +10 -0
- data/LICENSE.txt +22 -0
- data/README.md +297 -0
- data/Rakefile +21 -0
- data/data/base_unit.yaml +43 -0
- data/data/derived_unit.yaml +3542 -0
- data/data/prefix.yaml +121 -0
- data/lib/unitwise.rb +70 -0
- data/lib/unitwise/atom.rb +121 -0
- data/lib/unitwise/base.rb +58 -0
- data/lib/unitwise/compatible.rb +60 -0
- data/lib/unitwise/errors.rb +7 -0
- data/lib/unitwise/expression.rb +35 -0
- data/lib/unitwise/expression/composer.rb +41 -0
- data/lib/unitwise/expression/decomposer.rb +68 -0
- data/lib/unitwise/expression/matcher.rb +47 -0
- data/lib/unitwise/expression/parser.rb +58 -0
- data/lib/unitwise/expression/transformer.rb +37 -0
- data/lib/unitwise/ext.rb +2 -0
- data/lib/unitwise/ext/numeric.rb +45 -0
- data/lib/unitwise/functional.rb +117 -0
- data/lib/unitwise/measurement.rb +198 -0
- data/lib/unitwise/prefix.rb +24 -0
- data/lib/unitwise/scale.rb +139 -0
- data/lib/unitwise/search.rb +46 -0
- data/lib/unitwise/standard.rb +29 -0
- data/lib/unitwise/standard/base.rb +73 -0
- data/lib/unitwise/standard/base_unit.rb +21 -0
- data/lib/unitwise/standard/derived_unit.rb +49 -0
- data/lib/unitwise/standard/extras.rb +17 -0
- data/lib/unitwise/standard/function.rb +35 -0
- data/lib/unitwise/standard/prefix.rb +17 -0
- data/lib/unitwise/standard/scale.rb +25 -0
- data/lib/unitwise/term.rb +142 -0
- data/lib/unitwise/unit.rb +181 -0
- data/lib/unitwise/version.rb +3 -0
- data/test/support/scale_tests.rb +117 -0
- data/test/test_helper.rb +19 -0
- data/test/unitwise/atom_test.rb +129 -0
- data/test/unitwise/base_test.rb +6 -0
- data/test/unitwise/expression/decomposer_test.rb +45 -0
- data/test/unitwise/expression/matcher_test.rb +42 -0
- data/test/unitwise/expression/parser_test.rb +109 -0
- data/test/unitwise/ext/numeric_test.rb +54 -0
- data/test/unitwise/functional_test.rb +17 -0
- data/test/unitwise/measurement_test.rb +233 -0
- data/test/unitwise/prefix_test.rb +25 -0
- data/test/unitwise/scale_test.rb +7 -0
- data/test/unitwise/search_test.rb +18 -0
- data/test/unitwise/term_test.rb +55 -0
- data/test/unitwise/unit_test.rb +87 -0
- data/test/unitwise_test.rb +35 -0
- data/unitwise.gemspec +33 -0
- 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,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
|