quantify 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/COPYING +20 -0
- data/Examples.rb +104 -0
- data/README +7 -0
- data/lib/quantify/config.rb +379 -0
- data/lib/quantify/core_extensions.rb +63 -0
- data/lib/quantify/dimensions.rb +523 -0
- data/lib/quantify/exception.rb +21 -0
- data/lib/quantify/inflections.rb +63 -0
- data/lib/quantify/quantify.rb +37 -0
- data/lib/quantify/quantity.rb +325 -0
- data/lib/quantify/unit/base_unit.rb +518 -0
- data/lib/quantify/unit/compound_base_unit.rb +91 -0
- data/lib/quantify/unit/compound_unit.rb +321 -0
- data/lib/quantify/unit/non_si_unit.rb +20 -0
- data/lib/quantify/unit/prefix/base_prefix.rb +42 -0
- data/lib/quantify/unit/prefix/non_si_prefix.rb +10 -0
- data/lib/quantify/unit/prefix/prefix.rb +73 -0
- data/lib/quantify/unit/prefix/si_prefix.rb +10 -0
- data/lib/quantify/unit/si_unit.rb +10 -0
- data/lib/quantify/unit/unit.rb +217 -0
- data/lib/quantify.rb +26 -0
- data/spec/dimension_spec.rb +294 -0
- data/spec/quantity_spec.rb +250 -0
- data/spec/unit_spec.rb +687 -0
- metadata +103 -0
@@ -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,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
|