quantify 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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