quantify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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