quantify 1.0.0
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.
- data/COPYING +20 -0
- data/Examples.rb +104 -0
- data/README +7 -0
- data/lib/quantify/config.rb +379 -0
- data/lib/quantify/core_extensions.rb +63 -0
- data/lib/quantify/dimensions.rb +523 -0
- data/lib/quantify/exception.rb +21 -0
- data/lib/quantify/inflections.rb +63 -0
- data/lib/quantify/quantify.rb +37 -0
- data/lib/quantify/quantity.rb +325 -0
- data/lib/quantify/unit/base_unit.rb +518 -0
- data/lib/quantify/unit/compound_base_unit.rb +91 -0
- data/lib/quantify/unit/compound_unit.rb +321 -0
- data/lib/quantify/unit/non_si_unit.rb +20 -0
- data/lib/quantify/unit/prefix/base_prefix.rb +42 -0
- data/lib/quantify/unit/prefix/non_si_prefix.rb +10 -0
- data/lib/quantify/unit/prefix/prefix.rb +73 -0
- data/lib/quantify/unit/prefix/si_prefix.rb +10 -0
- data/lib/quantify/unit/si_unit.rb +10 -0
- data/lib/quantify/unit/unit.rb +217 -0
- data/lib/quantify.rb +26 -0
- data/spec/dimension_spec.rb +294 -0
- data/spec/quantity_spec.rb +250 -0
- data/spec/unit_spec.rb +687 -0
- metadata +103 -0
@@ -0,0 +1,321 @@
|
|
1
|
+
|
2
|
+
module Quantify
|
3
|
+
module Unit
|
4
|
+
class Compound < Base
|
5
|
+
|
6
|
+
# Compound units are units made up of two or more units and powers thereof.
|
7
|
+
#
|
8
|
+
# The relationships between these units represent multiplication or division.
|
9
|
+
# E.g. a 'kilowatt hour' is the unit derived from multiplying a kilowatt and
|
10
|
+
# an hour. The 'kilogram per cubic metre' similarly represents the kilogram
|
11
|
+
# divided by the cubic metre (which itself represents metre x metre x metre).
|
12
|
+
#
|
13
|
+
# There are many SI units and NonSI units which are technically compound
|
14
|
+
# units - being derived from several base units. For example, the watt
|
15
|
+
# represents the joule (itself compound) divided by the second. In this
|
16
|
+
# case though, the use of a special name for the unit - the 'watt' rather
|
17
|
+
# than the 'kilogram square metre per cubic second' - allows it to be
|
18
|
+
# treated as a standard SI unit.
|
19
|
+
#
|
20
|
+
# The Compound class provides support for arbitrarily defined compound units
|
21
|
+
# which don't have well-established names.
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
# Consilidates base quantities by finding multiple instances of the same unit
|
26
|
+
# type and reducing them into a single unit represenation, by altering the
|
27
|
+
# repsective index. It has the effect of raising units to powers and cancelling
|
28
|
+
# those which appear in the numerator AND denominator
|
29
|
+
#
|
30
|
+
# This is a class method which takes an arbitrary array of base units as an
|
31
|
+
# argument. This means that consolidation can be performed on either all
|
32
|
+
# base units or just a subset - the numerator or denominator units.
|
33
|
+
#
|
34
|
+
def self.consolidate_base_units(base_units)
|
35
|
+
raise InvalidArgumentError, "Must provide an array of base units" unless base_units.is_a? Array
|
36
|
+
|
37
|
+
new_base_units = []
|
38
|
+
|
39
|
+
while base_units.size > 0 do
|
40
|
+
new_base = base_units.shift
|
41
|
+
next if new_base.unit.is_dimensionless?
|
42
|
+
|
43
|
+
new_base.index = base_units.select do |other_base|
|
44
|
+
new_base.unit == other_base.unit
|
45
|
+
end.inject(new_base.index) do |index,other_base|
|
46
|
+
base_units.delete other_base
|
47
|
+
index += other_base.index
|
48
|
+
end
|
49
|
+
|
50
|
+
new_base_units << new_base unless new_base.is_dimensionless?
|
51
|
+
end
|
52
|
+
return new_base_units
|
53
|
+
end
|
54
|
+
|
55
|
+
# Make compound unit use consistent units for representing each physical
|
56
|
+
# quantity. For example, lb/kg => kg/kg.
|
57
|
+
#
|
58
|
+
# This is a class method which takes an arbitrary array of base units as an
|
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
|
+
#''
|
62
|
+
# The units to use for particular physical dimension can be specified
|
63
|
+
# following the inital argument. If no unit is specified for a physical
|
64
|
+
# quantity which is represented in the array of base units, then the first
|
65
|
+
# unit found for that physical quantity is used as the canonical one.
|
66
|
+
#
|
67
|
+
def self.rationalize_base_units(base_units=[],*required_units)
|
68
|
+
base_units.each do |base|
|
69
|
+
new_unit = required_units.map { |unit| Unit.for(unit) }.find { |unit| unit.measures == base.measures } ||
|
70
|
+
base_units.find { |unit| unit.measures == base.measures }.unit
|
71
|
+
base.unit = new_unit
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
attr_reader :base_units, :acts_as_equivalent_unit
|
76
|
+
|
77
|
+
# Initialize a compound unit by providing an array containing a represenation
|
78
|
+
# of each base unit.
|
79
|
+
#
|
80
|
+
# Array may contain elements specified as follows:
|
81
|
+
#
|
82
|
+
# 1. a instance of CompoundBaseUnit
|
83
|
+
#
|
84
|
+
# 2. an instance of Unit::Base (in which case its index is assumed as 1
|
85
|
+
#
|
86
|
+
# 3. a sub-array of size 2 containing an instance of Unit::Base and an
|
87
|
+
# explicit index
|
88
|
+
#
|
89
|
+
def initialize(*units)
|
90
|
+
@base_units = []
|
91
|
+
units.each do |unit|
|
92
|
+
if unit.is_a? CompoundBaseUnit
|
93
|
+
@base_units << unit
|
94
|
+
elsif unit.is_a? Unit::Base
|
95
|
+
@base_units << CompoundBaseUnit.new(unit)
|
96
|
+
elsif unit.is_a? Array and unit.first.is_a? Unit::Base and
|
97
|
+
not unit.first.is_a? Compound and unit.size == 2
|
98
|
+
@base_units << CompoundBaseUnit.new(unit.first,unit.last)
|
99
|
+
else
|
100
|
+
raise InvalidArgumentError, "#{unit} does not represent a valid base unit"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
@acts_as_alternative_unit = true
|
104
|
+
@acts_as_equivalent_unit = false
|
105
|
+
consolidate_numerator_and_denominator_units!
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
# Returns an array containing only the base units which have positive indices
|
110
|
+
def numerator_units
|
111
|
+
@base_units.select { |base| base.is_numerator? }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns an array containing only the base units which have negative indices
|
115
|
+
def denominator_units
|
116
|
+
@base_units.select { |base| base.is_denominator? }
|
117
|
+
end
|
118
|
+
|
119
|
+
# Convenient accessor method for pluralized names
|
120
|
+
def pluralized_name
|
121
|
+
derive_name :plural
|
122
|
+
end
|
123
|
+
|
124
|
+
# Determine is a unit object represents an SI named unit.
|
125
|
+
#
|
126
|
+
def is_si_unit?
|
127
|
+
@base_units.all? { |base| base.is_si_unit? }
|
128
|
+
end
|
129
|
+
|
130
|
+
def is_non_si_unit?
|
131
|
+
@base_units.any? { |base| base.is_non_si_unit? }
|
132
|
+
end
|
133
|
+
|
134
|
+
# Consolidate base units. A 'full' consolidation is performed, i.e.
|
135
|
+
# consolidation across numerator and denominator. This is equivalent to the
|
136
|
+
# automatic partial consolidation AND a cancelling of units (i.e.
|
137
|
+
# #cancel_base_units!)
|
138
|
+
#
|
139
|
+
def consolidate_base_units!
|
140
|
+
@base_units = Compound.consolidate_base_units(@base_units)
|
141
|
+
initialize_attributes
|
142
|
+
return self
|
143
|
+
end
|
144
|
+
|
145
|
+
# Cancel base units across numerator and denominator. If similar units occur
|
146
|
+
# in both the numerator and denominator, they can be cancelled, i.e. their
|
147
|
+
# powers reduced correspondingly until one is removed.
|
148
|
+
#
|
149
|
+
# This method is useful when wanting to remove specific units that can be
|
150
|
+
# cancelled from the compound unit configuration while retaining the
|
151
|
+
# remaining units in the current format.
|
152
|
+
#
|
153
|
+
# If no other potentially cancelable units need to be retained, the method
|
154
|
+
# #consolidate_base_units! can be called with the :full argument instead
|
155
|
+
#
|
156
|
+
# This method takes an arbitrary number of arguments which represent the units
|
157
|
+
# which are required to be cancelled (string, symbol or object)
|
158
|
+
#
|
159
|
+
def cancel_base_units!(*units)
|
160
|
+
units.each do |unit|
|
161
|
+
raise InvalidArgumentError, "Cannot cancel by a compound unit" if unit.is_a? Unit::Compound
|
162
|
+
unit = Unit.for unit unless unit.is_a? Unit::Base
|
163
|
+
|
164
|
+
numerator_unit = numerator_units.find { |base| unit == base.unit }
|
165
|
+
denominator_unit = denominator_units.find { |base| unit == base.unit }
|
166
|
+
|
167
|
+
if numerator_unit and denominator_unit
|
168
|
+
cancel_value = [numerator_unit.index,denominator_unit.index].min.abs
|
169
|
+
numerator_unit.index -= cancel_value
|
170
|
+
denominator_unit.index += cancel_value
|
171
|
+
end
|
172
|
+
end
|
173
|
+
consolidate_numerator_and_denominator_units!
|
174
|
+
end
|
175
|
+
|
176
|
+
def rationalize_base_units!(scope=:partial,*units)
|
177
|
+
if scope == :full
|
178
|
+
Compound.rationalize_base_units(@base_units,units)
|
179
|
+
else
|
180
|
+
Compound.rationalize_base_units(numerator_units,units)
|
181
|
+
Compound.rationalize_base_units(denominator_units,units)
|
182
|
+
end
|
183
|
+
consolidate_numerator_and_denominator_units!
|
184
|
+
end
|
185
|
+
|
186
|
+
def equivalent_known_unit
|
187
|
+
Unit.units.find do |unit|
|
188
|
+
self == unit and
|
189
|
+
not unit.is_compound_unit?
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
def or_equivalent &block
|
194
|
+
equivalent_unit = equivalent_known_unit
|
195
|
+
if equivalent_unit and equivalent_unit.acts_as_equivalent_unit
|
196
|
+
return equivalent_unit
|
197
|
+
else
|
198
|
+
return self
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
protected
|
203
|
+
|
204
|
+
def initialize_attributes
|
205
|
+
@dimensions = derive_dimensions
|
206
|
+
@name = derive_name
|
207
|
+
@symbol = derive_symbol
|
208
|
+
@factor = derive_factor
|
209
|
+
@label = derive_label
|
210
|
+
end
|
211
|
+
|
212
|
+
# Partially consolidate base units, i.e. numerator and denomiator are
|
213
|
+
# consolidated separately. This means that two instances of the same unit
|
214
|
+
# should not occur in the numerator OR denominator (rather they are combined
|
215
|
+
# and the index changed accordingly), but similar units are not cancelled
|
216
|
+
# across the numerator and denominators.
|
217
|
+
#
|
218
|
+
def consolidate_numerator_and_denominator_units!
|
219
|
+
new_base_units = []
|
220
|
+
new_base_units += Compound.consolidate_base_units(numerator_units)
|
221
|
+
new_base_units += Compound.consolidate_base_units(denominator_units)
|
222
|
+
@base_units = new_base_units
|
223
|
+
initialize_attributes
|
224
|
+
return self
|
225
|
+
end
|
226
|
+
|
227
|
+
# Derive a representation of the physical dimensions of the compound unit
|
228
|
+
# by multilying together the dimensions of each of the base units.
|
229
|
+
#
|
230
|
+
def derive_dimensions
|
231
|
+
@base_units.inject(Dimensions.dimensionless) do |dimension,base|
|
232
|
+
dimension * base.dimensions
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# Derive a name for the unit based on the names of the base units
|
237
|
+
#
|
238
|
+
# Both singluar and plural names can be derived. In the case of pluralized
|
239
|
+
# names, the last unit in the numerator is pluralized. Singular names are
|
240
|
+
# assumed by default, in which case no argument is required.
|
241
|
+
#
|
242
|
+
# Format for names includes the phrase 'per' to differentiate denominator
|
243
|
+
# units and words, rather than numbers, for representing powers, e.g.
|
244
|
+
#
|
245
|
+
# square metres per second
|
246
|
+
#
|
247
|
+
def derive_name(inflection=:singular)
|
248
|
+
unit_name = ""
|
249
|
+
unless numerator_units.empty?
|
250
|
+
units = numerator_units
|
251
|
+
last_unit = units.pop if inflection == :plural
|
252
|
+
units.inject(unit_name) do |name,base|
|
253
|
+
name << base.name + " "
|
254
|
+
end
|
255
|
+
unit_name << last_unit.pluralized_name + " " if last_unit
|
256
|
+
end
|
257
|
+
unless denominator_units.empty?
|
258
|
+
unit_name << "per "
|
259
|
+
denominator_units.inject(unit_name) do |name,base|
|
260
|
+
name << base.name + " "
|
261
|
+
end
|
262
|
+
end
|
263
|
+
return unit_name.strip
|
264
|
+
end
|
265
|
+
|
266
|
+
# Derive a symbol for the unit based on the symbols of the base units
|
267
|
+
#
|
268
|
+
# Get the units in order first so that the denominator values (those
|
269
|
+
# with negative powers) follow the numerators
|
270
|
+
#
|
271
|
+
# Symbol format use unit symbols, with numerator symbols followed by
|
272
|
+
# denominator symbols and powers expressed using the "^" notation with 'true'
|
273
|
+
# values (i.e. preservation of minus signs).
|
274
|
+
#
|
275
|
+
def derive_symbol
|
276
|
+
@base_units.sort do |base,next_unit|
|
277
|
+
next_unit.index <=> base.index
|
278
|
+
end.inject('') do |symbol,base|
|
279
|
+
symbol << base.symbol + " "
|
280
|
+
end.strip
|
281
|
+
end
|
282
|
+
|
283
|
+
# Derive a label for the comound unit. This follows the format used in the
|
284
|
+
# JScience library in using a middot notation ("·") to spearate units and
|
285
|
+
# slash notation "/" to separate numerator and denominator. Since the
|
286
|
+
# denominator is differentiated, denominator unit powers are rendered in
|
287
|
+
# absolute terms (i.e. minus sign omitted) except when no numerator values
|
288
|
+
# are present.
|
289
|
+
#
|
290
|
+
def derive_label
|
291
|
+
unit_label = ""
|
292
|
+
unless numerator_units.empty?
|
293
|
+
numerator_units.inject(unit_label) do |label,base|
|
294
|
+
label << "·" unless unit_label.empty?
|
295
|
+
label << base.label
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
unless denominator_units.empty?
|
300
|
+
format = ( unit_label.empty? ? :label : :reciprocalized_label )
|
301
|
+
unit_label << "/" unless unit_label.empty?
|
302
|
+
denominator_units.inject(unit_label) do |label,base|
|
303
|
+
label << "·" unless unit_label.empty? or unit_label =~ /\/\z/
|
304
|
+
label << base.send(format)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
return unit_label
|
308
|
+
end
|
309
|
+
|
310
|
+
# Derive the multiplicative factor for the unit based on those of the base
|
311
|
+
# units
|
312
|
+
#
|
313
|
+
def derive_factor
|
314
|
+
@base_units.inject(1) do |factor,base|
|
315
|
+
factor * base.factor
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
|
2
|
+
module Quantify
|
3
|
+
module Unit
|
4
|
+
class NonSI < Base
|
5
|
+
|
6
|
+
# Class representing SI units. This inherits from Unit::Base
|
7
|
+
|
8
|
+
# Additional initialize. Some NonSI units - temperature units, celsius and
|
9
|
+
# farenheit - contain scaling factors in addition to multiplicative factors.
|
10
|
+
# These are required in order to perform conversion, e.g. kelvin => celsius
|
11
|
+
# and therefore become and additional attribute to NonSI units
|
12
|
+
#
|
13
|
+
def initialize(options)
|
14
|
+
@scaling = options[:scaling].nil? ? 0.0 : options.delete(:scaling).to_f
|
15
|
+
super(options)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# To change this template, choose Tools | Templates
|
2
|
+
# and open the template in the editor.
|
3
|
+
|
4
|
+
module Quantify
|
5
|
+
module Unit
|
6
|
+
module Prefix
|
7
|
+
class Base
|
8
|
+
|
9
|
+
def self.load(options)
|
10
|
+
if options.is_a? Hash
|
11
|
+
Prefix.prefixes << self.new(options)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.configure &block
|
16
|
+
self.class_eval &block if block
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :name, :symbol, :factor
|
20
|
+
|
21
|
+
def initialize(options)
|
22
|
+
@symbol = options[:symbol].standardize
|
23
|
+
@factor = options[:factor].to_f
|
24
|
+
@name = options[:name].standardize.downcase
|
25
|
+
end
|
26
|
+
|
27
|
+
def is_si_prefix?
|
28
|
+
self.is_a? SI
|
29
|
+
end
|
30
|
+
|
31
|
+
def is_non_si_prefix?
|
32
|
+
self.is_a? NonSI
|
33
|
+
end
|
34
|
+
|
35
|
+
def label
|
36
|
+
symbol
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
|
2
|
+
module Quantify
|
3
|
+
module Unit
|
4
|
+
module Prefix
|
5
|
+
|
6
|
+
attr_accessor :prefixes
|
7
|
+
|
8
|
+
@prefixes = []
|
9
|
+
|
10
|
+
def self.load(prefix)
|
11
|
+
@prefixes << prefix if prefix.is_a? Quantify::Unit::Prefix::Base
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.unload(*unloaded_prefixes)
|
15
|
+
[unloaded_prefixes].flatten.each do |unloaded_prefix|
|
16
|
+
unloaded_prefix = Prefix.for(unloaded_prefix)
|
17
|
+
@prefixes.delete_if { |unit| unit.label == unloaded_prefix.label }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.prefixes
|
22
|
+
@prefixes
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.for(name_or_symbol,collection=nil)
|
26
|
+
return name_or_symbol.clone if name_or_symbol.is_a? Quantify::Unit::Prefix::Base
|
27
|
+
if name_or_symbol.is_a? String or name_or_symbol.is_a? Symbol
|
28
|
+
if prefix = (collection.nil? ? @prefixes : collection).find do |prefix|
|
29
|
+
prefix.name == name_or_symbol.standardize.downcase or
|
30
|
+
prefix.symbol == name_or_symbol.standardize
|
31
|
+
end
|
32
|
+
return prefix.clone
|
33
|
+
else
|
34
|
+
return nil
|
35
|
+
end
|
36
|
+
else
|
37
|
+
raise InvalidArgumentError, "Argument must be a Symbol or String"
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# This can be replicated by method missing approach, but explicit method provided
|
42
|
+
# given importance in Unit #match (and #for) methods regexen
|
43
|
+
#
|
44
|
+
def self.si_prefixes
|
45
|
+
@prefixes.select {|prefix| prefix.is_si_prefix? }
|
46
|
+
end
|
47
|
+
|
48
|
+
# This can be replicated by method missing approach, but explicit method provided
|
49
|
+
# given importance in Unit #match (and #for) methods regexen
|
50
|
+
#
|
51
|
+
def self.non_si_prefixes
|
52
|
+
@prefixes.select {|prefix| prefix.is_non_si_prefix? }
|
53
|
+
end
|
54
|
+
|
55
|
+
def self.method_missing(method, *args, &block)
|
56
|
+
if method.to_s =~ /((si|non_si)_)?prefixes(_by_(name|symbol|label))?/
|
57
|
+
if $2
|
58
|
+
prefixes = Prefix.prefixes.select { |prefix| instance_eval("prefix.is_#{$2}_prefix?") }
|
59
|
+
else
|
60
|
+
prefixes = Prefix.prefixes
|
61
|
+
end
|
62
|
+
return_format = ( $4 ? $4.to_sym : nil )
|
63
|
+
prefixes.map(&return_format)
|
64
|
+
elsif prefix = self.for(method)
|
65
|
+
return prefix
|
66
|
+
else
|
67
|
+
raise NoMethodError, "Undefined method `#{method}` for #{self}:#{self.class}"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|