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