quantify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,518 @@
1
+
2
+ module Quantify
3
+ module Unit
4
+ class Base
5
+
6
+ extend ExtendedMethods
7
+
8
+ # Base unit class, providing most of the functionality which is inherited
9
+ # by SI and NonSI unit classes.
10
+
11
+ # Create a new instance of self (i.e. Base or an inherited class) and load
12
+ # into the system of known units. See initialize for details of options
13
+ #
14
+ def self.load(options)
15
+ unit = self.new(options)
16
+ unit.load
17
+ end
18
+
19
+ def self.construct_and_load(unit,&block)
20
+ self.construct(unit, &block).load
21
+ end
22
+
23
+ # Mass load prefixed units. First argument is a single or array of units.
24
+ # Second argument is a single or array of prefixes. All specfied units will
25
+ # be loaded with all specified prefixes.
26
+ #
27
+ def self.prefix_and_load(prefixes,units)
28
+ [units].flatten.each do |unit|
29
+ unit = Unit.for(unit)
30
+ [prefixes].flatten.each do |prefix|
31
+ prefixed_unit = unit.with_prefix(prefix) rescue unit
32
+ prefixed_unit.load unless prefixed_unit.loaded?
33
+ end
34
+ end
35
+ end
36
+
37
+ # Define a new unit in terms of an already instantiated compound unit. This
38
+ # unit becomes a representation of the compound - without explicitly holding
39
+ # the base units, e.g.
40
+ #
41
+ # Unit::Base.define(Unit.m**2).name #=> "square metre"
42
+ #
43
+ # Unit::Base.define(Unit**3) do |unit|
44
+ # unit.name = "metres cubed"
45
+ # end.name #=> "metres cubed"
46
+ #
47
+ def self.construct(unit,&block)
48
+ new_unit = self.new unit.to_hash
49
+ yield new_unit if block_given?
50
+ return new_unit
51
+ end
52
+
53
+ # Syntactic sugar for defining the known units, enabling the required
54
+ # associated units to be loaded at runtime, e.g.
55
+ #
56
+ # Unit::[Base|SI|NonSI].configure do |config|
57
+ #
58
+ # load :name => :metre, :physical_quantity => :length
59
+ # load :name => 'hectare', :physical_quantity => :area, :factor => 10000
60
+ # load :name => :watt, :physical_quantity => :power, :symbol => 'W'
61
+ #
62
+ # end
63
+ #
64
+ def self.configure &block
65
+ class_eval &block if block
66
+ end
67
+
68
+ attr_accessor :name, :symbol, :label
69
+ attr_accessor :dimensions, :factor
70
+ attr_accessor :acts_as_alternative_unit, :acts_as_equivalent_unit
71
+
72
+ # Create a new Unit::Base instance.
73
+ #
74
+ # Valid options are: :name => The unit name, e.g. :kilometre
75
+ #
76
+ # :dimensions => The physical quantity represented
77
+ # by the unit (e.g. force, mass).
78
+ # This must be recognised as a member
79
+ # of the Dimensions.dimensions array
80
+ #
81
+ # :physical_quantity => Alias for :dimensions
82
+ #
83
+ # :symbol => The unit symbol, e.g. 'kg'
84
+ #
85
+ # :factor => The factor which relates the unit
86
+ # to the SI unit for the same physical
87
+ # quantity. For example the :factor for
88
+ # a foot would be 0.3048, since a foot
89
+ # = 0.3048 m (metre is the SI unit of
90
+ # length). If no factor is set, it is
91
+ # assumed to be 1 - which represents
92
+ # an SI benchmark unit.
93
+ #
94
+ # :scaling => A scaling factor, used only by NonSI
95
+ # temperature units
96
+ #
97
+ # :label => The label used by JScience for the
98
+ # unit
99
+ #
100
+ # The physical quantity option is used to locate the corresponding dimensional
101
+ # representation in the Dimensions class. This dimensions attribute is to
102
+ # provide much of the unit functionality
103
+ #
104
+ def initialize(options=nil)
105
+ if options.is_a? Hash
106
+ @name = options[:name].standardize.singularize.downcase
107
+ options[:dimensions] = options[:dimensions] || options[:physical_quantity]
108
+ if options[:dimensions].is_a? Dimensions
109
+ @dimensions = options[:dimensions]
110
+ elsif options[:dimensions].is_a? String or options[:dimensions].is_a? Symbol
111
+ @dimensions = Dimensions.for options[:dimensions]
112
+ else
113
+ raise InvalidArgumentError, "Unknown physical_quantity specified"
114
+ end
115
+ @factor = options[:factor].nil? ? 1.0 : options[:factor].to_f
116
+ @symbol = options[:symbol].nil? ? nil : options[:symbol].standardize
117
+ @label = options[:label].nil? ? nil : options[:label].to_s
118
+ @acts_as_alternative_unit = true
119
+ @acts_as_equivalent_unit = false
120
+ end
121
+ yield self if block_given?
122
+ valid?
123
+ end
124
+
125
+ # Permits a block to be used, operating on self. This is useful for modifying
126
+ # the attributes of an already instantiated unit, especially when defining
127
+ # units on the basis of operation on existing units for adding specific
128
+ # (rather than derived) names or symbols, e.g.
129
+ #
130
+ # (Unit.pound_force/(Unit.in**2)).operate do |unit|
131
+ # unit.symbol = 'psi'
132
+ # unit.label = 'psi'
133
+ # unit.name = 'pound per square inch'
134
+ # end
135
+ #
136
+ def operate
137
+ yield self if block_given?
138
+ return self if valid?
139
+ end
140
+
141
+ # Load an initialized Unit into the system of known units.
142
+ #
143
+ # If a block is given, the unit can be operated on prior to loading, in a
144
+ # similar to way to the #operate method.
145
+ #
146
+ def load
147
+ yield self if block_given?
148
+ raise InvalidArgumentError, "A unit with the same label: #{self.name}) already exists" if loaded?
149
+ Quantify::Unit.units << self if valid?
150
+ end
151
+
152
+ # Remove from system of known units.
153
+ def unload
154
+ Unit.unload(self.label)
155
+ end
156
+
157
+ # check if an object with the same label already exists
158
+ def loaded?
159
+ Unit.units.any? { |unit| self.has_same_identity_as? unit }
160
+ end
161
+
162
+ def make_canonical
163
+ unload
164
+ load
165
+ end
166
+
167
+ def acts_as_alternative_unit=(value)
168
+ @acts_as_alternative_unit = (value == (true||false) ? value : false)
169
+ make_canonical
170
+ end
171
+
172
+ def acts_as_equivalent_unit=(value)
173
+ @acts_as_equivalent_unit = (value == (true||false) ? value : false)
174
+ make_canonical
175
+ end
176
+
177
+ # Returns the scaling factor for the unit with repsect to its SI alternative.
178
+ #
179
+ # For example the scaling factor for degrees celsius is 273.15, i.e. celsius
180
+ # is a value of 273.15 greater than kelvin (but with no multiplicative factor).
181
+ #
182
+ def scaling
183
+ @scaling || 0.0
184
+ end
185
+
186
+ def has_scaling?
187
+ scaling != 0.0
188
+ end
189
+
190
+ # Describes what the unit measures/represents. This is taken from the
191
+ # @dimensions ivar, being, ultimately an attribute of the assocaited
192
+ # Dimensions object, e.g.
193
+ #
194
+ # Unit.metre.measures #=> :length
195
+ #
196
+ # Unit.J.measures #=> :energy
197
+ #
198
+ def measures
199
+ @dimensions.describe
200
+ end
201
+
202
+ def pluralized_name
203
+ self.name.pluralize
204
+ end
205
+
206
+ # Determine if the unit represents one of the base quantities
207
+ def is_base_unit?
208
+ Dimensions::BASE_QUANTITIES.map(&:standardize).include? self.measures
209
+ end
210
+
211
+ # Determine is the unit is a derived unit - that is, a unit made up of more
212
+ # than one of the base quantities
213
+ #
214
+ def is_derived_unit?
215
+ not is_base_unit?
216
+ end
217
+
218
+ # Determine if the unit is a prefixed unit
219
+ def is_prefixed_unit?
220
+ return true if valid_prefixes.size > 0 and
221
+ self.name =~ /\A(#{valid_prefixes.map(&:name).join("|")})/
222
+ return false
223
+ end
224
+
225
+ # Determine if the unit is one of the units against which all other units
226
+ # of the same physical quantity are defined. These units are almost entirely
227
+ # equivalent to the non-prefixed, SI units, but the one exception is the
228
+ # kilogram, making this method necessary.
229
+ #
230
+ def is_benchmark_unit?
231
+ self.factor == 1.0
232
+ end
233
+
234
+ # Determine is a unit object represents an SI named unit
235
+ def is_si_unit?
236
+ self.is_a? SI
237
+ end
238
+
239
+ # Determine is a unit object represents an NonSI named unit
240
+ def is_non_si_unit?
241
+ self.is_a? NonSI
242
+ end
243
+
244
+ # Determine is a unit object represents an compound unit consisting of SI
245
+ # or non-SI named units
246
+ def is_compound_unit?
247
+ self.is_a? Compound
248
+ end
249
+
250
+ def is_dimensionless?
251
+ self.dimensions.is_dimensionless?
252
+ end
253
+
254
+ # Determine if self is the same unit as another. Similarity is based on
255
+ # representing the same physical quantity (i.e. dimensions) and the same
256
+ # factor and scaling values.
257
+ #
258
+ # Unit.metre.is_same_as? Unit.foot #=> false
259
+ #
260
+ # Unit.metre.is_same_as? Unit.gram #=> false
261
+ #
262
+ # Unit.metre.is_same_as? Unit.metre #=> true
263
+ #
264
+ # The base_units attr of Compound units are not compared. Neither are the
265
+ # names or symbols. This is because we want to recognise cases where units
266
+ # derived from operations and defined as compound units (therefore having
267
+ # compounded names and symbols) are the same as known, named units. For
268
+ # example, if we build a unit for energy using only SI units, we want to
269
+ # recognise this as a joule, rather than a kg m^2 s^-2, e.g.
270
+ #
271
+ # (Unit.kg*Unit.m*Unit.m/Unit.s/Unit.s).is_same_as? Unit.joule
272
+ #
273
+ # #=> true
274
+ #
275
+ def is_same_as?(other)
276
+ [:dimensions,:factor,:scaling].all? do |attr|
277
+ self.send(attr) == other.send(attr)
278
+ end
279
+ end
280
+
281
+ alias :== :is_same_as?
282
+
283
+ # Check if unit has the identity as another, i.e. the same label. This is
284
+ # used to determine if a unit with the same accessors already exists in
285
+ # the module variable @@units
286
+ #
287
+ def has_same_identity_as?(other)
288
+ self.label == other.label and not self.label.nil?
289
+ end
290
+
291
+ # Determine if another unit is an alternative unit for self, i.e. do the two
292
+ # units represent the same physical quantity. This is established by compraing
293
+ # their dimensions attributes. E.g.
294
+ #
295
+ # Unit.metre.is_alternative_for? Unit.foot #=> true
296
+ #
297
+ # Unit.metre.is_alternative_for? Unit.gram #=> false
298
+ #
299
+ # Unit.metre.is_alternative_for? Unit.metre #=> true
300
+ #
301
+ def is_alternative_for?(other)
302
+ other.dimensions == self.dimensions
303
+ end
304
+
305
+ # List the alternative units for self, i.e. the other units which share
306
+ # the same dimensions.
307
+ #
308
+ # The list can be returned containing the alternative unit names, symbols
309
+ # or JScience labels by providing the required format as a symbolized
310
+ # argument.
311
+ #
312
+ # If no format is provide, the full unit objects for all alternative units
313
+ # are returned within the array
314
+ #
315
+ def alternatives(by=nil)
316
+ self.dimensions.units(nil).reject do |unit|
317
+ unit.is_same_as? self or not unit.acts_as_alternative_unit
318
+ end.map(&by)
319
+ end
320
+
321
+ # Returns the SI unit for the same physical quantity which is represented
322
+ # by self, e.g.
323
+ #
324
+ def si_unit
325
+ self.dimensions.si_unit
326
+ end
327
+
328
+ def valid?
329
+ return true if valid_descriptors? and valid_dimensions?
330
+ raise InvalidArgumentError, "Unit definition must include a name, a symbol, a label and physical quantity"
331
+ end
332
+
333
+ def valid_descriptors?
334
+ [:name, :symbol, :label].all? do |attr|
335
+ attribute = send(attr)
336
+ attribute.is_a? String and not attribute.empty?
337
+ end
338
+ end
339
+
340
+ def valid_dimensions?
341
+ @dimensions.is_a? Dimensions
342
+ end
343
+
344
+ # Returns an array representing the valid prefixes for the unit described
345
+ # by self
346
+ #
347
+ # If no argument is given, the array holds instances of Prefix::Base (or
348
+ # subclasses; SI, NonSI...). Alternatively only the names or symbols of each
349
+ # prefix can be returned by providing the appropriate prefix attribute as a
350
+ # symbolized argument, e.g.
351
+ #
352
+ # Unit.m.valid_prefixes #=> [ #<Quantify::Prefix: .. >,
353
+ # #<Quantify::Prefix: .. >,
354
+ # ... ]
355
+ #
356
+ # Unit.m.valid_prefixes :name #=> [ "deca", "hecto", "kilo",
357
+ # "mega", "giga", "tera"
358
+ # ... ]
359
+ #
360
+ # Unit.m.valid_prefixes :symbol #=> [ "da", "h", "k", "M", "G",
361
+ # "T", "P" ... ]
362
+ #
363
+ def valid_prefixes(by=nil)
364
+ return empty_array = [] if self.is_compound_unit?
365
+ return Unit::Prefix.si_prefixes.map(&by) if is_si_unit?
366
+ return Unit::Prefix.non_si_prefixes.map(&by) if is_non_si_unit?
367
+ end
368
+
369
+ # Multiply two units together. This results in the generation of a compound
370
+ # unit.
371
+ #
372
+ def multiply(other)
373
+ options = []
374
+ self.instance_of?(Unit::Compound) ? options += self.base_units : options << self
375
+ other.instance_of?(Unit::Compound) ? options += other.base_units : options << other
376
+ Unit::Compound.new(*options)
377
+ end
378
+
379
+ # Divide one unit by another. This results in the generation of a compound
380
+ # unit.
381
+ #
382
+ # In the event that the new unit represents a known unit, the non-compound
383
+ # representation is returned (i.e. of the SI or NonSI class).
384
+ #
385
+ def divide(other)
386
+ options = []
387
+ self.instance_of?(Unit::Compound) ? options += self.base_units : options << self
388
+
389
+ if other.instance_of? Unit::Compound
390
+ options += other.base_units.map { |base| base.index *= -1; base }
391
+ else
392
+ options << CompoundBaseUnit.new(other,-1)
393
+ end
394
+ Unit::Compound.new(*options)
395
+ end
396
+
397
+ # Raise a unit to a power. This results in the generation of a compound
398
+ # unit, e.g. m^3.
399
+ #
400
+ # In the event that the new unit represents a known unit, the non-compound
401
+ # representation is returned (i.e. of the SI or NonSI class).
402
+ #
403
+ def pow(power)
404
+ return nil if power == 0
405
+ original_unit = self.clone
406
+ if power > 0
407
+ new_unit = self.clone
408
+ (power - 1).times { new_unit *= original_unit }
409
+ elsif power < 0
410
+ new_unit = reciprocalize
411
+ ((power.abs) - 1).times { new_unit /= original_unit }
412
+ end
413
+ return new_unit
414
+ end
415
+
416
+ # Return new unit representing the reciprocal of self, i.e. 1/self
417
+ def reciprocalize
418
+ Unit.unity / self
419
+ end
420
+
421
+ alias :times :multiply
422
+ alias :* :multiply
423
+ alias :/ :divide
424
+ alias :** :pow
425
+
426
+ # Apply a prefix to self. Returns new unit according to the prefixed version
427
+ # of self, complete with modified name, symbol, factor, etc..
428
+ #
429
+ def with_prefix(name_or_symbol)
430
+ if self.name =~ /\A(#{valid_prefixes(:name).join("|")})/
431
+ raise InvalidArgumentError, "Cannot add prefix where one already exists: #{self.name}"
432
+ end
433
+
434
+ prefix = Unit::Prefix.for(name_or_symbol,valid_prefixes)
435
+
436
+ unless prefix.nil?
437
+ new_unit_options = {}
438
+ new_unit_options[:name] = "#{prefix.name}#{self.name}"
439
+ new_unit_options[:symbol] = "#{prefix.symbol}#{self.symbol}"
440
+ new_unit_options[:label] = "#{prefix.symbol}#{self.label}"
441
+ new_unit_options[:factor] = prefix.factor * self.factor
442
+ new_unit_options[:physical_quantity] = self.dimensions
443
+ self.class.new(new_unit_options)
444
+ else
445
+ raise InvalidArgumentError, "Prefix unit is not known: #{prefix}"
446
+ end
447
+ end
448
+
449
+ def with_prefixes(*prefixes)
450
+ [prefixes].map { |prefix| self.with_prefix(prefix) }
451
+ end
452
+
453
+ # Return a hash representation of self containing each unit attribute (i.e
454
+ # each instance variable)
455
+ #
456
+ def to_hash
457
+ hash = {}
458
+ self.instance_variables.each do |var|
459
+ symbol = var.gsub("@","").to_sym
460
+ hash[symbol] = send symbol
461
+ end
462
+ return hash
463
+ end
464
+
465
+ # Enables shorthand for reciprocal of a unit, e.g.
466
+ #
467
+ # unit = Unit.m
468
+ #
469
+ # (1/unit).symbol #=> "m^-1"
470
+ #
471
+ def coerce(object)
472
+ if object.kind_of? Numeric and object == 1
473
+ return Unit.unity, self
474
+ else
475
+ raise InvalidArgumentError, "Cannot coerce #{self.class} into #{object.class}"
476
+ end
477
+ end
478
+
479
+ # Clone self and explicitly clone the associated Dimensions object located
480
+ # at @dimensions.
481
+ #
482
+ # This enables full or 'deep' copies of the already initialized units to be
483
+ # retrieved and manipulated without corrupting the known unit representations.
484
+ # (self.clone makes only a shallow copy, i.e. clones attributes but not
485
+ # referenced objects)
486
+ #
487
+ def initialize_copy(source)
488
+ super
489
+ instance_variable_set("@dimensions", dimensions.clone)
490
+ if self.is_compound_unit?
491
+ instance_variable_set("@base_units", base_units.map {|base| base.clone })
492
+ end
493
+ end
494
+
495
+ # Provides syntactic sugar for several methods. E.g.
496
+ #
497
+ # Unit.metre.to_kilo
498
+ #
499
+ # is equivalent to Unit.metre.with_prefix :kilo.
500
+ #
501
+ # Unit.m.alternatives_by_name
502
+ #
503
+ # is equaivalent to Unit.m.alternatives :name
504
+ #
505
+ def method_missing(method, *args, &block)
506
+ if method.to_s =~ /(to_)(.*)/ and prefix = Prefix.for($2.to_sym)
507
+ return self.with_prefix prefix
508
+ elsif method.to_s =~ /(alternatives_by_)(.*)/ and self.respond_to? $2.to_sym
509
+ return self.alternatives $2.to_sym
510
+ elsif method.to_s =~ /(valid_prefixes_by_)(.*)/ and Prefix::Base.instance_methods.include? $2.to_s
511
+ return self.valid_prefixes $2.to_sym
512
+ end
513
+ super
514
+ end
515
+
516
+ end
517
+ end
518
+ end
@@ -0,0 +1,91 @@
1
+
2
+ module Quantify
3
+ module Unit
4
+ class CompoundBaseUnit
5
+
6
+ # Container class for compound unit base units. Each instance is represented
7
+ # by a unit and an index, i.e. a unit raised to some power. If no index is
8
+ # present, 1 is assumed.
9
+ #
10
+ # Instances of this class can be used to initialize base units, and are the
11
+ # structures which hold base units within compound units
12
+ #
13
+
14
+ attr_accessor :unit, :index
15
+
16
+ def initialize(unit,index=1)
17
+ @unit = Unit.match(unit) || raise(InvalidUnitError, "Base unit not known: #{unit}")
18
+ raise InvalidUnitError, "Base unit cannot be compound unit" if @unit.is_a? Compound
19
+ @index = index
20
+ end
21
+
22
+ def dimensions
23
+ @unit.dimensions ** @index
24
+ end
25
+
26
+ # Only refers to the unit index, rather than the dimensions configuration
27
+ # of the actual unit
28
+ #
29
+ def is_dimensionless?
30
+ @index == 0
31
+ end
32
+
33
+ # Absolute index as names always contain 'per' before denominator units
34
+ def name
35
+ @unit.name.to_power(@index.abs)
36
+ end
37
+
38
+ def pluralized_name
39
+ @unit.pluralized_name.to_power(@index.abs)
40
+ end
41
+
42
+ def symbol
43
+ @unit.symbol.to_s + ( @index.nil? or @index == 1 ? "" : "^#{@index}" )
44
+ end
45
+
46
+ def label
47
+ @unit.label + (@index == 1 ? "" : "^#{@index}")
48
+ end
49
+
50
+ # Reciprocalized version of label, i.e. sign changed. This is used to make
51
+ # a denominator unit renderable in cases where there are no numerator units,
52
+ # i.e. where no '/' appears in the label
53
+ #
54
+ def reciprocalized_label
55
+ @unit.label + (@index == -1 ? "" : "^#{@index * -1}")
56
+ end
57
+
58
+ def factor
59
+ @unit.factor ** @index
60
+ end
61
+
62
+ def is_numerator?
63
+ @index > 0
64
+ end
65
+
66
+ def is_denominator?
67
+ @index < 0
68
+ end
69
+
70
+ def is_si_unit?
71
+ @unit.is_si_unit?
72
+ end
73
+
74
+ def is_non_si_unit?
75
+ @unit.is_non_si_unit?
76
+ end
77
+
78
+ # Physical quantity represented by self. This refers only to the unit, rather
79
+ # than the unit together with the index. Is used to match base units with
80
+ # similar units of same physical quantity
81
+ #
82
+ def measures
83
+ @unit.dimensions.physical_quantity
84
+ end
85
+
86
+ def initialize_copy(source)
87
+ instance_variable_set("@unit", unit.clone)
88
+ end
89
+ end
90
+ end
91
+ end