quantify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,523 @@
1
+ #! usr/bin/ruby
2
+
3
+ module Quantify
4
+
5
+ # The Dimensions class represents specfic physical quantities in
6
+ # terms of powers of their constituent base dimensions, e.g.:
7
+ #
8
+ # area = length^2
9
+ # force = mass^1 x length^1 x time^-2
10
+ #
11
+ # Each dimension object is characterised by instance variables
12
+ # which describe the power (or index) of the respective base dimensions.
13
+ # Dimension objects can be manipulated - multiplied, divided, raised
14
+ # to powers, etc.
15
+ #
16
+ # Standard physical quantities (e.g. length, acceleration, energy)
17
+ # are loaded into the @@dimensions class variable at runtime. These
18
+ # can be accessed, used and manipulated for arbitrary dimensional uses.
19
+ #
20
+ # Instances of Dimensions are also used as the basis for defining and
21
+ # manipulating objects of the Unit::Base class.
22
+
23
+ class Dimensions
24
+
25
+ # The BASE_QUANTITIES array specifies the system of base quantities
26
+ # upon which all Dimensions objects are defined.
27
+ #
28
+ # :information, :currency, :item represent tentative additions to
29
+ # the standard set of base quantities.
30
+ #
31
+ # :item is intended to represent arbitrary 'things' for specifying
32
+ # quantities such as, for example:
33
+ #
34
+ # 'dollars per capita' (:currency => 1, :items => -1)
35
+ # 'trees per hectare' (:items => 1, :length => -2).
36
+ #
37
+ BASE_QUANTITIES = [
38
+ :mass, :length, :time, :electric_current, :temperature,
39
+ :luminous_intensity, :amount_of_substance, :information,
40
+ :currency, :item ]
41
+
42
+ # Class variable which holds in memory all defined (and 'loaded') quantities
43
+ @@dimensions = []
44
+
45
+ # Provides access the class array which holds all defined quantities
46
+ def self.dimensions
47
+ @@dimensions
48
+ end
49
+
50
+ # Returns an array of Dimensions objects representing just the base quantities,
51
+ # i.e. length, mass, time, temperature, etc.
52
+ #
53
+ def self.base_dimensions
54
+ @@dimensions.select do |dimensions|
55
+ BASE_QUANTITIES.map(&:standardize).include? dimensions.describe
56
+ end
57
+ end
58
+
59
+ # This method allows specific, named quantities to be initialized and
60
+ # loaded into the @@dimensions array. Quantities are specified by their
61
+ # consituent base dimensions, but must also include a name/description,
62
+ # i.e. 'acceleration', :force - indicated by the :physical_quantity key -
63
+ # in order to be included in the system of known dimensions, e.g.:
64
+ #
65
+ # Dimensions.load :physical_quantity => :force,
66
+ # :length => 1,
67
+ # :mass => 1,
68
+ # :time => -2
69
+ #
70
+ # Standard quantities such as force, energy, mass, etc. should not need to
71
+ # be defined as they are included in the set of quantities already defined
72
+ # (see config.rb) and automatically loaded. These can be removed, overridden
73
+ # or configured differently if desired.
74
+ #
75
+ def self.load(options)
76
+ if options[:physical_quantity]
77
+ @@dimensions << Dimensions.new(options)
78
+ else
79
+ raise InvalidDimensionError, "Cannot load dimensions without physical quantity description"
80
+ end
81
+ end
82
+
83
+ # Remove a dimension from the system of known dimensions
84
+ def self.unload(*unloaded_dimensions)
85
+ [unloaded_dimensions].flatten.each do |unloaded_dimension|
86
+ unloaded_dimension = Dimensions.for(unloaded_dimensions)
87
+ @@dimensions.delete_if { |unit| unit.physical_quantity == unloaded_dimension.physical_quantity }
88
+ end
89
+ end
90
+
91
+ # Returns an array containing the names/descriptions of all known (loaded)
92
+ # physical quantities, e.g.:
93
+ #
94
+ # Dimensions.physical_quantities #=> [ 'acceleration',
95
+ # 'area',
96
+ # 'electric Current',
97
+ # ... ]
98
+ #
99
+ def self.physical_quantities
100
+ @@dimensions.map(&:physical_quantity)
101
+ end
102
+
103
+ # Retrieve a known quantity - returns a Dimensions instance, which is a
104
+ # clone of the initialized instance of the specified quantity. This enables
105
+ # the object to be modified/manipulated without corrupting the representation
106
+ # of the quantity in the @@dimensions class array.
107
+ #
108
+ # The required quantity name/descriptor can be specified as a symbol or a
109
+ # string, e.g.:
110
+ #
111
+ # Dimensions.for :acceleration
112
+ # Dimensions.for 'luminous_flux'
113
+ #
114
+ # These can be shortened to, e.g. Dimensions.acceleration by virtue of the
115
+ # #method_missing class method (below)
116
+ #
117
+ def self.for(name)
118
+ return name if name.is_a? Dimensions
119
+ if name.is_a? String or name.is_a? Symbol
120
+ if quantity = @@dimensions.find do |quantity|
121
+ quantity.physical_quantity == name.standardize.downcase
122
+ end
123
+ return quantity.clone
124
+ else
125
+ raise InvalidArgumentError, "Physical quantity not known: #{name}"
126
+ end
127
+ else
128
+ raise InvalidArgumentError, "Argument must be a Symbol or String"
129
+ end
130
+ end
131
+
132
+ # Syntactic sugar for defining the known quantities. This method simply
133
+ # evaluates code within the context of the Dimensions class, enabling
134
+ # the required quantities to be loaded at runtime, e.g.
135
+ #
136
+ # Dimensions.configure do
137
+ #
138
+ # load :physical_quantity => :length, :length => 1
139
+ # load :physical_quantity => :area, :length => 2
140
+ # load :physical_quantity => :power, :mass => 1, :length => 2, :time => -3
141
+ #
142
+ # end
143
+ #
144
+ def self.configure &block
145
+ self.class_eval &block if block
146
+ end
147
+
148
+ # Provides a shorthand for retrieving known quantities, e.g.:
149
+ #
150
+ # Dimensions.force
151
+ #
152
+ # is equivalent to
153
+ #
154
+ # Dimensions.for :force
155
+ #
156
+ # Both variants return a clone of the initialized dimensional representation
157
+ # of the specified physical quantity (i.e. force).
158
+ #
159
+ def self.method_missing(method, *args, &block)
160
+ if dimensions = self.for(method)
161
+ return dimensions
162
+ end
163
+ super
164
+ end
165
+
166
+ BASE_QUANTITIES.each { |quantity| attr_reader quantity }
167
+
168
+ attr_accessor :physical_quantity
169
+
170
+ # Initialize a new Dimension object.
171
+ #
172
+ # The options argument is a hash which represents the base dimensions that
173
+ # define the physical quantity. Each key-value pair should consist of a key
174
+ # included in the BASE_QUANTITIES array, and a value which represents the
175
+ # index/power of that base quantity.
176
+ #
177
+ # In addition, a name or description of the physical quantity can be
178
+ # specified (i.e. 'acceleration', 'electric_current'). This is optional for
179
+ # creating a new Dimensions instance, but required if that object is to be
180
+ # loaded into the @@dimensions class array. e.g.:
181
+ #
182
+ # Dimensions.new :physical_quantity => :density,
183
+ # :mass => 1,
184
+ # :length => -3
185
+ #
186
+ def initialize(options={})
187
+ if options.has_key?(:physical_quantity)
188
+ @physical_quantity = options.delete(:physical_quantity).standardize.downcase
189
+ end
190
+ enumerate_base_quantities(options)
191
+ describe
192
+ end
193
+
194
+ # Load an already instantiated Dimensions object into the @@dimensions class
195
+ # array, from which it will be accessible as a universal representation of
196
+ # that physical quantity.
197
+ #
198
+ # Object must include a non-nil @physical_quantity attribute, i.e. a name or
199
+ # description of the physical quantity represented.
200
+ #
201
+ def load
202
+ if describe and not loaded?
203
+ @@dimensions << self
204
+ elsif describe
205
+ raise InvalidDimensionError, "A dimension instance with the same physical quantity already exists"
206
+ else
207
+ raise InvalidDimensionError, "Cannot load dimensions without physical quantity description"
208
+ end
209
+ end
210
+
211
+ def loaded?
212
+ Dimensions.dimensions.any? { |quantity| self.has_same_identity_as? quantity }
213
+ end
214
+
215
+ # Remove from system of known units.
216
+ def unload
217
+ Dimensions.unload(self.physical_quantity)
218
+ end
219
+
220
+ def has_same_identity_as?(other)
221
+ self.physical_quantity == other.physical_quantity and not self.physical_quantity.nil?
222
+ end
223
+
224
+ # Return a description of what physical quantity self represents. If no
225
+ # value is found in the @physical_quantity instance variable, the task is
226
+ # delegated to the #get_description method.
227
+ #
228
+ def describe
229
+ @physical_quantity or get_description
230
+ end
231
+
232
+ # Searches the system of known physical quantities (@@dimensions class
233
+ # array) looking for any which match self in terms of the configuration of
234
+ # base dimensions, i.e. an object which dimensionally represents the same
235
+ # thing.
236
+ #
237
+ # If found, the name/description of that quantity is assigned to the
238
+ # @physical_quantity attribute of self.
239
+ #
240
+ # This method is useful in cases where Dimensions instances are manipulated
241
+ # using operators (e.g. multiply, divide, power, reciprocal), resulting in
242
+ # a change to the configuration of base dimensions (perhaps as a new instance).
243
+ # This method tries to find a description of the new quantity.
244
+ #
245
+ # If none is found, self.physical_quantity is set to nil.
246
+ #
247
+ def get_description
248
+ similar = @@dimensions.find { |quantity| quantity == self }
249
+ @physical_quantity = ( similar.nil? ? nil : similar.physical_quantity )
250
+ end
251
+
252
+ # Returns an array containing the known units which represent the physical
253
+ # quantity described by self
254
+ #
255
+ # If no argument is given, the array holds instances of Unit::Base (or
256
+ # subclasses) which represent each unit. Alternatively only the names or
257
+ # symbols of each unit can be returned by providing the appropriate unit
258
+ # attribute as a symbolized argument, e.g.
259
+ #
260
+ # Dimensions.energy.units #=> [ #<Quantify::Dimensions: .. >,
261
+ # #<Quantify::Dimensions: .. >,
262
+ # ... ]
263
+ #
264
+ # Dimensions.mass.units :name #=> [ 'kilogram', 'ounce',
265
+ # 'pound', ... ]
266
+ #
267
+ # Dimensions.length.units :symbol #=> [ 'm', 'ft', 'yd', ... ]
268
+ #
269
+ def units(by=nil)
270
+ Unit.units.select { |unit| unit.dimensions == self }.map(&by)
271
+ end
272
+
273
+ # Returns the SI unit for the physical quantity described by self.
274
+ #
275
+ # Plane/solid angle are special cases which are dimensionless units, and so
276
+ # are handled explicitly. Otherwise, the si base units for each of the base
277
+ # dimensions of self are indentified and the corresponding compound unit is
278
+ # derived. If this new unit is the same as a known (SI derived) unit, the
279
+ # known unit is returned.
280
+ #
281
+ # Dimensions.energy.units #=> #<Quantify::Dimensions: .. >
282
+ #
283
+ # Dimensions.energy.si_unit.name #=> 'joule'
284
+ #
285
+ # Dimensions.kinematic_viscosity.si_unit.name
286
+ #
287
+ # #=> 'metre squared per second'
288
+ #
289
+ def si_unit
290
+ return Unit.steridian if self.describe == 'solid angle'
291
+ return Unit.radian if self.describe == 'plane angle'
292
+ return si_base_units.inject(Unit.unity) do |compound,unit|
293
+ compound * unit
294
+ end.or_equivalent
295
+ rescue
296
+ return nil
297
+ end
298
+
299
+ # Returns an array representing the base SI units for the physical quantity
300
+ # described by self
301
+ #
302
+ # If no argument is given, the array holds instances of Unit::Base (or
303
+ # subclasses) which represent each base unit. Alternatively only the names
304
+ # or symbols of each unit can be returned by providing the appropriate unit
305
+ # attribute as a symbolized argument, e.g.
306
+ #
307
+ # Dimensions.energy.si_base_units #=> [ #<Quantify::Unit: .. >,
308
+ # #<Quantify::Unit: .. >,
309
+ # ... ]
310
+ #
311
+ # Dimensions.energy.si_base_units :name
312
+ #
313
+ # #=> [ "metre squared",
314
+ # "per second squared",
315
+ # "kilogram"] #
316
+ #
317
+ # Dimensions.force.units :symbol #=> [ "m", "s^-2", "kg"]
318
+ #
319
+ def si_base_units(by=nil)
320
+ self.to_hash.map do |dimension,index|
321
+ Unit.si_base_units.select do |unit|
322
+ unit.measures == dimension.standardize
323
+ end.first.clone ** index
324
+ end.map(&by)
325
+ end
326
+
327
+ # Compares the base quantities of two Dimensions objects and returns true if
328
+ # they are the same. This indicates that the two objects represent the same
329
+ # physical quantity (irrespective of their names - @physical_quantity - being
330
+ # similar, different, or absent.
331
+ #
332
+ def ==(other)
333
+ self.to_hash == other.to_hash
334
+ end
335
+
336
+ # Returns true if the physical quantity that self represents is known
337
+ def is_known?
338
+ describe ? true : false
339
+ end
340
+
341
+ # Returns true if self is a dimensionless quantity
342
+ def is_dimensionless?
343
+ base_quantities.empty?
344
+ end
345
+
346
+ # Returns true if self represents one of the base quantities (i.e. length,
347
+ # mass, time, etc.)
348
+ def is_base?
349
+ base_quantities.size == 1 and
350
+ self.instance_variable_get(base_quantities.first) == 1 ? true : false
351
+ end
352
+
353
+ # Method for identifying quantities which are 'specific' quantities, i.e
354
+ # quantities which represent a quantity of something *per unit mass*
355
+ #
356
+ def is_specific_quantity?
357
+ denominator_quantities == ["@mass"]
358
+ end
359
+
360
+ # Method for identifying quantities which are 'molar' quantities, i.e
361
+ # quantities which represent a quantity of something *per mole*
362
+ #
363
+ def is_molar_quantity?
364
+ denominator_quantities == ["@amount_of_substance"]
365
+ end
366
+
367
+
368
+ # Multiplies self by another Dimensions object, returning self with an
369
+ # updated configuration of dimensions. Since this is likely to have resulted
370
+ # in the representation of a different physical quantity than was originally
371
+ # represented, the #get_description method is invoked to attempt to find a
372
+ # suitable description.
373
+ #
374
+ def multiply!(other)
375
+ enumerate_base_quantities(other.to_hash)
376
+ get_description
377
+ return self
378
+ end
379
+
380
+ # Similar to #multiply! but returns a new Dimensions instance representing
381
+ # the physical quantity which results from the multiplication.
382
+ #
383
+ def multiply(other)
384
+ Dimensions.new(self.to_hash).multiply! other
385
+ end
386
+
387
+ # Similar to #multiply! but performs a division of self by the specified
388
+ # Dimensions object.
389
+ #
390
+ def divide!(other)
391
+ enumerate_base_quantities(other.reciprocalize.to_hash)
392
+ get_description
393
+ return self
394
+ end
395
+
396
+ # Similar to #divide! but returns a new Dimensions instance representing
397
+ # the physical quantity which results from the division.
398
+ #
399
+ def divide(other)
400
+ Dimensions.new(self.to_hash).divide! other
401
+ end
402
+
403
+ # Raises self to the power provided. As with multiply and divide, the
404
+ # #get_description method is invoked to attempt to find a suitable
405
+ # description for the new quantity represented.
406
+ #
407
+ def pow!(power)
408
+ make_dimensionless if power == 0
409
+ if power < 0
410
+ self.reciprocalize!
411
+ power *= -1
412
+ end
413
+ original_dimensions = self.clone
414
+ (power - 1).times { self.multiply!(original_dimensions) }
415
+ get_description
416
+ return self
417
+ end
418
+
419
+ # Similar to #pow! but returns a new Dimensions instance representing
420
+ # the physical quantity which results from the raised power.
421
+ #
422
+ def pow(power)
423
+ Dimensions.new(self.to_hash).pow!(power)
424
+ end
425
+
426
+ # Inverts self, returning a representation of 1/self. This is equivalent to
427
+ # raising to the power -1. The #get_description method is invoked to attempt
428
+ # to find a suitable description for the new quantity represented.
429
+ #
430
+ def reciprocalize!
431
+ base_quantities.each do |variable|
432
+ new_value = self.instance_variable_get(variable) * -1
433
+ self.instance_variable_set(variable, new_value)
434
+ end
435
+ get_description
436
+ return self
437
+ end
438
+
439
+ # Similar to #reciprocalize! but returns a new Dimensions instance representing
440
+ # the physical quantity which results from the inversion.
441
+ #
442
+ def reciprocalize
443
+ Dimensions.new(self.to_hash).reciprocalize!
444
+ end
445
+
446
+ alias :times :multiply
447
+ alias :* :multiply
448
+ alias :/ :divide
449
+ alias :** :pow
450
+
451
+ protected
452
+
453
+ # Returns an array containing the names of the instance variables which
454
+ # represent the base quantities of self. This enables various operations to
455
+ # be performed on these variables without touching the @physical_quantity
456
+ # variable.
457
+ #
458
+ def base_quantities
459
+ quantities = self.instance_variables
460
+ quantities.delete("@physical_quantity")
461
+ return quantities
462
+ end
463
+
464
+ # Just the base quantities which have positive indices
465
+ def numerator_quantities
466
+ base_quantities.select { |quantity| self.instance_variable_get(quantity) > 0 }
467
+ end
468
+
469
+ # Just the base quantities which have negative indices
470
+ def denominator_quantities
471
+ base_quantities.select { |quantity| self.instance_variable_get(quantity) < 0 }
472
+ end
473
+
474
+ # Returns a hash representation of the base dimensions of self. This is used
475
+ # in various operations and is useful for instantiating new objects with
476
+ # the same base dimensions.
477
+ #
478
+ def to_hash
479
+ hash = {}
480
+ base_quantities.each do |variable|
481
+ hash[variable.gsub("@","").to_sym] = self.instance_variable_get(variable)
482
+ end
483
+ return hash
484
+ end
485
+
486
+ # Method for initializing the base quantities of self.
487
+ #
488
+ # Where base quantities are already defined, the new indices are added to
489
+ # the existing ones. This represents the multiplication of base quantities
490
+ # (multiplication of similar quantities involves the addition of their
491
+ # powers).
492
+ #
493
+ # This method is therefore used in the multiplication of Dimensions objects,
494
+ # but also in divisions and raising of powers following other operations.
495
+ #
496
+ def enumerate_base_quantities(options)
497
+ options.each_pair do |base_quantity,index|
498
+ base_quantity = base_quantity.to_s.downcase.to_sym
499
+ unless index.is_a? Integer and BASE_QUANTITIES.include? base_quantity
500
+ raise InvalidDimensionError, "An invalid base quantity was specified (#{base_quantity})"
501
+ end
502
+ variable = "@#{base_quantity}"
503
+ if self.instance_variable_defined?(variable)
504
+ new_index = self.instance_variable_get(variable) + index
505
+ if new_index == 0
506
+ remove_instance_variable(variable)
507
+ else
508
+ self.instance_variable_set(variable, new_index)
509
+ end
510
+ else
511
+ self.instance_variable_set(variable, index)
512
+ end
513
+ end
514
+ end
515
+
516
+ # Make object represent a dimensionless quantity.
517
+ def make_dimensionless
518
+ self.physical_quantity = 'dimensionless'
519
+ base_quantities.each { |var| remove_instance_variable(var) }
520
+ end
521
+
522
+ end
523
+ end
@@ -0,0 +1,21 @@
1
+ module Quantify
2
+
3
+ class QuantityParseError < Exception
4
+ end
5
+
6
+ class InvalidObjectError < Exception
7
+ end
8
+
9
+ class InvalidUnitError < Exception
10
+ end
11
+
12
+ class InvalidDimensionError < Exception
13
+ end
14
+
15
+ class InvalidPhysicalQuantityError < Exception
16
+ end
17
+
18
+ class InvalidArgumentError < Exception
19
+ end
20
+
21
+ end
@@ -0,0 +1,63 @@
1
+
2
+ ActiveSupport::Inflector.inflections do |inflect|
3
+
4
+ inflect.uncountable %w( clo hertz lux siemens )
5
+
6
+ inflect.plural /(metre)/i, '\1s'
7
+ inflect.singular /(metre)s?/i, '\1'
8
+
9
+ inflect.plural /(degree)/i, '\1s'
10
+ inflect.singular /(degree)s?/i, '\1'
11
+
12
+ inflect.plural /(barrel)/i, '\1s'
13
+ inflect.singular /(barrel)s?/i, '\1'
14
+
15
+ inflect.plural /(unit)/i, '\1s'
16
+ inflect.singular /(unit)s?/i, '\1'
17
+
18
+ inflect.plural /(ounce)/i, '\1s'
19
+ inflect.singular /(ounce)s?/i, '\1'
20
+
21
+ inflect.plural /(volt)/i, '\1s'
22
+ inflect.singular /(volt)s?/i, '\1'
23
+
24
+ inflect.plural /(foot)/i, 'feet'
25
+ inflect.singular /(feet)/i, 'foot'
26
+
27
+ inflect.plural /(gallon)/i, '\1s'
28
+ inflect.singular /(gallon)s?/i, '\1'
29
+
30
+ inflect.plural /(horsepower)/i, '\1'
31
+ inflect.singular /(horsepower)/i, '\1'
32
+
33
+ inflect.plural /(hundredweight)/i, '\1'
34
+ inflect.singular /(hundredweight)/i, '\1'
35
+
36
+ inflect.plural /(inch)/i, '\1es'
37
+ inflect.singular /(inch)(es)?/i, '\1'
38
+
39
+ inflect.plural /(league)/i, '\1s'
40
+ inflect.singular /(league)s?/i, '\1'
41
+
42
+ inflect.plural /(mass)/i, '\1es'
43
+ inflect.singular /(mass)(es)?/i, '\1'
44
+
45
+ inflect.plural /(mile)/i, '\1s'
46
+ inflect.singular /(mile)s?/i, '\1'
47
+
48
+ inflect.plural /(pound)/i, '\1s'
49
+ inflect.singular /(pound)s?/i, '\1'
50
+
51
+ inflect.plural /(ton)/i, '\1s'
52
+ inflect.singular /(ton)s?/i, '\1'
53
+
54
+ inflect.plural /(tonne)/i, '\1s'
55
+ inflect.singular /(tonne)s?/i, '\1'
56
+
57
+ inflect.plural /(stone)/i, '\1s'
58
+ inflect.singular /(stone)s?/i, '\1'
59
+
60
+ inflect.irregular 'footcandle', 'footcandles'
61
+ inflect.irregular 'kilowatt hour', 'kilowatt hours'
62
+
63
+ end
@@ -0,0 +1,37 @@
1
+ module Quantify
2
+
3
+ def self.configure &block
4
+ self.module_eval &block if block
5
+ end
6
+
7
+ module ExtendedMethods
8
+
9
+ # Provides syntactic sugar for accessing units via the #for method.
10
+ # Specify:
11
+ #
12
+ # Unit.degree_celsius
13
+ #
14
+ # rather than Unit.for :degree_celsius
15
+ #
16
+ def method_missing(method, *args, &block)
17
+ if method.to_s =~ /((si|non_si|compound)_)?(non_(prefixed)_)?((base|derived|benchmark)_)?units(_by_(name|symbol|label))?/
18
+ if $2 or $4 or $6
19
+ conditions = []
20
+ conditions << "unit.is_#{$2}_unit?" if $2
21
+ conditions << "!unit.is_prefixed_unit?" if $4
22
+ conditions << "unit.is_#{$6}_unit?" if $6
23
+ units = Unit.units.select { |unit| instance_eval(conditions.join(" and ")) }
24
+ else
25
+ units = Unit.units
26
+ end
27
+ return_format = ( $8 ? $8.to_sym : nil )
28
+ units.map(&return_format)
29
+ elsif unit = Unit.for(method)
30
+ return unit
31
+ else
32
+ super
33
+ end
34
+ end
35
+
36
+ end
37
+ end