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.
@@ -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,10 @@
1
+
2
+ module Quantify
3
+ module Unit
4
+ module Prefix
5
+ class NonSI < Base
6
+
7
+ end
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,10 @@
1
+
2
+ module Quantify
3
+ module Unit
4
+ module Prefix
5
+ class SI < Base
6
+
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,10 @@
1
+
2
+ module Quantify
3
+ module Unit
4
+ class SI < Base
5
+
6
+ # Class representing SI units. This inherits from Unit::Base
7
+
8
+ end
9
+ end
10
+ end