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.
@@ -0,0 +1,8 @@
1
+ require 'quantity'
2
+ require 'quantity/systems/si'
3
+ # this could just as well be 'imperial', had to pick something.
4
+ require 'quantity/systems/us'
5
+ require 'quantity/systems/information'
6
+ require 'quantity/systems/enumerable'
7
+ # other isn't done yet
8
+ #require 'quantity/systems/other'
@@ -0,0 +1,269 @@
1
+ class Quantity
2
+ # A Dimension is a measurable something. It is often
3
+ # called a 'base quantity'.
4
+ #
5
+ # There are 8 base physical dimensions: Length, Mass,
6
+ # Current, Mass, Time, Temperature, Substance, and Luminosity.
7
+ # There are additionally a number of other useful dimensions,
8
+ # such as Enumerable items (think of 'dozen' as a unit of
9
+ # measurement).
10
+ #
11
+ # From these base dimensions all other measurement dimensions
12
+ # can be constructed. For example, speed is Length / Time.
13
+ # Such dimensions are called compound dimensions.
14
+ #
15
+ class Dimension
16
+ ### Class-level methods/vars
17
+ # all known dimensions
18
+ @@dimensions = {}
19
+
20
+ # The dimension for a given symbol, dimension, or string description
21
+ # of a compound dimension
22
+ # @param [String Symbol Dimension]
23
+ # @return [Dimension]
24
+ def self.for(to)
25
+ case to
26
+ when Dimension
27
+ if @@dimensions[to.name]
28
+ @@dimensions[to.name]
29
+ else
30
+ add_alias to, to.name
31
+ to
32
+ end
33
+ when Symbol
34
+ if @@dimensions.has_key?(to)
35
+ @@dimensions[to]
36
+ else
37
+ # it's possible we have a non-normalized form, such as mass*length
38
+ # instead of length * mass
39
+ @@dimensions[string_form(parse_string_form(to)).to_sym]
40
+ end
41
+ when Array
42
+ @@dimensions[string_form(to).to_sym]
43
+ else
44
+ nil
45
+ end
46
+ end
47
+
48
+ # DSL component to add a new dimension. Dimension name, reference unit,
49
+ # and aliases. This should be the only way that dimensions are added.
50
+ # @param [Symbol] name
51
+ # @param [[Symbol String]] *aliases
52
+ def self.add_dimension(name, *aliases)
53
+ dim = nil
54
+ if name.is_a?(Dimension)
55
+ dim = name
56
+ name.name = aliases.first if aliases.first
57
+ else
58
+ dim = self.for(name) || self.new({ :name => aliases.first , :description => name})
59
+ self.add_alias(dim,name)
60
+ end
61
+ unless (dim.class == Dimension)
62
+ dim.name = dim.class.name.downcase.split(/::/).last.to_sym
63
+ self.add_alias(dim,dim.name)
64
+ end
65
+ self.add_alias(dim,*aliases)
66
+ dim
67
+ end
68
+
69
+ # Register a dimension to the known list, with the given aliases
70
+ # @param [Dimension]
71
+ # @param [[*names]]
72
+ def self.add_alias(dimension, *names)
73
+ names.each do |name|
74
+ raise ArgumentError, "Invalid dimension alias: #{name}" unless (name.to_s =~ /^(\^|\/)/).nil?
75
+ @@dimensions[name] = dimension
76
+ end
77
+ end
78
+
79
+ # All known dimensions
80
+ # @return [[Dimension]]
81
+ def self.all_dimensions
82
+ @@dimensions.values.uniq
83
+ end
84
+
85
+ # Reset the known dimensions. Generally only used for testing.
86
+ def self.__reset!
87
+ @@dimensions = {}
88
+ end
89
+
90
+ DimensionComponent = Struct.new(:dimension, :power)
91
+ DimensionComponent.class_eval do
92
+ def inspect
93
+ "#{dimension.inspect}^#{power}"
94
+ end
95
+ end
96
+
97
+ attr_reader :numerators, :denominators
98
+ attr_accessor :name
99
+ ### Instance-level methods/vars
100
+
101
+ # A new dimension
102
+ # @param [Hash] options
103
+ # @return Dimension
104
+ def initialize(opts)
105
+ if (opts[:description])
106
+ (@numerators,@denominators) = Dimension.parse_string_form(opts[:description])
107
+ elsif (opts[:numerators])
108
+ @numerators = opts[:numerators]
109
+ @denominators = opts[:denominators]
110
+ else
111
+ raise ArgumentError, "Invalid options for dimension constructors"
112
+ end
113
+ raise ArgumentError, "Dimensions require a numerator" unless @numerators.first.dimension
114
+ @name = opts[:name] || string_form.to_sym
115
+ Dimension.add_alias(self,@name)
116
+ Dimension.add_alias(self,string_form.to_sym)
117
+ end
118
+
119
+ def to_s
120
+ @name.to_s
121
+ end
122
+
123
+ # Dimensional multiplication
124
+ # @param [Dimension] other
125
+ # @return [Dimension]
126
+ def *(other)
127
+ raise ArgumentError, "Cannot multiply #{self} and #{other.class}" unless other.is_a?(Dimension)
128
+ (new_n, new_d) = Dimension.reduce(@numerators + other.numerators, @denominators + other.denominators)
129
+ existing = Dimension.for([new_n,new_d])
130
+ existing.nil? ? Dimension.new({:numerators => new_n, :denominators => new_d}) : existing
131
+ end
132
+
133
+ # Dimensional division
134
+ # @param [Dimension] other
135
+ # @return [Dimension]
136
+ def /(other)
137
+ raise ArgumentError, "Cannot divide #{self} by #{other.class}" unless other.is_a?(Dimension)
138
+ (new_n, new_d) = Dimension.reduce(@numerators + other.denominators, @denominators + other.numerators)
139
+ existing = Dimension.for([new_n,new_d])
140
+ existing.nil? ? Dimension.new({:numerators => new_n, :denominators => new_d}) : existing
141
+ end
142
+
143
+ # Dimensional exponentiation
144
+ # @param [Numeric] other
145
+ # @return [Dimension]
146
+ def **(other)
147
+ raise ArgumentError, "Dimensions can only be raised to whole powers" unless other.is_a?(Fixnum) && other > 0
148
+ other == 1 ? self : self * self**(other-1)
149
+ end
150
+
151
+ # Whether or not this is a compound representation of a base dimension
152
+ # @return [Boolean]
153
+ def is_base?
154
+ @denominators.size == 0 && @numerators.size == 1 && @numerators.first.power == 1
155
+ end
156
+
157
+ # Spaceship operator for comparable.
158
+ # @param [Any] other
159
+ # @return [-1 0 1]
160
+ def <=>(other)
161
+ if other.is_a?(Dimension)
162
+ if self.is_base? && other.is_base?
163
+ name.to_s <=> other.name.to_s
164
+ elsif other.is_base?
165
+ 1
166
+ else
167
+ string_form <=> other.string_form
168
+ end
169
+ else
170
+ nil
171
+ end
172
+ end
173
+
174
+ # Returns a developer-friendly representation of this value.
175
+ #
176
+ # The string will be of the format `#<Quantity::Dimension::0x12345678(...)>`,
177
+ # where `...` is the string returned by #to_s.
178
+ #
179
+ # @return [String]
180
+ def inspect
181
+ sprintf("#<%s:%#0x %s (%s)>", self.class.name, object_id, string_form, @name)
182
+ end
183
+
184
+
185
+ # Returns a vaguely human-readable and parsable description of this dimension
186
+ # @return [String]
187
+ def string_form
188
+ Dimension.string_form(@numerators,@denominators)
189
+ end
190
+
191
+ # A vaguely human-readable, vaguely machine-readable string description of
192
+ # a set of numerators and denominators.
193
+ # @private
194
+ # @ param [[DimensionComponent],[DimensionComponent]]
195
+ # @return [String]
196
+ def self.string_form(numerators, denominators = nil)
197
+ # We sometimes get [numerators,denominators],nil
198
+ (numerators,denominators) = numerators if (numerators.first.is_a?(Array))
199
+ string = ""
200
+ string_thunk = lambda do | array |
201
+ array.each_with_index do | component, n |
202
+ string << component.dimension.to_s
203
+ (string << '^' << component.power.to_s) if component.power.to_i > 1
204
+ string << '*' if n < array.size - 1
205
+ end
206
+ end
207
+ string_thunk.call(numerators)
208
+ string << "/" if denominators && denominators.size > 0
209
+ string_thunk.call(denominators) if denominators
210
+ string
211
+ end
212
+
213
+ # Parse the output of string_form into numerators and denominators
214
+ # @param [String] string
215
+ # @return [[DimensionComponent],[DimensionComponent]]
216
+ # @private
217
+ def self.parse_string_form(serialized)
218
+ parse_thunk = lambda do | string |
219
+ components = []
220
+ if !string.nil?
221
+ string.split(/\*/).each do | component |
222
+ (dimension, power) = component.split(/\^/)
223
+ components << DimensionComponent.new(dimension.to_sym,power.nil? ? 1 : power.to_i)
224
+ end
225
+ end
226
+ components
227
+ end
228
+ (top, bottom) = serialized.to_s.split(/\//)
229
+ Dimension.reduce(parse_thunk.call(top), parse_thunk.call(bottom))
230
+ end
231
+
232
+ # Returns numerators and denominators that represent the reduced form of the given
233
+ # numerators and denominators
234
+ # @return [[Array],[Array]]
235
+ # @private
236
+ def self.reduce(numerators,denominators)
237
+ new_numerators = reduce_multiplied_units(numerators)
238
+ new_denominators = reduce_multiplied_units(denominators)
239
+
240
+ new_numerators.each_with_index do | comp, i |
241
+ new_denominators.each_with_index do | dcomp, j |
242
+ if dcomp.dimension == comp.dimension
243
+ diff = [dcomp.power,comp.power].max - (dcomp.power - comp.power).abs
244
+ dcomp.power -= diff
245
+ comp.power -= diff
246
+ new_numerators.delete_at(i) if comp.power <= 0
247
+ new_denominators.delete_at(j) if dcomp.power <= 0
248
+ end
249
+ end
250
+ end
251
+ [new_numerators, new_denominators]
252
+ end
253
+
254
+ # Reduce an array of units to its most compact, sorted form
255
+ # @param [[DimensionComponent]]
256
+ # @return [[DimensionComponent]]
257
+ # @private
258
+ def self.reduce_multiplied_units(array)
259
+ new = {}
260
+ array.each do | item |
261
+ new[item.dimension] = DimensionComponent.new(item.dimension,0) unless new[item.dimension]
262
+ new[item.dimension].power += item.power
263
+ end
264
+ new.values.sort { |a,b| a.dimension.to_s <=> b.dimension.to_s }
265
+ end
266
+
267
+
268
+ end
269
+ end
@@ -0,0 +1,54 @@
1
+ #
2
+ #
3
+ class Quantity
4
+ #
5
+ # This module attempts to enumerate all of simple, base dimensions.
6
+ # This includes all of the base SI dimensions and some others
7
+ #
8
+ class Dimension
9
+
10
+ class Length < Quantity::Dimension ; end
11
+ length = Length.add_dimension :length, :width, :distance
12
+
13
+ class Time < Quantity::Dimension ; end
14
+ time = Time.add_dimension :time
15
+
16
+ class Mass < Quantity::Dimension ; end
17
+ mass = Mass.add_dimension :mass
18
+
19
+ class Current < Quantity::Dimension ; end
20
+ current = Current.add_dimension :current
21
+
22
+ class Luminosity < Quantity::Dimension ; end
23
+ luminosity = Luminosity.add_dimension :luminosity
24
+
25
+ class Substance < Quantity::Dimension ; end
26
+ substance = Substance.add_dimension :substance
27
+
28
+ class Temperature < Quantity::Dimension ; end
29
+ temp = Temperature.add_dimension :temperature
30
+
31
+ area = add_dimension length**2, :area
32
+
33
+ speed = add_dimension length / time, :speed, :velocity
34
+
35
+ accel = add_dimension speed / time, :acceleration
36
+
37
+ force = add_dimension mass * accel, :force
38
+
39
+ volume = add_dimension length**3, :volume
40
+
41
+ class Information < Quantity::Dimension ; end
42
+ information = Information.add_dimension :information, :data
43
+
44
+ # Quantity is the base dimension for the quantity of enumerable objects.
45
+ # Units are things like '2 dozen'.
46
+ class Quantity < Quantity::Dimension ; end
47
+ information = Quantity.add_dimension :quantity, :items, :enumerables
48
+
49
+ # Hardly a scientific base measurement, but it comes up a lot
50
+ class Currency < Dimension ; end
51
+ currency = Currency.add_dimension :money
52
+
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # a few countable quantities
2
+
3
+ class Quantity
4
+ class Unit
5
+
6
+ add_unit :dozen, :quantity, 12, :dozens
7
+ add_unit :couple, :quantity, 2
8
+ add_unit :score, :quantity, 20, :scores
9
+
10
+ end
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ # Imperial versions of british/american customary units
2
+ class Quantity::Unit
3
+ add_unit :foot, :length, 304.8, :ft, :feet
4
+ add_unit :inch, :length, 25.4, :in, :inches
5
+ add_unit :yard, :length, 914.4, :yd, :yards
6
+ add_unit :mile, :length, 1_609_344, :miles
7
+
8
+ add_unit :pound, :mass, 453592.37, :pounds, :lb, :lbs
9
+ add_unit :ounce, :mass, 28349.5231, :ounces, :oz
10
+ end
@@ -0,0 +1,30 @@
1
+ class Quantity
2
+ class Unit
3
+ ##
4
+ # @see http://en.wikipedia.org/wiki/Mebibyte
5
+ # @see http://en.wikipedia.org/wiki/Units_of_information
6
+
7
+ add_unit :bit, :data, 1, :bits
8
+ add_unit :nibble, :data, 4, :nibbles, :nybble, :nybbles
9
+ add_unit :byte, :data, 8, :bytes
10
+
11
+ add_unit :kilobyte, :data, 8 * 1000, :kb, :kilobytes
12
+ add_unit :megabyte, :data, 8 * (1000**2), :mb, :megabytes
13
+ add_unit :gigabyte, :data, 8 * (1000**3), :gb, :gigabytes
14
+ add_unit :terabyte, :data, 8 * (1000**4), :tb, :terabytes
15
+ add_unit :petabyte, :data, 8 * (1000**5), :pb, :petabytes
16
+ add_unit :exabyte, :data, 8 * (1000**6), :exabytes
17
+ add_unit :zettabyte, :data, 8 * (1000**7), :zettabytes
18
+ add_unit :yottabyte, :data, 8 * (1000**8), :yottabytes
19
+
20
+ add_unit :kibibyte, :data, 8 * 1024, :kibibytes, :KiB, :kib
21
+ add_unit :mebibyte, :data, 8 * (1024**2), :mebibytes, :MiB, :mib
22
+ add_unit :gibibyte, :data, 8 * (1024**3), :gibibytes, :GiB, :gib
23
+ add_unit :tebibyte, :data, 8 * (1024**4), :tebibytes, :TiB, :tib
24
+ add_unit :pebibyte, :data, 8 * (1024**5), :pebibytes, :PiB, :pib
25
+ add_unit :exbibyte, :data, 8 * (1024**6), :exbibytes, :EiB, :eib
26
+ add_unit :zebibyte, :data, 8 * (1024**7), :zebibytes, :ZiB, :zib
27
+ add_unit :yobibyte, :data, 8 * (1024**8), :yobibytes, :YiB, :yib
28
+
29
+ end
30
+ end
@@ -0,0 +1,105 @@
1
+ require 'quantity/dimension/base'
2
+
3
+ # SI units for Length, Mass, Luminosity, Current, Substance,
4
+ # Temperature, and Time. Units from yocto- to yotta- are supplied.
5
+ #
6
+ # Also supplied:
7
+ # * Ångstroms are supplied for Length. (use angstrom or angstroms)
8
+ # * Tonnes (Metric) are supplied for mass.
9
+ # * cc's for volume
10
+ #
11
+ # Volume (liters) is also part of this, since it follows the same pattern,
12
+ # even though the SI considers it a derived unit.
13
+ #
14
+ # The 'reference' unit is milli-. Units larger than milli-
15
+ # constructed via Fixnums/Bignums, such as 2.meters, will be stored with
16
+ # Fixnum / Bignum accuracy. Smaller items, such as 35.femtometers, will
17
+ # be stored with rationals or floats. Generally speaking, you shouldn't
18
+ # have to worry about this--use the numbers, and it will Do The Right Thing.
19
+ # Do remember that you may need to do a .to_f before dividing if that's
20
+ # what you want.
21
+ #
22
+ # @see http://physics.nist.gov/cuu/Units/units.html
23
+ # @see http://physics.nist.gov/cuu/Units/current.html
24
+ # @see http://physics.nist.gov/cuu/Units/prefixes.html
25
+ class Quantity
26
+ class Unit
27
+
28
+ prefixes = {}
29
+ units = {}
30
+ aliases = {}
31
+
32
+ prefixes['yotta'] = 10 ** 27
33
+ prefixes['zetta'] = 10 ** 24
34
+ prefixes['exa'] = 10 ** 21
35
+ prefixes['peta'] = 10 ** 18
36
+ prefixes['tera'] = 10 ** 15
37
+ prefixes['giga'] = 10 ** 12
38
+ prefixes['mega'] = 10 ** 9
39
+ prefixes['kilo'] = 10 ** 6
40
+ prefixes['hecto'] = 10 ** 5
41
+ prefixes['deca'] = 10 ** 4
42
+ prefixes[''] = 10 ** 3
43
+ prefixes['deci'] = 10 ** 2
44
+ prefixes['centi'] = 10
45
+ # milli is the reference point for SI-measured units
46
+ prefixes['milli'] = 1
47
+ prefixes['micro'] = 10 ** -3
48
+ prefixes['nano'] = 10 ** -6
49
+ prefixes['pico'] = 10 ** -9
50
+ prefixes['femto'] = 10 ** -12
51
+ prefixes['atto'] = 10 ** -15
52
+ prefixes['zepto'] = 10 ** -18
53
+ prefixes['yocto'] = 10 ** -21
54
+
55
+ units['meter'] = :length
56
+ units['gram'] = :mass
57
+ units['second'] = :time
58
+ units['kelvin'] = :temperature
59
+ units['candela'] = :luminosity
60
+ units['ampere'] = :current
61
+ units['mole'] = :substance
62
+ # liter is a special cased, handled separately below
63
+
64
+ aliases['ampere'] = ['amp', 'amps', 'A']
65
+ aliases['liter'] = ['litre', 'litres']
66
+ aliases['candela'] = ['cd']
67
+ aliases['mole'] = ['mol']
68
+ aliases['kelvin'] = ['K']
69
+
70
+ units.each do | unit, dimension |
71
+ prefixes.each do | prefix, value |
72
+ add_unit "#{prefix + unit}".to_sym, dimension, value, "#{prefix + unit}s".to_sym
73
+ if aliases[unit]
74
+ aliases[unit].each do | unit_alias |
75
+ add_alias "#{prefix + unit}".to_sym, "#{prefix + unit_alias}".to_sym
76
+ end
77
+ end
78
+ end
79
+ end
80
+
81
+ add_alias :kilometer, :km
82
+ add_alias :centimeter, :cm
83
+ add_alias :meter, :m
84
+ add_alias :nanometer, :nm
85
+ add_alias :millimeter, :mm
86
+ add_alias :millisecond, :ms
87
+ add_unit :angstrom, :length, 10 ** -7, :angstroms
88
+
89
+ add_alias :kilogram, :kg
90
+ add_alias :gram, :g
91
+ add_alias :milligram, :mg
92
+ add_alias :megagram, :tonne, :tonnes
93
+
94
+
95
+ prefixes.each do | prefix, value |
96
+ add_unit "#{prefix}liter".to_sym, :volume, value * 1000, "#{prefix}liters".to_sym
97
+ (aliases['liter']).each do | unit_alias |
98
+ add_alias "#{prefix}liter".to_sym, "#{prefix + unit_alias}".to_sym
99
+ end
100
+ end
101
+ add_alias :liter, :l
102
+ add_alias :milliliter, :ml
103
+
104
+ end
105
+ end