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.
- 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,325 @@
|
|
1
|
+
#! usr/bin/ruby
|
2
|
+
|
3
|
+
module Quantify
|
4
|
+
class Quantity
|
5
|
+
|
6
|
+
# The quantity class represents quantities. Quantities are represented by a
|
7
|
+
# numeric value - of class Numeric - and a unit - of class Unit::Base.
|
8
|
+
#
|
9
|
+
# Quantity objects can be added and subtracted. Mulyiplication and division
|
10
|
+
# by scalar values is also possible, but multiplying and dividing two quantity
|
11
|
+
# objects will be possible when Unit::Compound is implemented.
|
12
|
+
#
|
13
|
+
# Quantity units can be initialized using three methods:
|
14
|
+
#
|
15
|
+
# 1. Conventionally, using:
|
16
|
+
#
|
17
|
+
# Quantity.new <value>, <unit> , e.g. #=> Quantity
|
18
|
+
#
|
19
|
+
# quantity = Quantity.new 3599.8, :joule #=> Quantity
|
20
|
+
#
|
21
|
+
# The unit argument can be an instance of Unit::Base (or inheritors) or
|
22
|
+
# the name, symbol or JScience label of a known unit provided as a string
|
23
|
+
# or a symbol
|
24
|
+
#
|
25
|
+
# 2. Using the class method #parse, which parses a string containing a value
|
26
|
+
# and a unit, e.g.
|
27
|
+
#
|
28
|
+
# Quantity.parse "29 m" #=> Quantity
|
29
|
+
#
|
30
|
+
# Quantity.parse "1025 tonne" #=> Quantity
|
31
|
+
#
|
32
|
+
# 3. Using a shorthand method enabled by extending the Numeric class (see
|
33
|
+
# core_extensions.rb). This allows the name, symbol or JScience label of
|
34
|
+
# a unit to be appended to a Numeric object as a method, e.g.
|
35
|
+
#
|
36
|
+
# 1.metre #=> Quantity
|
37
|
+
#
|
38
|
+
# 68.54321.kilogram #=> Quantity
|
39
|
+
#
|
40
|
+
# 16.Gg #=> Quantity (gigagrams)
|
41
|
+
|
42
|
+
|
43
|
+
|
44
|
+
# Parse a string and return a Quantity object based upon the value and
|
45
|
+
# subseqent unit name, symbol or JScience label
|
46
|
+
def self.parse(string)
|
47
|
+
if quantity = /\A([\d\s.,]+)(\D+.*)\z/.match(string)
|
48
|
+
Quantity.new($1.strip, $2)
|
49
|
+
else
|
50
|
+
raise Quantify::QuantityParseError, "Cannot parse string into value and unit"
|
51
|
+
end
|
52
|
+
rescue Quantify::InvalidArgumentError
|
53
|
+
raise Quantify::QuantityParseError, "Cannot parse string into value and unit"
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.configure &block
|
57
|
+
self.class_eval &block if block
|
58
|
+
end
|
59
|
+
|
60
|
+
attr_accessor :value, :unit
|
61
|
+
|
62
|
+
# Initialize a new Quantity object. Two arguments are required: a value and
|
63
|
+
# a unit. The unit can be a an instance of Unit::Base or the name, symbol or
|
64
|
+
# JScience label of a known (or derivable through know units and prefixes)
|
65
|
+
# unit
|
66
|
+
def initialize(value, unit)
|
67
|
+
@value = value.to_f
|
68
|
+
@unit = Unit.for(unit)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns a description of what the quantity describes, based upon the physica
|
72
|
+
# quantity which is represented by the Dimensions object in the Quantity unit.
|
73
|
+
# e.g.
|
74
|
+
#
|
75
|
+
# Quantity.parse( )"25 yr").represents #=> :time
|
76
|
+
#
|
77
|
+
# 1.foot.represents #=> :length
|
78
|
+
#
|
79
|
+
# Quantity.new(123.456, :degree_celsius).represents
|
80
|
+
# #=> :temperature
|
81
|
+
def represents
|
82
|
+
self.unit.measures
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a string representation of the quantity, using the unit symbol
|
86
|
+
def to_s format=:symbol
|
87
|
+
if format == :name
|
88
|
+
if self.value == 1 or self.value == -1
|
89
|
+
"#{self.value} #{self.unit.name}"
|
90
|
+
else
|
91
|
+
"#{self.value} #{self.unit.pluralized_name}"
|
92
|
+
end
|
93
|
+
else
|
94
|
+
"#{self.value} #{self.unit.send format}"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Converts self into a quantity using the unit provided as an argument. The
|
99
|
+
# new unit must represent the same physical quantity, i.e. have the same
|
100
|
+
# dimensions, e.g.
|
101
|
+
#
|
102
|
+
# Quantity.parse("12 yd").to(:foot).to_s #=> "36 ft"
|
103
|
+
#
|
104
|
+
# 1000.kilogram.to(:tonne).to_s #=> "1 t"
|
105
|
+
#
|
106
|
+
# The method #method_missing provides some syntactic sugar for the new unit to
|
107
|
+
# be provided as part of the method name, based around /to_(<unit>)/, e.g.
|
108
|
+
#
|
109
|
+
# 200.cm.to_metre.to_s #=> "1 t"
|
110
|
+
#
|
111
|
+
# The unit value is converted to the corresponding value for the same quantity
|
112
|
+
# in terms of the new unit.
|
113
|
+
#
|
114
|
+
def to(new_unit)
|
115
|
+
new_unit = Unit.for new_unit
|
116
|
+
if is_basic_conversion_with_scalings? new_unit
|
117
|
+
Quantity.new(value,unit).conversion_with_scalings! new_unit
|
118
|
+
elsif self.unit.is_alternative_for? new_unit
|
119
|
+
Quantity.new(value,unit).convert_to_equivalent_unit! new_unit
|
120
|
+
elsif self.unit.is_compound_unit?
|
121
|
+
Quantity.new(value,unit).convert_compound_unit_to_non_equivalent_unit! new_unit
|
122
|
+
else
|
123
|
+
nil # raise? or ...
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def is_basic_conversion_with_scalings?(new_unit)
|
128
|
+
return true if (self.unit.has_scaling? or new_unit.has_scaling?) and
|
129
|
+
not self.unit.is_compound_unit? and
|
130
|
+
not new_unit.is_compound_unit?
|
131
|
+
return false
|
132
|
+
end
|
133
|
+
|
134
|
+
# Conversion where both units (including compound units) are of precisely
|
135
|
+
# equivalent dimensions, i.e. direct alternatives for one another. Where
|
136
|
+
# previous unit is a compound unit, new unit must be cancelled by all original
|
137
|
+
# base units
|
138
|
+
#
|
139
|
+
def convert_to_equivalent_unit!(new_unit)
|
140
|
+
old_unit = unit
|
141
|
+
self.multiply!(Unit.ratio new_unit, old_unit)
|
142
|
+
old_base_units = old_unit.base_units.map { |base| base.unit } if old_unit.is_compound_unit?
|
143
|
+
self.cancel_base_units!(*old_base_units || old_unit)
|
144
|
+
end
|
145
|
+
|
146
|
+
def conversion_with_scalings!(new_unit)
|
147
|
+
@value = (((self.value + self.unit.scaling) * self.unit.factor) / new_unit.factor) - new_unit.scaling
|
148
|
+
@unit = new_unit
|
149
|
+
return self
|
150
|
+
end
|
151
|
+
|
152
|
+
# Conversion where self is a compound unit, and new unit is not an alternative
|
153
|
+
# to the whole compound but IS an alternative to one or more of the base units,
|
154
|
+
# e.g.,
|
155
|
+
#
|
156
|
+
# Unit.kilowatt_hour.to :second #=> 'kilowatt second'
|
157
|
+
#
|
158
|
+
def convert_compound_unit_to_non_equivalent_unit!(new_unit)
|
159
|
+
self.unit.base_units.select do |base|
|
160
|
+
base.unit.is_alternative_for? new_unit
|
161
|
+
end.inject(self) do |quantity,base|
|
162
|
+
factor = Unit.ratio(new_unit**base.index, base.unit**base.index)
|
163
|
+
quantity.multiply!(factor).cancel_base_units!(base.unit)
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
# Converts a quantity to the equivalent quantity using only SI units
|
168
|
+
def to_si
|
169
|
+
if self.unit.is_compound_unit?
|
170
|
+
Quantity.new(value,unit).convert_compound_unit_to_si!
|
171
|
+
else
|
172
|
+
self.to(unit.si_unit)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def convert_compound_unit_to_si!
|
177
|
+
until self.unit.is_si_unit? do
|
178
|
+
unit = self.unit.base_units.find do |base|
|
179
|
+
!base.unit.is_si_unit?
|
180
|
+
end.unit
|
181
|
+
self.convert_compound_unit_to_non_equivalent_unit!(unit.si_unit)
|
182
|
+
end
|
183
|
+
return self
|
184
|
+
end
|
185
|
+
|
186
|
+
# Quantities must be of the same dimension in order to operate. If they are
|
187
|
+
# represented by different units (but represent the same physical quantity)
|
188
|
+
# the second quantity is converted into the unit belonging to the first unit
|
189
|
+
# and the addition is completed
|
190
|
+
#
|
191
|
+
def add_or_subtract!(operator,other)
|
192
|
+
if other.is_a? Quantity
|
193
|
+
other = other.to(unit) if other.unit.is_alternative_for?(unit)
|
194
|
+
if self.unit == other.unit
|
195
|
+
@value = value.send operator, other.value
|
196
|
+
return self
|
197
|
+
else
|
198
|
+
raise Quantify::InvalidObjectError "Cannot add or subtract Quantities with different dimensions"
|
199
|
+
end
|
200
|
+
else
|
201
|
+
raise Quantify::InvalidObjectError "Cannot add or subtract non-Quantity objects"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def multiply_or_divide!(operator,other)
|
206
|
+
if other.kind_of? Numeric
|
207
|
+
@value = value.send(operator,other)
|
208
|
+
return self
|
209
|
+
elsif other.kind_of? Quantity
|
210
|
+
@unit = unit.send(operator,other.unit).or_equivalent
|
211
|
+
@value = value.send(operator,other.value)
|
212
|
+
return self
|
213
|
+
else
|
214
|
+
raise Quantify::InvalidArgumentError "Cannot multiply or divide a Quantity by a non-Quantity or non-Numeric object"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def pow!(power)
|
219
|
+
raise InvalidArgumentError, "Argument must be an integer" unless power.is_a? Integer
|
220
|
+
@value = value ** power
|
221
|
+
@unit = unit ** power
|
222
|
+
return self
|
223
|
+
end
|
224
|
+
|
225
|
+
def add!(other)
|
226
|
+
add_or_subtract!(:+, other)
|
227
|
+
end
|
228
|
+
|
229
|
+
def subtract!(other)
|
230
|
+
add_or_subtract!(:-, other)
|
231
|
+
end
|
232
|
+
|
233
|
+
def multiply!(other)
|
234
|
+
multiply_or_divide!(:*, other)
|
235
|
+
end
|
236
|
+
|
237
|
+
def divide!(other)
|
238
|
+
multiply_or_divide!(:/, other)
|
239
|
+
end
|
240
|
+
|
241
|
+
def add(other)
|
242
|
+
Quantity.new(value,unit).add!(other)
|
243
|
+
end
|
244
|
+
|
245
|
+
def subtract(other)
|
246
|
+
Quantity.new(value,unit).subtract!(other)
|
247
|
+
end
|
248
|
+
|
249
|
+
def multiply(other)
|
250
|
+
Quantity.new(value,unit).multiply!(other)
|
251
|
+
end
|
252
|
+
|
253
|
+
def divide(other)
|
254
|
+
Quantity.new(value,unit).divide!(other)
|
255
|
+
end
|
256
|
+
|
257
|
+
def pow(power)
|
258
|
+
Quantity.new(value,unit).pow!(power)
|
259
|
+
end
|
260
|
+
|
261
|
+
alias :times :multiply
|
262
|
+
alias :* :multiply
|
263
|
+
alias :+ :add
|
264
|
+
alias :- :subtract
|
265
|
+
alias :/ :divide
|
266
|
+
alias :** :pow
|
267
|
+
|
268
|
+
def rationalize_units
|
269
|
+
return self unless unit.is_a? Unit::Compound
|
270
|
+
self.to unit.rationalize_base_units
|
271
|
+
end
|
272
|
+
|
273
|
+
def cancel_base_units!(*units)
|
274
|
+
@unit = unit.cancel_base_units!(*units) if unit.is_compound_unit?
|
275
|
+
return self
|
276
|
+
end
|
277
|
+
|
278
|
+
# Round the value attribute to the specified number of decimal places. If no
|
279
|
+
# argument is given, the value is rounded to NO decimal places, i.e. to an
|
280
|
+
# integer
|
281
|
+
#
|
282
|
+
def round!(decimal_places=0)
|
283
|
+
factor = ( decimal_places == 0 ? 1 : 10.0 ** decimal_places )
|
284
|
+
@value = (@value * factor).round / factor
|
285
|
+
self
|
286
|
+
end
|
287
|
+
|
288
|
+
# Similar to #round! but returns new Quantity instance rather than rounding
|
289
|
+
# in place
|
290
|
+
#
|
291
|
+
def round(decimal_places=0)
|
292
|
+
rounded_quantity = Quantity.new @value, @unit
|
293
|
+
rounded_quantity.round! decimal_places
|
294
|
+
end
|
295
|
+
|
296
|
+
# Enables shorthand for reciprocal of quantity, e.g.
|
297
|
+
#
|
298
|
+
# quantity = 2.m
|
299
|
+
#
|
300
|
+
# (1/quantity).to_s :name #=> "0.5 per metre"
|
301
|
+
#
|
302
|
+
def coerce(object)
|
303
|
+
if object.kind_of? Numeric
|
304
|
+
return Quantity.new(object, Unit.unity), self
|
305
|
+
else
|
306
|
+
raise InvalidArgumentError, "Cannot coerce #{self.class} into #{object.class}"
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
# Dynamic method for converting to another unit, e.g
|
311
|
+
#
|
312
|
+
# 2.ft.to_metre.to_s #=> "0.6096 m"
|
313
|
+
#
|
314
|
+
# 30.degree_celsius.to_K.to_s :name #=> "303.15 kelvins"
|
315
|
+
#
|
316
|
+
def method_missing(method, *args, &block)
|
317
|
+
if method.to_s =~ /(to_)(.*)/
|
318
|
+
to($2)
|
319
|
+
else
|
320
|
+
raise NoMethodError, "Undefined method '#{method}' for #{self}:#{self.class}"
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
end
|
325
|
+
end
|