quantity 0.0.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/README CHANGED
@@ -1,5 +1,209 @@
1
1
  Quantity.rb: Units and Quantities for Ruby
2
2
  ==========================================
3
+ Quantity.rb provides first-class support for units and quantities in Ruby.
4
+ The right abstractions for true quantity representation and complex conversions.
5
+ Hopefully this readme will be all you need, but [there are yardocs](http://quantity.rubyforge.org)
6
+
7
+ ## Overview
8
+ require 'quantity/all'
9
+ 1.meter #=> 1 meter
10
+ 1.meter.to_feet #=> 3.28083... foot
11
+ c = 299792458.meters / 1.second #=> 299792458 meter/second
12
+
13
+ newton = 1.meter * 1.kilogram / 1.second**2 #=> 1 meter*kilogram/second^2
14
+ newton.to_feet #=> 3.28083989501312 foot*kilogram/second^2
15
+ newton.convert(:feet) #=> 3.28083989501312 foot*kilogram/second^2
16
+ jerk_newton / 1.second #=> 1 meter*kilogram/second^3
17
+ jerk_newton * 1.second == newton #=> true
18
+
19
+ mmcubed = 1.mm.cubed #=> 1 millimeter^3
20
+ mmcubed * 1000 == 1.milliliter #=> true
21
+
22
+ [1.meter, 1.foot, 1.inch].sort #=> [1 inch, 1 foot, 1 meter]
23
+
24
+ m_to_f = Quantity::Unit.for(:meter).convert_proc(:feet)
25
+ m_to_f.call(1) #=> 3.28083... (or a Rational)
26
+
27
+ Quantity.rb provides full-featured support for quantities, units, and
28
+ dimensions in Ruby. Some terminology:
29
+
30
+ * Quantity: An amount of a unit, such as 12 meters.
31
+ * Unit: An amount of a given dimension to be measured, such as 'meter'
32
+ * Dimension: Some base quantity to be measured, such as 'length'
33
+
34
+ Quantities perform complete mathematical operations over their units,
35
+ including `+`, `-`, `\*`, `/`, `\*`\*`, `%`, `abs`, `divmod`, `<=>`, and negation. Units
36
+ and the dimensions they measure are fully represented and support
37
+ `\*` and `/`.
38
+
39
+ Quantity extends Numeric to allow easy creation of quantities, but there
40
+ are direct interfaces to the library as well.
41
+
42
+ 1.meter == Quantity.new(1,Quantity::Unit.for(:meter))
43
+ 1.meter.unit == Quantity::Unit.for(:meter)
44
+ 1.meter.unit.dimension == Quantity::Dimension.for(:length)
45
+
46
+ See the units section for supported units, and how to add your own.
47
+
48
+ Quantities are first-class citizens which do a fair job of imitating
49
+ Numeric. Quantities support coerce, and can thus be used in almost
50
+ any situation a numeric can:
51
+
52
+ 2.5 + 5.meters # => 7.5 meters
53
+ 5 == 5.meters # => true
54
+
55
+ ## Status and TODO
56
+ Quantity.rb is not ready for production use for some areas, but should be
57
+ fine for simple conversion use cases. If it breaks, please email the
58
+ author for a full refund.
59
+
60
+ Specifically broken in this version are some operations on named
61
+ higher dimensions:
62
+
63
+ 1.liter / 1.second #=> should be 1 liter/second, but explodes
64
+ 1.liter.convert(:'mm^3') / 1.second #=> 1000000.0 millimeter^3/second
65
+
66
+ If you just work with units derived from the base dimensions, there aren't
67
+ known bugs. Please add a spec if you find one.
68
+
69
+ ### TODO
70
+ * Lots more units are planned.
71
+ * BigDecimal support a la Rational.
72
+ * Supporting lambdas for unit values
73
+ * BigDecimal/Rational compatible values for existing units
74
+ * Some DSL sugar for adding derived dimension units
75
+
76
+ ## Units
77
+ Quantity.rb comes with a sizable collection of units, but still needs significant expansion.
78
+
79
+ A number of base unit sets exist:
80
+ require 'quantity/all' #=> load everything. uses US versions of foot, lb, etc
81
+ require 'quantity/systems/si' #=> load SI
82
+ require 'quantity/systems/us' #=> load US versions of foot, lb, etc
83
+ require 'quantity/systems/imperial' #=> load British versions of foot, lb, etc
84
+ require 'quantity/systems/information' #=> bits, bytes, and all that
85
+ require 'quantity/systems/enumerable' #=> countable things--dozen, score, etc
86
+
87
+ Note that US and Imperial conflict with each other. Loading both is unsupported.
88
+
89
+ Adding your own units is simple:
90
+
91
+ Quantity::Unit.add_unit :furlong, :length, 201168, :furlongs
92
+ 1.furlong #=> 1 furlong
93
+
94
+ 201168 represents 1 furlong in millimeters. Each base dimension, such as length, time,
95
+ current, temperature, etc, is represented by a reference unit, which is generally the
96
+ milli-version of the SI unit referencing that domain. [NIST](http://physics.nist.gov/cuu/Units/units.html)
97
+ has an explanation of how the SI system works, and how all units are actually derived from
98
+ very few.
99
+
100
+ All units for derived dimensions used the derived reference unit. For example, length
101
+ is referenced to millimeters, so each unit of length is defined in terms of them:
102
+
103
+ Quantity::Unit.add_unit :meter, :length, 1000
104
+ Quantity::Unit.add_unit :millimeter, :length, 1, :mm
105
+
106
+ Thus, the base unit for volume is 1 mm^3:
107
+ volume = Quantity::Dimension.add_dimension length**3, :volume
108
+ ml = Quantity::Dimension.add_unit :milliliter, :volume, 1000, :ml, :milliliters
109
+ 1.mm**3 * 1000 == 1.milliliter #=> true
110
+
111
+ See the bugs section for some current issues using units defined on derived dimensions.
112
+
113
+ The full list of included base dimensions and their reference units:
114
+ * :length => :millimeter
115
+ * :time => :millisecond
116
+ * :current => :milliampere
117
+ * :luminosity => :millicandela
118
+ * :substance => :millimole
119
+ * :temperature => :millikelvin
120
+ * :mass => :milligram
121
+ * :information => :bit # use :megabytes and :mebibytes
122
+ * :quantity => :item # for countable quantities. units include 1.dozen, for example
123
+ * :currency => :dollar # These are not really implemented yet
124
+
125
+ To determine the base unit for a derived dimension, you can use Quantity.rb itself:
126
+
127
+ force = Quantity::Dimension.for(:force)
128
+ newton = 1.meter * 1.kilogram / 1.second**2
129
+ newton.measures == force #=> true
130
+ newton_value = newton.to_mm.to_mg.to_ms #=> 1000.0 millimeter*milligram/millisecond^2
131
+
132
+ Thus, a newton would be 1000 when added specifically:
133
+
134
+ Quantity::Unit.add_unit :newton, :force, 1000, :newtons
135
+ 1.newton == newton #=> true
136
+
137
+ ## Dimensions
138
+ A dimension is a measurable thing, often called a 'base quantity' in scientific literature,
139
+ but Quantity.rb specifically avoids that nomenclature, reserving 'quantity' for the class
140
+ representing a unit and a value. As always, [wikipedia has the answers.](http://en.wikipedia.org/wiki/Physical_quantity)
141
+
142
+ Dimensions are not very useful by themselves, but you can play with them
143
+ if you want.
144
+
145
+ length = Quantity::Dimension.for(:length)
146
+ time = Quantity::Dimension.for(:time)
147
+ speed = length / time
148
+
149
+ A number of dimensions are enabled by default (see dimension/base.rb).
150
+
151
+ A DSL of sorts is provided for declaring dimensions:
152
+
153
+ length = Quantity::Dimension.add_dimenson :length
154
+ area = Quantity::Dimension.add_dimension length**2, :area
155
+
156
+ length = Quantity::Dimension.for(:length)
157
+ area = Quantity::Dimension.for(:area)
158
+ area == length * length #=> true
159
+
160
+ Quantity::Dimension is extended with empty subclasses for some base dimensions,
161
+ so you can do pattern patching on the class:
162
+
163
+ case 1.meter.measures
164
+ when Quantity::Dimension::Length
165
+ puts "I am printed"
166
+ end
167
+
168
+ ## I just want to convert things, this is all just too much
169
+ Quantity.rb provides you the ability to intuitively create the conversions
170
+ your application needs, and then bypass the rest of the library.
171
+
172
+ m_to_f = 1.meter.measures.convert_proc(:feet)
173
+ m_to_f.call(5) # => 5 meters in feet
174
+
175
+ This Proc object has been broken down into a single division; it no longer references
176
+ any units, dimensions, or quantities. It's hard to be faster in pure Ruby.
177
+
178
+ ### On precision and speed
179
+
180
+ By default, whatever Numeric you are using will be the stored value for the
181
+ quantity.
182
+
183
+ 5.meters
184
+ Rational(5).meters
185
+ 5.0.meters
186
+
187
+ This value will be held. However, divisions are required for conversions,
188
+ and the default is to force values into floats.
189
+
190
+ If accuracy is required, just require 'rational'. If Rational is defined,
191
+ you'll get rationals instead of divided floats everywhere. In tests, this
192
+ is an order of magnitude slower.
193
+
194
+ ## 'Why' and previous work
195
+ This is by no means the first unit conversion/quantity library for Ruby, but
196
+ none of the existing ones scratched my itch just right. My goal is that this will
197
+ be the last one I (and you) need. The abstractions go all the way down, and
198
+ any conceivable conversion or munging functionality should be buildable on top
199
+ of this.
200
+
201
+ Inspiration comes from:
202
+
203
+ * [Quanty](http://narray.rubyforge.org/quanty/quanty-en.html)
204
+ Why oh why did they involve yacc?
205
+ * [Ruby Units](http://ruby-units.rubyforge.org/ruby-units/)
206
+ * [Alchemist](http://github.com/toastyapps/alchemist)
3
207
 
4
208
  Authors
5
209
  -------
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.0.0
1
+ 0.1.1
@@ -1,17 +1,376 @@
1
1
  require 'quantity/version'
2
+ require 'quantity/dimension'
3
+ require 'quantity/dimension/base'
4
+ require 'quantity/unit'
5
+ require 'quantity/systems/si'
6
+ require 'quantity/systems/us'
2
7
 
8
+ #
9
+ # A quantity of something. Quantities are immutable; conversions and other operations return
10
+ # a new quantity.
11
+ #
12
+ # ## General Use
13
+ # require 'quantity/all'
14
+ #
15
+ # 12.meters #=> Quantity
16
+ # 12.meters.measures #=> :length
17
+ # 12.meters.units #=> :meters
18
+ # 12.meters.unit #=> Quantity::Unit::Length
19
+ # 12.meters.in_centimeters == 1200.centimeters #=> true
20
+ # 12.meters == 12 #=> true
21
+ # 12.meters == 12.centimeters #=> false
22
+ # 12.meters + 5.centimeters == 12.05.meters #=> true
23
+ # 12.meters.in_picograms #=> raises ArgumentError
24
+ #
25
+ # ## Derived Units
26
+ # require 'quantity/si'
27
+ # speed_of_light = 299_752_458.meters / 1.second #=>Quantity::Unit::Derived
28
+ # speed_of_light.measures #=> "meters per second"
29
+ # speed_of_light.units #=> "meters per second"
30
+ #
31
+ # ludicrous_speed = speed_of_light * 1000
32
+ # ludicrous_speed.measures #=> "meters per second" #TODO: velocity, accleration ?
33
+ # ludicrous_speed.to_s #=> "299752458000 meters per second"
34
+ #
35
+ # If the default to_s isn't what you want, you can buld it with 12.meters.value and 12.meters.units
36
+ #
37
+ # @see Quantity::Unit
3
38
  class Quantity
39
+ include Comparable
4
40
  autoload :Unit, 'quantity/unit'
5
41
 
6
- undef_method *(instance_methods - %w(__id__ __send__ __class__ __eval__ instance_eval inspect))
42
+ #undef_method *(instance_methods - %w(__id__ __send__ __class__ __eval__ instance_eval inspect should))
7
43
 
44
+ # User-visible value, i.e. 2.meters.value == 2
8
45
  attr_reader :value
46
+
47
+ # Unit of measurement
9
48
  attr_reader :unit
10
49
 
11
- ##
12
- # @param [Numeric] value
13
- # @param [Unit] unit
14
- def initialize(value, unit)
15
- @value, @unit = value, unit
50
+ # This quantity in terms of the reference value, declared by fiat for everything measurable
51
+ attr_reader :reference_value
52
+
53
+ #
54
+ # Initialize a new, immutable quantity
55
+ # @overload initialize(value, unit, options)
56
+ # @param [Numeric] value
57
+ # @param [Unit] unit
58
+ # @return [Quantity]
59
+ #
60
+ # @overload initialize(options)
61
+ # Only one of value or reference value can be used, if both are given, reference
62
+ # value will be used.
63
+ # @param [Hash{Symbol => Object}] options
64
+ # @option options [Numeric] :value Visible value
65
+ # @option options [Numeric] :reference_value Reference value
66
+ # @option options [Symbol Unit] :unit Units
67
+ # @return [Quantity]
68
+ #
69
+ def initialize(value, unit = nil )
70
+ case value
71
+ when Hash
72
+ @unit = Unit.for(value[:unit])
73
+ @reference_value = value[:reference_value] || (value[:value] * @unit.value)
74
+ @value = @unit.value_for(@reference_value) #dimension.reference.convert_proc(@unit).call(@reference_value)
75
+ #@value = @unit.convert_proc(@unit).call(@reference_value)
76
+ when Numeric
77
+ @unit = Unit.for(unit)
78
+ if @unit.nil?
79
+ @unit = Unit.from_string_form(unit)
80
+ end
81
+ @value = value
82
+ @reference_value = value * @unit.value
83
+ end
84
+ end
85
+
86
+ # String version of this quantity
87
+ # @param [String] format Format for sprintf, will be given
88
+ # @return [String]
89
+ def to_s
90
+ @unit.s_for(value)
91
+ end
92
+
93
+ # What this measures
94
+ # @return [Symbol String] What this measures. Derived types will be a string
95
+ def measures
96
+ @unit.dimension
97
+ end
98
+
99
+ # Units of measurement
100
+ # @return [Symbol String] Units of measurement. Derived types will be a string
101
+ def units
102
+ @unit.name
103
+ end
104
+
105
+ # Abs implementation
106
+ # @return [Quantity]
107
+ def abs
108
+ if @reference_value < 0
109
+ -self
110
+ else
111
+ self
112
+ end
113
+ end
114
+
115
+ # Ruby coercion. Allows things like 2 + 5.meters
116
+ # @return [Quantity, Quantity]
117
+ def coerce(other)
118
+ if other.class == @value.class
119
+ [Quantity.new(other, @unit),self]
120
+ elsif defined?(Rational) && (@value.is_a?(Fixnum)) && (other.is_a?(Fixnum))
121
+ [Quantity.new(Rational(other), @unit), self]
122
+ elsif defined?(Rational) && (other.is_a?(Rational))
123
+ [Quantity.new(other, @unit), self]
124
+ else
125
+ [Quantity.new(other.to_f, @unit),Quantity.new(@value.to_f, @unit)]
126
+ end
127
+ end
128
+
129
+
130
+ # Addition. Add two quantities of the same type. Do not need to have the same units.
131
+ # @param [Quantity Numeric] other
132
+ # @return [Quantity]
133
+ def +(other)
134
+ if (other.is_a?(Numeric))
135
+ Quantity.new(@value + other, @unit)
136
+ elsif(other.is_a?(Quantity) && @unit.dimension == other.unit.dimension)
137
+ Quantity.new({:unit => @unit,:reference_value => @reference_value + other.reference_value})
138
+ else
139
+ raise ArgumentError,"Cannot add #{self} to #{other}"
140
+ end
141
+ end
142
+
143
+ # Subtraction. Subtract a quantity from another of the same type. They do not need
144
+ # to share units.
145
+ # @param [Quantity Numeric] other
146
+ # @return [Quantity]
147
+ def -(other)
148
+ if (other.is_a?(Numeric))
149
+ Quantity.new(@value - other, @unit)
150
+ elsif(other.is_a?(Quantity) && @unit.dimension == other.unit.dimension)
151
+ Quantity.new({:unit => @unit,:reference_value => @reference_value - other.reference_value})
152
+ else
153
+ raise ArgumentError, "Cannot subtract #{other} from #{self}"
154
+ end
155
+ end
156
+
157
+ # Comparison. Compare this to another quantity or numeric. Compared to a numeric,
158
+ # this will assume a numeric of the same unit as self.
159
+ # @param [Quantity Numeric] other
160
+ # @return [-1 0 1]
161
+ def <=>(other)
162
+ if (other.is_a?(Numeric))
163
+ @value <=> other
164
+ elsif(other.is_a?(Quantity) && measures == other.measures)
165
+ @reference_value <=> other.reference_value
166
+ else
167
+ nil
168
+ end
169
+ end
170
+
171
+ # Type-aware equality
172
+ # @param [Any]
173
+ # @return [Boolean]
174
+ def eql?(other)
175
+ other.is_a?(Quantity) && other.units == units && self == other
176
+ end
177
+
178
+ # Multiplication.
179
+ # @param [Numeric, Quantity]
180
+ # @return [Quantity]
181
+ def *(other)
182
+ if (other.is_a?(Numeric))
183
+ Quantity.new(@value * other, @unit)
184
+ elsif(other.is_a?(Quantity))
185
+ Quantity.new({:unit => other.unit * @unit, :reference_value => @reference_value * other.reference_value})
186
+ else
187
+ raise ArgumentError, "Cannot multiply #{other} with #{self}"
188
+ end
189
+ end
190
+
191
+ # Division
192
+ # @param [Numeric, Quantity]
193
+ # @return [Quantity]
194
+ def /(other)
195
+ if (other.is_a?(Numeric))
196
+ Quantity.new(@value / other, @unit)
197
+ elsif(other.is_a?(Quantity))
198
+ ref = nil
199
+ if defined?(Rational) && (@value.is_a?(Fixnum)) && (other.is_a?(Fixnum))
200
+ ref = Rational(@reference_value,other.reference_value)
201
+ elsif defined?(Rational) && (@value.is_a?(Rational)) && (other.is_a?(Rational))
202
+ ref = @reference_value / other.reference_value
203
+ else
204
+ ref = @reference_value / other.reference_value.to_f
205
+ end
206
+ Quantity.new({:unit => @unit / other.unit, :reference_value => ref})
207
+ else
208
+ raise ArgumentError, "Cannot multiply #{other} with #{self}"
209
+ end
210
+ end
211
+
212
+ # Exponentiation. Quantities cannot be raised to negative or fractional powers, only
213
+ # positive Fixnum.
214
+ # @param [Numeric]
215
+ # @return [Quantity]
216
+ def **(power)
217
+ unless power.is_a?(Fixnum) && power > 0
218
+ raise ArgumentError, "Quantities can only be raised to fixed powers (given #{power})"
219
+ end
220
+ if power == 1
221
+ self
222
+ else
223
+ self * self**(power - 1)
224
+ end
225
+ end
226
+
227
+ # Square the units of this quantity
228
+ # @example
229
+ # 4.meters.squared == Quantity.new(4.'m^2')
230
+ # @return [Quantity]
231
+ def squared
232
+ Quantity.new(@value, @unit * @unit)
233
+ end
234
+
235
+ # Cube the units of this quantity
236
+ # @example
237
+ # 4.meters.cubed == Quantity.new(4.'m^3')
238
+ # @return [Quantity]
239
+ def cubed
240
+ Quantity.new(@value, @unit * @unit * @unit)
241
+ end
242
+
243
+ # Mod
244
+ # @return [Quantity]
245
+ def %(other)
246
+ if (other.is_a?(Numeric))
247
+ Quantity.new(@value % other, @unit)
248
+ elsif(other.is_a?(Quantity) && self.measures == other.measures)
249
+ Quantity.new({:unit => @unit, :reference_value => @reference_value % other.reference_value})
250
+ else
251
+ raise ArgumentError, "Cannot modulo #{other} with #{self}"
252
+ end
253
+ end
254
+
255
+ # Both names for modulo
256
+ alias_method :modulo, :%
257
+
258
+ # Negation
259
+ # @return [Quantity]
260
+ def -@
261
+ Quantity.new({:unit => @unit, :reference_value => @reference_value * -1})
262
+ end
263
+
264
+ # Unary + (self)
265
+ # @return [Quantity]
266
+ def +@
267
+ self
268
+ end
269
+
270
+ # Integer representation
271
+ # @return [Fixnum]
272
+ def to_i
273
+ @value.to_i
274
+ end
275
+
276
+ # Float representation
277
+ # @return [Float]
278
+ def to_f
279
+ @value.to_f
280
+ end
281
+
282
+ # Round this value to the nearest integer
283
+ # @return [Quantity]
284
+ def round
285
+ Quantity.new(@value.round, @unit)
286
+ end
287
+
288
+ # Truncate this value to an integer
289
+ # @return [Quantity]
290
+ def truncate
291
+ Quantity.new(@value.truncate, @unit)
292
+ end
293
+
294
+ # Largest integer quantity less than or equal to this
295
+ # @return [Quantity]
296
+ def floor
297
+ Quantity.new(@value.floor, @unit)
298
+ end
299
+
300
+ # Smallest integer quantity greater than or equal to this
301
+ # @return [Quantity]
302
+ def ceil
303
+ Quantity.new(@value.ceil, @unit)
304
+ end
305
+
306
+ # Divmod
307
+ # @return [Quantity,Quantity]
308
+ def divmod(other)
309
+ if (other.is_a?(Numeric))
310
+ (q, r) = @value.divmod(other)
311
+ [Quantity.new(q,@unit),Quantity.new(r,@unit)]
312
+ elsif (other.is_a?(Quantity) && measures == other.measures)
313
+ (q, r) = @value.divmod(other.value)
314
+ [Quantity.new(q,@unit),Quantity.new(r,@unit)]
315
+ else
316
+ raise ArgumentError, "Cannot divmod #{other} with #{self}"
317
+ end
318
+ end
319
+
320
+ # Returns true if self has a zero value
321
+ # @return [Boolean]
322
+ def zero?
323
+ @value.zero?
324
+ end
325
+
326
+ # Convert to another unit of measurement.
327
+ # For most uses, Quantity#to_<unit> is what you want, but this can be handy
328
+ # for variable units.
329
+ # @param [Unit Symbol]
330
+ def convert(to)
331
+ Quantity.new({:unit => @unit.convert(to), :reference_value => @reference_value})
332
+ end
333
+
334
+ #
335
+ # :method to_unit
336
+ # Convert this quantity to another quantity.
337
+ # unit can be any unit that measures the same thing as this quantity, i.e.
338
+ # 12.meters can call .to_feet, .to_centimeters, etc. An error is raised with
339
+ # other types, i.e. 12.meters.to_grams
340
+ # @raises ArgumentError
341
+ # @return [Quantity]
342
+
343
+ # Developer-friendly string representation
344
+ # @return [String]
345
+ def inspect
346
+ to_s
347
+ end
348
+
349
+ # this creates the conversion methods of .to_* and .in_*
350
+ # @private
351
+ def method_missing(method, *args, &block)
352
+ if method.to_s =~ /(to_|in_)(.*)/
353
+ if (Unit.is_unit?($2.to_sym))
354
+ convert($2.to_sym)
355
+ else
356
+ raise ArgumentError, "Unknown target unit type: #{$2}"
357
+ end
358
+ else
359
+ raise NoMethodError, "Undefined method `#{method}` for #{self}:#{self.class}"
360
+ end
361
+ end
362
+
363
+ end
364
+
365
+ # @private
366
+ # Plug our constructors into Numeric
367
+ class Numeric
368
+ alias_method :quantity_method_missing, :method_missing
369
+ def method_missing(method, *args, &block)
370
+ if Quantity::Unit.is_unit?(method)
371
+ Quantity.new(self,Quantity::Unit.for(method))
372
+ else
373
+ quantity_method_missing(method,*args, &block)
374
+ end
16
375
  end
17
376
  end