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