quantity 0.0.0 → 0.1.1

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