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,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