quantify 1.0.5 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/quantify.rb +3 -1
- data/lib/quantify/config.rb +243 -243
- data/lib/quantify/core_extensions/numeric.rb +19 -0
- data/lib/quantify/core_extensions/string.rb +29 -0
- data/lib/quantify/core_extensions/symbol.rb +7 -0
- data/lib/quantify/dimensions.rb +19 -21
- data/lib/quantify/quantify.rb +32 -1
- data/lib/quantify/quantity.rb +37 -37
- data/lib/quantify/unit/base_unit.rb +113 -61
- data/lib/quantify/unit/compound_base_unit.rb +54 -14
- data/lib/quantify/unit/compound_unit.rb +42 -18
- data/lib/quantify/unit/non_si_unit.rb +1 -1
- data/lib/quantify/unit/prefix/base_prefix.rb +5 -5
- data/lib/quantify/unit/prefix/prefix.rb +4 -4
- data/lib/quantify/unit/unit.rb +55 -24
- data/spec/compound_unit_spec.rb +205 -0
- data/spec/dimension_spec.rb +49 -1
- data/spec/quantify_spec.rb +20 -0
- data/spec/quantity_spec.rb +9 -9
- data/spec/string_spec.rb +55 -0
- data/spec/unit_spec.rb +292 -10
- metadata +24 -19
- data/lib/quantify/core_extensions.rb +0 -63
@@ -32,19 +32,19 @@ module Quantify
|
|
32
32
|
|
33
33
|
# Absolute index as names always contain 'per' before denominator units
|
34
34
|
def name
|
35
|
-
@unit.name
|
35
|
+
name_to_power(@unit.name, @index.abs)
|
36
36
|
end
|
37
37
|
|
38
38
|
def pluralized_name
|
39
|
-
@unit.pluralized_name
|
39
|
+
name_to_power(@unit.pluralized_name, @index.abs)
|
40
40
|
end
|
41
41
|
|
42
42
|
def symbol
|
43
|
-
@unit.symbol.to_s + ( @index.nil?
|
43
|
+
@unit.symbol.to_s + ( @index.nil? || @index == 1 ? "" : formatted_index )
|
44
44
|
end
|
45
45
|
|
46
46
|
def label
|
47
|
-
@unit.label + (@index == 1 ? "" :
|
47
|
+
@unit.label + (@index == 1 ? "" : formatted_index)
|
48
48
|
end
|
49
49
|
|
50
50
|
# Reciprocalized version of label, i.e. sign changed. This is used to make
|
@@ -52,7 +52,7 @@ module Quantify
|
|
52
52
|
# i.e. where no '/' appears in the label
|
53
53
|
#
|
54
54
|
def reciprocalized_label
|
55
|
-
@unit.label + (@index == -1 ? "" :
|
55
|
+
@unit.label + (@index == -1 ? "" : formatted_index(@index * -1))
|
56
56
|
end
|
57
57
|
|
58
58
|
def factor
|
@@ -70,22 +70,62 @@ module Quantify
|
|
70
70
|
# The following methods refer only to the unit of the CompoundBaseUnit
|
71
71
|
# object, rather than the unit *together with its index*
|
72
72
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
73
|
+
def is_base_quantity_si_unit?
|
74
|
+
@unit.is_base_quantity_si_unit?
|
75
|
+
end
|
76
|
+
|
77
|
+
def is_base_unit?
|
78
|
+
@unit.is_base_unit?
|
79
|
+
end
|
80
|
+
|
81
|
+
def is_benchmark_unit?
|
82
|
+
@unit.is_benchmark_unit?
|
83
|
+
end
|
84
|
+
|
85
|
+
def is_si_unit?
|
86
|
+
@unit.is_si_unit?
|
87
|
+
end
|
88
|
+
|
89
|
+
def is_non_si_unit?
|
90
|
+
@unit.is_non_si_unit?
|
91
|
+
end
|
92
|
+
|
93
|
+
def is_prefixed_unit?
|
94
|
+
@unit.is_prefixed_unit?
|
81
95
|
end
|
82
96
|
|
97
|
+
def is_derived_unit?
|
98
|
+
@unit.is_derived_unit?
|
99
|
+
end
|
100
|
+
|
83
101
|
def measures
|
84
102
|
@unit.measures
|
85
103
|
end
|
86
104
|
|
87
105
|
def initialize_copy(source)
|
88
|
-
instance_variable_set("@unit", unit.clone)
|
106
|
+
instance_variable_set("@unit", @unit.clone)
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
# Returns a string representation of the unit index, formatted according to
|
112
|
+
# the global superscript configuration
|
113
|
+
#
|
114
|
+
def formatted_index(index=nil)
|
115
|
+
index = "^#{index.nil? ? @index : index}"
|
116
|
+
Quantify.use_superscript_characters? ? index.with_superscript_characters : index
|
117
|
+
end
|
118
|
+
|
119
|
+
def name_to_power(string,index)
|
120
|
+
name = string.clone
|
121
|
+
case index
|
122
|
+
when 1 then name
|
123
|
+
when 2 then "square #{name}"
|
124
|
+
when 3 then "cubic #{name}"
|
125
|
+
else
|
126
|
+
ordinal = ActiveSupport::Inflector.ordinalize index
|
127
|
+
name << " to the #{ordinal} power"
|
128
|
+
end
|
89
129
|
end
|
90
130
|
end
|
91
131
|
end
|
@@ -41,7 +41,7 @@ module Quantify
|
|
41
41
|
next if new_base.unit.is_dimensionless?
|
42
42
|
|
43
43
|
new_base.index = base_units.select do |other_base|
|
44
|
-
new_base.unit
|
44
|
+
new_base.unit.is_equivalent_to? other_base.unit
|
45
45
|
end.inject(new_base.index) do |index,other_base|
|
46
46
|
base_units.delete other_base
|
47
47
|
index += other_base.index
|
@@ -57,8 +57,8 @@ module Quantify
|
|
57
57
|
#
|
58
58
|
# This is a class method which takes an arbitrary array of base units as an
|
59
59
|
# argument. This means that consolidation can be performed on either all
|
60
|
-
# base units or just a subset - the numerator or denominator units.
|
61
|
-
#
|
60
|
+
# base units or just a subset - e.g. the numerator or denominator units.
|
61
|
+
#
|
62
62
|
# The units to use for particular physical dimension can be specified
|
63
63
|
# following the inital argument. If no unit is specified for a physical
|
64
64
|
# quantity which is represented in the array of base units, then the first
|
@@ -93,8 +93,8 @@ module Quantify
|
|
93
93
|
@base_units << unit
|
94
94
|
elsif unit.is_a? Unit::Base
|
95
95
|
@base_units << CompoundBaseUnit.new(unit)
|
96
|
-
elsif unit.is_a?
|
97
|
-
|
96
|
+
elsif unit.is_a?(Array) && unit.first.is_a?(Unit::Base) &&
|
97
|
+
!unit.first.is_a?(Compound) && unit.size == 2
|
98
98
|
@base_units << CompoundBaseUnit.new(unit.first,unit.last)
|
99
99
|
else
|
100
100
|
raise Exceptions::InvalidArgumentError, "#{unit} does not represent a valid base unit"
|
@@ -165,10 +165,10 @@ module Quantify
|
|
165
165
|
raise Exceptions::InvalidArgumentError, "Cannot cancel by a compound unit" if unit.is_a? Unit::Compound
|
166
166
|
unit = Unit.for unit unless unit.is_a? Unit::Base
|
167
167
|
|
168
|
-
numerator_unit = numerator_units.find { |base| unit
|
169
|
-
denominator_unit = denominator_units.find { |base| unit
|
168
|
+
numerator_unit = numerator_units.find { |base| unit.is_equivalent_to? base.unit }
|
169
|
+
denominator_unit = denominator_units.find { |base| unit.is_equivalent_to? base.unit }
|
170
170
|
|
171
|
-
if numerator_unit
|
171
|
+
if numerator_unit && denominator_unit
|
172
172
|
cancel_value = [numerator_unit.index,denominator_unit.index].min.abs
|
173
173
|
numerator_unit.index -= cancel_value
|
174
174
|
denominator_unit.index += cancel_value
|
@@ -177,6 +177,20 @@ module Quantify
|
|
177
177
|
consolidate_numerator_and_denominator_units!
|
178
178
|
end
|
179
179
|
|
180
|
+
# Make the base units of self use consistent units for each physical quantity
|
181
|
+
# represented. For example, lb/kg => kg/kg.
|
182
|
+
#
|
183
|
+
# By default, units are rationalized within the the numerator and denominator
|
184
|
+
# respectively. That is, different units representing the same physical
|
185
|
+
# quantity may appear across the numerator and denominator, but not within
|
186
|
+
# each. To fully rationalize the base units of self, pass in the symbol
|
187
|
+
# :full as a first argument. Otherwise :partial is passed as the default.
|
188
|
+
#
|
189
|
+
# The units to use for particular physical dimension can be specified
|
190
|
+
# following the inital argument. If no unit is specified for a physical
|
191
|
+
# quantity which is represented in the array of base units, then the first
|
192
|
+
# unit found for that physical quantity is used as the canonical one.
|
193
|
+
#
|
180
194
|
def rationalize_base_units!(scope=:partial,*units)
|
181
195
|
if scope == :full
|
182
196
|
Compound.rationalize_base_units(@base_units,*units)
|
@@ -187,16 +201,26 @@ module Quantify
|
|
187
201
|
consolidate_numerator_and_denominator_units!
|
188
202
|
end
|
189
203
|
|
204
|
+
# Return a known unit which is equivalent to self in terms of its physical
|
205
|
+
# quantity (dimensions), factor and scaling attributes (i.e. representing the
|
206
|
+
# precise same physical unit but perhaps with different identifiers), e.g.
|
207
|
+
#
|
208
|
+
# ((Unit.kg*(Unit.m**"))/(Unit.s**2)).equivalent_known_unit.name
|
209
|
+
#
|
210
|
+
# #=> "joule"
|
211
|
+
#
|
190
212
|
def equivalent_known_unit
|
191
213
|
Unit.units.find do |unit|
|
192
|
-
self
|
193
|
-
not unit.is_compound_unit?
|
214
|
+
self.is_equivalent_to?(unit) && !unit.is_compound_unit?
|
194
215
|
end
|
195
216
|
end
|
196
217
|
|
197
|
-
|
218
|
+
# Returns an equivalent known unit (via #equivalent_known_unit) if it exists.
|
219
|
+
# Otherwise, returns false.
|
220
|
+
#
|
221
|
+
def or_equivalent
|
198
222
|
equivalent_unit = equivalent_known_unit
|
199
|
-
if equivalent_unit
|
223
|
+
if equivalent_unit && equivalent_unit.acts_as_equivalent_unit
|
200
224
|
return equivalent_unit
|
201
225
|
else
|
202
226
|
return self
|
@@ -206,11 +230,11 @@ module Quantify
|
|
206
230
|
protected
|
207
231
|
|
208
232
|
def initialize_attributes
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
233
|
+
self.dimensions = derive_dimensions
|
234
|
+
self.name = derive_name
|
235
|
+
self.symbol = derive_symbol
|
236
|
+
self.factor = derive_factor
|
237
|
+
self.label = derive_label
|
214
238
|
end
|
215
239
|
|
216
240
|
# Partially consolidate base units, i.e. numerator and denomiator are
|
@@ -304,7 +328,7 @@ module Quantify
|
|
304
328
|
format = ( unit_label.empty? ? :label : :reciprocalized_label )
|
305
329
|
unit_label << "/" unless unit_label.empty?
|
306
330
|
denominator_units.inject(unit_label) do |label,base|
|
307
|
-
label << "·" unless unit_label.empty?
|
331
|
+
label << "·" unless unit_label.empty? || unit_label =~ /\/\z/
|
308
332
|
label << base.send(format)
|
309
333
|
end
|
310
334
|
end
|
@@ -10,16 +10,16 @@ module Quantify
|
|
10
10
|
end
|
11
11
|
end
|
12
12
|
|
13
|
-
def self.configure
|
14
|
-
self.class_eval
|
13
|
+
def self.configure(&block)
|
14
|
+
self.class_eval(&block) if block
|
15
15
|
end
|
16
16
|
|
17
17
|
attr_reader :name, :symbol, :factor
|
18
18
|
|
19
19
|
def initialize(options)
|
20
|
-
@symbol = options[:symbol].
|
20
|
+
@symbol = options[:symbol].remove_underscores
|
21
21
|
@factor = options[:factor].to_f
|
22
|
-
@name = options[:name].
|
22
|
+
@name = options[:name].remove_underscores.downcase
|
23
23
|
end
|
24
24
|
|
25
25
|
def is_si_prefix?
|
@@ -27,7 +27,7 @@ module Quantify
|
|
27
27
|
end
|
28
28
|
|
29
29
|
def is_non_si_prefix?
|
30
|
-
self.is_a?
|
30
|
+
self.is_a?(NonSI)
|
31
31
|
end
|
32
32
|
|
33
33
|
def label
|
@@ -14,7 +14,7 @@ module Quantify
|
|
14
14
|
def self.unload(*unloaded_prefixes)
|
15
15
|
[unloaded_prefixes].flatten.each do |unloaded_prefix|
|
16
16
|
unloaded_prefix = Prefix.for(unloaded_prefix)
|
17
|
-
@prefixes.delete_if { |
|
17
|
+
@prefixes.delete_if { |prefix| prefix.label == unloaded_prefix.label }
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
@@ -24,10 +24,10 @@ module Quantify
|
|
24
24
|
|
25
25
|
def self.for(name_or_symbol,collection=nil)
|
26
26
|
return name_or_symbol.clone if name_or_symbol.is_a? Quantify::Unit::Prefix::Base
|
27
|
-
if name_or_symbol.is_a?
|
27
|
+
if name_or_symbol.is_a?(String) || name_or_symbol.is_a?(Symbol)
|
28
28
|
if prefix = (collection.nil? ? @prefixes : collection).find do |prefix|
|
29
|
-
prefix.name == name_or_symbol.
|
30
|
-
prefix.symbol == name_or_symbol.
|
29
|
+
prefix.name == name_or_symbol.remove_underscores.downcase ||
|
30
|
+
prefix.symbol == name_or_symbol.remove_underscores
|
31
31
|
end
|
32
32
|
return prefix.clone
|
33
33
|
else
|
data/lib/quantify/unit/unit.rb
CHANGED
@@ -25,8 +25,8 @@ module Quantify
|
|
25
25
|
attr_reader :units
|
26
26
|
end
|
27
27
|
|
28
|
-
def self.configure
|
29
|
-
self.class_eval
|
28
|
+
def self.configure(&block)
|
29
|
+
self.class_eval(&block) if block
|
30
30
|
end
|
31
31
|
|
32
32
|
# Instance variable containing system of known units
|
@@ -40,8 +40,7 @@ module Quantify
|
|
40
40
|
# Remove a unit from the system of known units
|
41
41
|
def self.unload(*unloaded_units)
|
42
42
|
[unloaded_units].flatten.each do |unloaded_unit|
|
43
|
-
|
44
|
-
@units.delete_if { |unit| unit.label == unloaded_unit.label }
|
43
|
+
@units.delete(Unit.for(unloaded_unit))
|
45
44
|
end
|
46
45
|
end
|
47
46
|
|
@@ -90,10 +89,10 @@ module Quantify
|
|
90
89
|
#
|
91
90
|
def self.for(name_symbol_label_or_object)
|
92
91
|
return name_symbol_label_or_object.clone if name_symbol_label_or_object.is_a? Unit::Base
|
93
|
-
return nil if name_symbol_label_or_object.nil?
|
94
|
-
( name_symbol_label_or_object.is_a?(String)
|
92
|
+
return nil if name_symbol_label_or_object.nil? ||
|
93
|
+
( name_symbol_label_or_object.is_a?(String) && name_symbol_label_or_object.empty? )
|
95
94
|
name_symbol_or_label = name_symbol_label_or_object
|
96
|
-
unless name_symbol_or_label.is_a?
|
95
|
+
unless name_symbol_or_label.is_a?(String) || name_symbol_or_label.is_a?(Symbol)
|
97
96
|
raise Exceptions::InvalidArgumentError, "Argument must be a Symbol or String"
|
98
97
|
end
|
99
98
|
if unit = Unit.match(name_symbol_or_label)
|
@@ -109,7 +108,7 @@ module Quantify
|
|
109
108
|
# Parse complex strings into unit.
|
110
109
|
#
|
111
110
|
def self.parse(string)
|
112
|
-
string = string.
|
111
|
+
string = string.remove_underscores.without_superscript_characters
|
113
112
|
if string.scan(/(\/|per)/).size > 1
|
114
113
|
raise Exceptions::InvalidArgumentError, "Malformed unit: multiple uses of '/' or 'per'"
|
115
114
|
end
|
@@ -118,7 +117,7 @@ module Quantify
|
|
118
117
|
numerator, per, denominator = string.split(/(\/|per)/)
|
119
118
|
units += Unit.parse_numerator_units(numerator)
|
120
119
|
units += Unit.parse_denominator_units(denominator) unless denominator.nil?
|
121
|
-
if units.size == 1
|
120
|
+
if units.size == 1 && units.first.index == 1
|
122
121
|
return units.first.unit
|
123
122
|
else
|
124
123
|
return Unit::Compound.new(*units)
|
@@ -141,40 +140,51 @@ module Quantify
|
|
141
140
|
|
142
141
|
def self.match_known_unit(attribute, string_or_symbol)
|
143
142
|
string_or_symbol = Unit.format_unit_attribute(attribute, string_or_symbol)
|
144
|
-
|
145
|
-
|
143
|
+
@units.find do |unit|
|
144
|
+
unit_attribute = unit.send(attribute)
|
145
|
+
if attribute == :name
|
146
|
+
unit_attribute.downcase == string_or_symbol
|
147
|
+
else
|
148
|
+
unit_attribute == string_or_symbol
|
149
|
+
end
|
150
|
+
end.clone rescue nil
|
146
151
|
end
|
147
152
|
|
148
153
|
def self.match_prefixed_variant(attribute, string_or_symbol)
|
149
154
|
string_or_symbol = Unit.format_unit_attribute(attribute, string_or_symbol)
|
150
|
-
if string_or_symbol =~ /\A(#{Unit::Prefix
|
151
|
-
string_or_symbol =~ /\A(#{Unit::Prefix
|
155
|
+
if string_or_symbol =~ /\A(#{units_for_regex(Unit::Prefix,:si_prefixes,attribute)})(#{units_for_regex(Unit,:si_non_prefixed_units,attribute)})\z/ ||
|
156
|
+
string_or_symbol =~ /\A(#{units_for_regex(Unit::Prefix,:non_si_prefixes,attribute)})(#{units_for_regex(Unit,:non_si_non_prefixed_units,attribute)})\z/
|
152
157
|
return Unit.for($2).with_prefix($1).clone
|
153
158
|
end
|
154
159
|
return nil
|
155
160
|
end
|
156
161
|
|
157
|
-
#
|
162
|
+
# standardize query strings or symbols into canonical form for unit names,
|
158
163
|
# symbols and labels
|
159
164
|
#
|
160
165
|
def self.format_unit_attribute(attribute, string_or_symbol)
|
161
166
|
string_or_symbol = case attribute
|
162
|
-
when :symbol then string_or_symbol.
|
163
|
-
when :name then string_or_symbol.
|
167
|
+
when :symbol then string_or_symbol.remove_underscores
|
168
|
+
when :name then string_or_symbol.remove_underscores.singularize.downcase
|
164
169
|
else string_or_symbol.to_s
|
165
170
|
end
|
171
|
+
Quantify.use_superscript_characters? ?
|
172
|
+
string_or_symbol.with_superscript_characters : string_or_symbol.without_superscript_characters
|
166
173
|
end
|
167
174
|
|
168
175
|
def self.parse_unit_and_index(string)
|
169
176
|
string.scan(/([^0-9\^]+)\^?([\d\.-]*)?/i)
|
170
|
-
index = ($2.nil?
|
177
|
+
index = ($2.nil? || $2.empty? ? 1 : $2.to_i)
|
171
178
|
CompoundBaseUnit.new($1.to_s, index)
|
172
179
|
end
|
173
180
|
|
174
181
|
def self.parse_numerator_units(string)
|
175
182
|
# If no middot then names parsed by whitespace
|
176
|
-
|
177
|
-
|
183
|
+
if string =~ /·/
|
184
|
+
num_units = string.split("·")
|
185
|
+
else
|
186
|
+
num_units = Unit.escape_multi_word_units(string).split(" ")
|
187
|
+
end
|
178
188
|
num_units.map! do |substring|
|
179
189
|
Unit.parse_unit_and_index(substring)
|
180
190
|
end
|
@@ -199,26 +209,47 @@ module Quantify
|
|
199
209
|
# given importance in #match (and #for) methods regexen
|
200
210
|
#
|
201
211
|
def self.si_non_prefixed_units
|
202
|
-
@units.select {|unit| unit.is_si_unit?
|
212
|
+
@units.select {|unit| unit.is_si_unit? && !unit.is_prefixed_unit? }
|
203
213
|
end
|
204
214
|
|
205
215
|
# This can be replicated by method missing approach, but explicit method provided
|
206
216
|
# given importance in #match (and #for) methods regexen
|
207
217
|
#
|
208
218
|
def self.non_si_non_prefixed_units
|
209
|
-
@units.select {|unit| unit.is_non_si_unit?
|
219
|
+
@units.select {|unit| unit.is_non_si_unit? && !unit.is_prefixed_unit? }
|
210
220
|
end
|
211
221
|
|
222
|
+
# Return a list of unit names which are multi-word
|
212
223
|
def self.multi_word_unit_names
|
213
|
-
@units.map
|
224
|
+
@units.map {|unit| unit.name if unit.name.word_count > 1 }.compact
|
214
225
|
end
|
215
226
|
|
227
|
+
# Return a list of pluralised unit names which are multi-word
|
216
228
|
def self.multi_word_unit_pluralized_names
|
217
|
-
multi_word_unit_names.map
|
229
|
+
multi_word_unit_names.map {|name| name.pluralize }
|
218
230
|
end
|
219
231
|
|
232
|
+
# Return a list of unit symbols which are multi-word
|
220
233
|
def self.multi_word_unit_symbols
|
221
|
-
@units.map
|
234
|
+
@units.map {|unit| unit.symbol if unit.symbol.word_count > 1 }.compact
|
235
|
+
end
|
236
|
+
|
237
|
+
# Underscore any parts of string which represent multi-word unit identifiers
|
238
|
+
# so that units can be parsed on whitespace
|
239
|
+
#
|
240
|
+
def self.escape_multi_word_units(string)
|
241
|
+
(multi_word_unit_symbols + multi_word_unit_pluralized_names + multi_word_unit_names).each do |id|
|
242
|
+
string.gsub!(id, id.gsub(" ","_")) if /#{id}/.match(string)
|
243
|
+
end
|
244
|
+
string
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns a list of "|" separated unit identifiers for use as regex
|
248
|
+
# alternatives. Lists are constructed by calling
|
249
|
+
def self.units_for_regex(klass,method,attribute)
|
250
|
+
list = klass.send(method).map { |item| item.send(attribute) }
|
251
|
+
list.map! { |item| item.downcase } if attribute == :name
|
252
|
+
list.join("|")
|
222
253
|
end
|
223
254
|
|
224
255
|
end
|