units-system 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,21 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Javier Goizueta
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,120 @@
1
+ = Ruby Units-System
2
+
3
+ Units of measure conversions for Ruby 1.9, using Ruby objects and Ruby syntax rather than text strings.
4
+
5
+ There are a number of Ruby units libraries, but I don't think they take this approach (I haven't
6
+ done much research, though.)
7
+
8
+ Currently Ruby 1.9 is required because we use the feature that
9
+ a +module.clas_eval{...}+ block can reference module's contants without qualification.
10
+ To make this apt for Ruby 1.8 we should replace expressions such as +u{W}+ by either
11
+ +u{W()}+, +u{self.W}+ (method invocation) or +u{self::W}+ (qualified constant).
12
+
13
+ Also, note that UTF-8 characters are liberally used in identifiers.
14
+
15
+ = Usage examples
16
+
17
+ To work with units a +units+ block can be used. Beware: in it +self+ is changed,
18
+ so outer self methods or instance variables are not accessible, unless usigned to
19
+ local variables.
20
+
21
+ require 'units-system'
22
+
23
+ Units.units do
24
+
25
+ # In the units environment predefined variables are available for all units and they
26
+ # can b combined arithmetically:
27
+ x = 3*m/s
28
+ puts x # => 3.0*m/s
29
+ # Note that SI prefixes (k for kilo, etc.) can be used as part of the unit names:
30
+ x += 17*km/h
31
+ puts x # => 7.72222222222222*m/s
32
+ puts x.to(km/h) # => 27.8*km/h
33
+ puts x.magnitude # => 7.72222222222222
34
+
35
+ # Let's use some unit powers: convert 3 cubic meters to litres:
36
+ puts (3*m**3).to(l) # => 3000.0*l
37
+
38
+ # Now let's convert som imperial units to SI:
39
+ puts (100*mi/h).to_si # => 44.704*m/s
40
+
41
+ # Note that in is a Ruby keyword, so to use inches you must use self.in:
42
+ puts (10*cm).to(self.in) # => 3.93700787401575*in
43
+ # ...or use tha alternative nonstandard name +inch+
44
+ puts (10*cm).to(self.inch) # => 3.93700787401575*inch
45
+
46
+ # Now let's use derived units, e.g. power units:
47
+ x = 10*kW
48
+ # show a verbose description of the measure:
49
+ puts x.describe # => 10.0 kiloWatt
50
+ # convert to base units
51
+ puts x.base # => 10000.0*(m**2*kg)/s**3
52
+ # a more natural notation can be used instead of the default Ruby syntax:
53
+ puts x.base.abr # => 10000.0 (m^2 kg)/s^3
54
+
55
+ # Note that unit names that start with uppercase letters are OK:
56
+ puts 11*W # => 11.0*W
57
+ puts (2*Mg).to(kg) # => 2000.0*kg
58
+
59
+ # Let's use kilograms-force (kiloponds) (not a SI unit)
60
+ x = 10*kgf
61
+ puts x # => 10.0*kgf
62
+ # conversion to SI units uses the SI unit of force the newton N (which is a derived unit)
63
+ puts x.to_si # => 98.0665*N
64
+ # conversion to base units substitutes derived units for base units
65
+ puts x.base # => 98066.5*(g*m)/s**2
66
+ # but g (gram) is not a base SI unit, to get SI base units we must:
67
+ puts x.base.to_si # => 98.0665*(kg*m)/s**2
68
+
69
+ # And now, for some trigonometry fun! (note the use of unicode characters)
70
+ x = 90*°
71
+ puts x # => 90.0*°
72
+ puts x.to(rad) # => 1.5707963267949*rad
73
+ puts sin(x) # => 1.0
74
+
75
+ puts sin(45*°+30*′+10*″) # => 0.713284429355996
76
+
77
+ puts asin(0.5) # => 0.523598775598299*rad
78
+ puts asin(0.5).to(°) # => 30.0*°
79
+ puts asin(0.5).in(°) # => 30.0
80
+
81
+ puts atan2(10*cm, 0.1*m).to(°) # => 45.0*°
82
+
83
+ # Temperature conversions may be absolute (convert levels of temperature)
84
+ # or relative (convert differences of temperature)
85
+ # When a measure has a single unit of temperature, conversion is absolute:
86
+ puts (20*°C).to(K) # => 293.15*K
87
+ puts (20*°C).to(°F) # => 67.9999999999999*°F
88
+ puts (20*mK).to(°C) # => -273.13*°C
89
+ # In other cases conversion is relative:
90
+ puts (2*°C/h).to(K/h) # => 2.0*K/h
91
+ puts (2*°C/h).to(°F/h) # => 3.6*°F/h
92
+ # To force the relative conversion of a single temperature pass a second argument to to():
93
+ puts (20*°C).to(K,:relative) # => 20.0*K
94
+ puts (20*°C).to(°F,:relative) # => 36.0*°F
95
+ puts (20*mK).to(°C,:relative) # => 0.02*°C
96
+
97
+ end
98
+
99
+ For short expressions, the abbreviation +Units.u+ can be used instead of +Units.units+
100
+
101
+ include Units
102
+ puts u{60*km + 10*mi} # => 76.09344*km
103
+ puts u{sin(45*°)} # => 0.707106781186547
104
+ x = u{120*km/h}
105
+ puts x.to(u{mi/h}) # => 74.5645430684801*mi/h
106
+
107
+
108
+ == Note on Patches/Pull Requests
109
+
110
+ * Fork the project.
111
+ * Make your feature addition or bug fix.
112
+ * Add tests for it. This is important so I don't break it in a
113
+ future version unintentionally.
114
+ * Commit, do not mess with rakefile, version, or history.
115
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
116
+ * Send me a pull request. Bonus points for topic branches.
117
+
118
+ == Copyright
119
+
120
+ Copyright (c) 2009 Javier Goizueta. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "units-system"
8
+ gem.summary = %Q{Arithmetic with units of measure}
9
+ gem.description = %Q{Experimental unit conversion & arithmetic for Ruby 1.9}
10
+ gem.required_ruby_version = '>= 1.9.1'
11
+ gem.email = "jgoizueta@gmail.com"
12
+ gem.homepage = "http://github.com/jgoizueta/units-system"
13
+ gem.authors = ["Javier Goizueta"]
14
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "units-system #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
@@ -0,0 +1,634 @@
1
+ # encoding: utf-8
2
+
3
+ # Ruby 1.9 Units-System experiments.
4
+
5
+ module Units
6
+
7
+ class Measure
8
+
9
+ def initialize(mag=1.0, units={})
10
+ @magnitude = mag # rename to value?
11
+ case units
12
+ when Symbol
13
+ uinfo = Units.unit(units)
14
+ if uinfo
15
+ if uinfo.dim
16
+ @units = {uinfo.dim=>[units,1]}
17
+ else
18
+ @magnitude *= uinfo.factor
19
+ @units = {}
20
+ end
21
+ else
22
+ raise ArgumentError,"Invalid symbol for Measure definition: #{units.inspect} "
23
+ end
24
+ when Measure
25
+ @magnitude *= units.magnitude
26
+ @units = units.units
27
+ else
28
+ @units = units
29
+ end
30
+ end
31
+
32
+ attr_reader :magnitude, :units
33
+
34
+ # represent in text using Ruby notation
35
+ def to_s
36
+ return @magnitude.to_s if magnitude?
37
+ u_descr = Units.units_descr(@units)
38
+ "#{@magnitude}*#{u_descr}"
39
+ end
40
+
41
+ # more verbose description (not grammatically perfect)
42
+ def describe
43
+ return @magnitude.to_s if magnitude?
44
+ u_descr = Units.units_descr(@units, true)
45
+ "#{@magnitude} #{u_descr}"
46
+ end
47
+
48
+ # more natural concise text representation
49
+ def abr
50
+ self.to_s.gsub('**','^').tr('*',' ')
51
+ end
52
+
53
+ # decompose compound units
54
+ def detailed_units(all_levels = false)
55
+ mag = @magnitude
56
+ prev_units = self.units
57
+ units = {}
58
+ loop do
59
+ compound = false
60
+ prev_units.each_pair do |dim, (unit,mul)|
61
+ ud = Units.unit(unit)
62
+ if ud.decomposition
63
+ compound = true
64
+ mag *= ud.decomposition.magnitude
65
+ ud.decomposition.units.each_pair do |d, (u,m)|
66
+ mag *= self.class.combine(units, d, u, m)
67
+ end
68
+ else
69
+ mag *= self.class.combine(units, dim, unit, mul)
70
+ end
71
+ end
72
+ if all_levels && compound
73
+ prev_units = units
74
+ units = {}
75
+ else
76
+ break
77
+ end
78
+ end
79
+ Measure.new mag, units end
80
+
81
+ # decompose to base units
82
+ def base
83
+ detailed_units true
84
+ end
85
+
86
+ def self.combine(units, dim, unit, mult)
87
+ factor = 1
88
+ if units[dim]
89
+ u,m = units[dim]
90
+ if u!=unit
91
+ factor *= Units.conversion_factor(unit, u)**mult
92
+ end
93
+ units[dim] = [u, m+mult]
94
+ else
95
+ units[dim] = [unit, mult]
96
+ end
97
+ factor
98
+ end
99
+
100
+ def /(other)
101
+ self * (other.kind_of?(Numeric) ? 1.0/other : other.inverse)
102
+ end
103
+
104
+ def inspect
105
+ "Measure(#{@magnitude.inspect}, #{@units.inspect})"
106
+ end
107
+
108
+ def *(other)
109
+ case other
110
+ when Numeric
111
+ mag = self.magnitude*other
112
+ units = self.units
113
+ else
114
+ mag = self.magnitude*other.magnitude
115
+ units = {}
116
+ (self.units.keys | other.units.keys).each do |dim|
117
+ other_u = other.units[dim] || [nil,0]
118
+ this_u = self.units[dim] || [other_u.first,0]
119
+ # mag *= Units.conversion_factor(this_u.first, other_u.first) if other_u.first
120
+ mult = this_u.last+other_u.last
121
+ mag *= Units.conversion_factor(other_u.first, this_u.first)**(other_u.last) if other_u.first
122
+ units[dim] = [this_u.first, mult]
123
+ end
124
+ end
125
+ units.reject!{|dim,(u,m)| m==0}
126
+ Measure.new(mag, units)
127
+ end
128
+
129
+ def +(other)
130
+ Measure.new self.magnitude+other.to(self.units).magnitude, self.units
131
+ end
132
+
133
+ def -(other)
134
+ self + (-other)
135
+ end
136
+
137
+ def **(n)
138
+ raise ArgumentError,"Only integer powers are allowed for magnitudes" unless n.kind_of?(Integer)
139
+ units = @units.dup
140
+ units.each_pair do |dim, (u, m)|
141
+ units[dim] = [u, m*n]
142
+ end
143
+ Measure.new @magnitude, units
144
+ end
145
+
146
+ def inverse
147
+ #Measure.new(1.0/@magnitude, @units.map_hash{|unit,mult| [unit, -mult]})
148
+ units = {}
149
+ @units.each_pair do |dim, (unit, mult)|
150
+ units[dim] = [unit, -mult]
151
+ end
152
+ Measure.new(1.0/@magnitude, units)
153
+ end
154
+
155
+ def -@
156
+ Measure.new(-@magnitude, units)
157
+ end
158
+
159
+ def in(other, mode=:absolute)
160
+ other = Measure.new(1.0, other) unless other.kind_of?(Measure)
161
+ other = other.base
162
+ this = self.base
163
+ dims = this.units.keys | other.units.keys
164
+ mag = this.magnitude/other.magnitude
165
+ dims.each do |dim|
166
+ if !this.units[dim] || !other.units[dim] ||
167
+ (this.units[dim].last != other.units[dim].last)
168
+ raise "Inconsistent units #{Units.units_descr(this.units)} #{Units.units_descr(other.units)}"
169
+ end
170
+ this_u, mult = this.units[dim]
171
+ other_u = other.units[dim].first
172
+ mag *= Units.conversion_factor(this_u, other_u)**mult
173
+ end
174
+ if mode!=:relative && dims.size==1 && this.units[dims.first].last==1
175
+ # consider "level" conversion for biased units (otherwise consider interval or difference values)
176
+ mag += Units.conversion_bias(this.units[dims.first].first, other.units[dims.first].first)
177
+ end
178
+ mag
179
+ end
180
+
181
+ def to(units, mode=:absolute)
182
+ units = units.u if units.kind_of?(Measure)
183
+ Measure.new self.in(units, mode), units
184
+ end
185
+
186
+ def si_units
187
+ units = {}
188
+ @units.each_pair do |dim, (unit, mult)|
189
+ si_unit = SI_UNITS[dim]
190
+ if si_unit.kind_of?(Measure)
191
+ si_unit.units.each_pair do |d, (u,m)|
192
+ self.class.combine(units, d, u, m*mult)
193
+ end
194
+ else
195
+ self.class.combine(units, dim, si_unit, mult)
196
+ end
197
+ end
198
+ units
199
+ end
200
+
201
+ def to_si
202
+ to(si_units)
203
+ end
204
+
205
+ def coerce(other)
206
+ [Measure.new(other, {}), self]
207
+ end
208
+
209
+ def u # dimension? unit? only_units? strip_units? units_measure?
210
+ Measure.new(1.0, self.units)
211
+ end
212
+
213
+ # dimension (quantity)
214
+ def dimension
215
+ q = nil
216
+ u = self.base.si_units
217
+ SI_UNITS.each_pair do |dim, unit|
218
+ unit = Measure.new(1.0, unit) unless unit.kind_of?(Measure)
219
+ if unit.base.si_units == u
220
+ q = dim
221
+ break
222
+ end
223
+ end
224
+ q
225
+ end
226
+
227
+ def dimensionless?
228
+ base.units.reject{|d,(u,m)| m==0}.empty?
229
+ end
230
+
231
+ # less strict dimensionless condition (e.g. an angle is not a pure magnitude in this sense)
232
+ def magnitude?
233
+ self.units.reject{|d,(u,m)| m==0}.empty?
234
+ end
235
+
236
+ end # Measure
237
+
238
+ def Measure(*args)
239
+ if args.size==1
240
+ case args.first
241
+ when Numeric
242
+ m = args.first
243
+ u = {}
244
+ else
245
+ m = 1.0
246
+ u = args.first
247
+ end
248
+ args = [m,u]
249
+ end
250
+ Units::Measure.new(*args)
251
+ end
252
+ module_function :Measure
253
+
254
+ PREFIXES = {
255
+ :y=>[1E-24, 'yocto'],
256
+ :z=>[1E-21, 'zepto'],
257
+ :a=>[1E-18, 'atto'],
258
+ :f=>[1E-15, 'femto'],
259
+ :p=>[1E-12, 'pico'],
260
+ :n=>[1E-09, 'nano'],
261
+ :µ=>[1E-06, 'micro'], # ASCII alternative: u; define it?
262
+ :m=>[1E-03, 'milli'],
263
+ :c=>[1E-02, 'centi'],
264
+ :d=>[1E-01, 'deci'],
265
+ :da=>[1E1, 'deca'],
266
+ :h=>[1E02, 'hecto'],
267
+ :k=>[1E03, 'kilo'],
268
+ :M=>[1E06, 'mega'],
269
+ :G=>[1E09, 'giga'],
270
+ :T=>[1E12, 'tera'],
271
+ :P=>[1E15, 'peta'],
272
+ :E=>[1E18, 'exa'],
273
+ :Z=>[1E21, 'zetta'],
274
+ :Y=>[1E24, 'yotta']
275
+ }
276
+
277
+ def self.prefix_factor(prefix)
278
+ pd = PREFIXES[prefix.to_sym]
279
+ pd && pd.first
280
+ end
281
+
282
+ def self.prefix_name(prefix)
283
+ pd = PREFIXES[prefix.to_sym]
284
+ pd && pd.last
285
+ end
286
+
287
+ def self.prefix_factor_and_name(prefix)
288
+ PREFIXES[prefix]
289
+ end
290
+
291
+ # mathematical functions available to unit blocks are defined here
292
+ module Math
293
+ end
294
+
295
+ # unit methods and constants are defined here to be injected in units blocks
296
+ module System
297
+
298
+ extend Math
299
+
300
+ def self.define(unit)
301
+ define_var unit
302
+ PREFIXES.each do |prefix, (factor, name)|
303
+ define_var "#{prefix}#{unit}".to_sym
304
+ end
305
+ end
306
+
307
+ class <<self
308
+ private
309
+ def define_var(name)
310
+ name_initial = name.to_s[0,1]
311
+ if name_initial==name_initial.upcase && name_initial!=name_initial.downcase
312
+ # we could define a method with the requested name, but it would
313
+ # no be usable without qualification (System.X) or brackets (X())
314
+ # so we define a constant. But this requires Ruby 1.9 to be useful;
315
+ # otherwise the constant is not accesible without qualification in units blocks.
316
+ System.const_set name, Units.Measure(name)
317
+ end
318
+ self.class_eval do
319
+ define_method name do
320
+ Units.Measure(name)
321
+ end
322
+ module_function name
323
+ end
324
+ end
325
+ end
326
+
327
+ end # Units::System
328
+
329
+ def units(&blk)
330
+ Units::System.class_eval(&blk)
331
+ end
332
+ alias :u :units
333
+ module_function :units, :u
334
+
335
+ UnitDefinition = Struct.new(:dim, :factor, :name, :decomposition, :bias)
336
+
337
+ UNITS = {} # Hash.new{|h,k| h[k]=UnitDefinition.new()}
338
+
339
+ # get unit definition
340
+ def self.unit(unit_symbol)
341
+ ud = UNITS[unit_symbol]
342
+ if ud.nil?
343
+ factor = 1.0
344
+ if factor_name = PREFIXES[unit_symbol]
345
+ ud = UnitDefinition.new(nil, *factor_name)
346
+ else
347
+ u = unit_symbol.to_s
348
+ PREFIXES.each_pair do |prefix, (f,name)|
349
+ prefix = prefix.to_s
350
+ if u[0...prefix.length] == prefix
351
+ factor = f
352
+ ud = UNITS[u[prefix.length..-1].to_sym].dup
353
+ if ud
354
+ ud.name = "#{name}#{ud.name}"
355
+ break
356
+ end
357
+ end
358
+ end
359
+ end
360
+ ud.factor *= factor if ud
361
+ ud.decomposition *= factor if ud && ud.decomposition
362
+ end
363
+ raise ArgumentError,"Invalid Units #{unit_symbol}" unless ud
364
+ ud
365
+ end
366
+
367
+ # Define new units.
368
+ # Define a base unit (with a factor for conversion to SI units)
369
+ # Units.define :unit_symbol, 'unit-name', :quantity, si_units_per_this_unit
370
+ # Define a unit in terms or another (valid for base or derived units)
371
+ # Units.define :unit_symbol, 'unit-name', value, :in_units
372
+ # Define a base unit as a measure-expression
373
+ # Units.define :unit_symbol, 'unit_name', u{...}
374
+ # Define a derived unit as a measure-expression
375
+ # Units.define :unit_symbol, 'unit_name', :quantity, u{...}
376
+ # For base dimensions the SI unit for a quantity must also be stablished with Unit.si_units;
377
+ # for derived units, SI units are automatically taken to be the first define unit of the quantity
378
+ # with unitary value in SI base units.
379
+ def self.define(unit_symbol, name, *args)
380
+ eqhivalence = nil
381
+ si_unit = false
382
+ bias = nil
383
+ if args.first.kind_of?(Symbol)
384
+ dim = args.shift
385
+ if args.first.kind_of?(Numeric)
386
+ # unidad simple
387
+ factor = args.shift
388
+ factor_units = args.shift
389
+ if factor_units
390
+ ud = unit(factor_units)
391
+ if ud.dim != dim
392
+ raise ArgumentError, "Inconsistent units #{factor_units} in definition of #{unit_symbol}"
393
+ end
394
+ # maybe it was not simple after all...
395
+ equivalence = factor*ud.decomposition if ud.decomposition
396
+ factor *= ud.factor
397
+ end
398
+ else
399
+ # unidad compuesta
400
+ equivalence = args.shift
401
+ factor = equivalence.to_si.magnitude
402
+ si_unit = (factor==1.0) # TODO: tolerance?
403
+ end
404
+ elsif args.first.kind_of?(Numeric)
405
+ # unidad definida en función de otra
406
+ factor = args.shift
407
+ factor_units = args.shift
408
+ u = unit(factor_units)
409
+ dim = u.dim
410
+ factor *= u.factor
411
+ equivalence = factor*u.decomposition if u.decomposition
412
+ bias = args.shift
413
+ else
414
+ # unidad simple definida en función de una expressión
415
+ definition = args.shift
416
+ if definition.units.keys.size!=1
417
+ raise ArgumentError,"To define a compound unit a dimension must be specified"
418
+ end
419
+ dim = definition.units.keys.first
420
+ factor = definition.to_si.magnitude
421
+ end
422
+ unit_def = UnitDefinition.new(dim, factor, name, equivalence, bias)
423
+ if UNITS.has_key?(unit_symbol)
424
+ raise "Se ha intentando redefinir #{unit_symbol} (previamente definido como #{UNITS[unit_symbol]}) como #{unit_def}"
425
+ end
426
+ UNITS[unit_symbol] = unit_def
427
+ System.define unit_symbol
428
+ Units.si_units unit_def.dim, unit_symbol if si_unit && !SI_UNITS.has_key?(unit_def.dim)
429
+ end
430
+
431
+ SI_UNITS = {}
432
+
433
+ def self.si_units(dim, unit)
434
+ SI_UNITS[dim] = unit
435
+ end
436
+
437
+ def self.dimension(u)
438
+ unit(u).dim
439
+ end
440
+
441
+ def self.conversion_factor(from, to)
442
+ from_u = unit(from)
443
+ to_u = unit(to)
444
+ raise ArgumentError,"Inconsistent Units (#{from}, #{to})" if from_u.dim!=to_u.dim
445
+ from_u.factor/to_u.factor
446
+ end
447
+
448
+ def self.conversion_bias(from, to)
449
+ from_u = unit(from)
450
+ to_u = unit(to)
451
+ raise ArgumentError,"Inconsistent Units (#{from}, #{to})" if from_u.dim!=to_u.dim
452
+ factor = from_u.factor/to_u.factor
453
+ (from_u.bias||0)*factor - (to_u.bias||0)
454
+ end
455
+
456
+ # simple unit name
457
+ def self.unit_name(u)
458
+ uinfo = Units.unit(u)
459
+ uinfo && uinfo.name
460
+ end
461
+
462
+ def self.unit_descr(u, long=false, mult=1)
463
+ if long
464
+ u = unit_name(u)
465
+ if mult!=1
466
+ case mult
467
+ when 2
468
+ "squared #{u}"
469
+ when 3
470
+ "cubed #{u}"
471
+ else
472
+ "#{u} to the #{mult} power"
473
+ end
474
+ else
475
+ u
476
+ end
477
+ else
478
+ if mult!=1
479
+ "#{u}**#{mult}"
480
+ else
481
+ u.to_s
482
+ end
483
+ end
484
+ end
485
+
486
+ def self.units_descr(units, long=false)
487
+ units = units.values.sort_by{|u,m| -m}
488
+ pos_units = units.select{|u| u.last>0}
489
+ neg_units = units.select{|u| u.last<0}
490
+ times = long ? " " : "*"
491
+ num = pos_units.map{|u,m| unit_descr(u,long,m)}.join(times)
492
+ num = "(#{num})" if pos_units.size>1 && !neg_units.empty? && !long
493
+ den = neg_units.map{|u,m| unit_descr(u,long,-m)}.join("*")
494
+ den = "(#{den})" if neg_units.size>1 && !long
495
+ if pos_units.empty?
496
+ u_descr = "1/#{den}"
497
+ elsif neg_units.empty?
498
+ u_descr = num
499
+ else
500
+ connector = long ? " per " : "/"
501
+ u_descr = "#{num}#{connector}#{den}"
502
+ end
503
+ u_descr
504
+ end
505
+
506
+ module Math
507
+
508
+ [:sin, :cos, :tan].each do |fun|
509
+ define_method fun do |x|
510
+ x = Units.u{x.in(rad)} if x.kind_of?(Measure)
511
+ ::Math.send(fun, x)
512
+ end
513
+ module_function fun
514
+ end
515
+
516
+ [:asin, :acos, :atan].each do |fun|
517
+ define_method fun do |x|
518
+ if x.kind_of?(Measure)
519
+ raise ArgumentError,"Invalid dimensions for #{fun} argument" unless x.dimensionless?
520
+ x = x.magnitude
521
+ end
522
+ Units.u{::Math.send(fun, x)*rad}
523
+ end
524
+ module_function fun
525
+ end
526
+
527
+ module_function
528
+ def atan2(x,y)
529
+ if x.kind_of?(Measure)
530
+ if y.kind_of?(Measure)
531
+ if x.base.to_si.units != y.base.to_si.units
532
+ raise ArgumentError,"Inconsistent units for atan2 arguments #{x.u}, #{y.u}"
533
+ end
534
+ # or x = x.to_si.magnitude, y=y.to_si.magnitude
535
+ y = y.in(x.units)
536
+ x = x.magnitude
537
+ else
538
+ raise ArgumentError,"Invalid dimensions for atan2 argument #{x.u}" unless x.dimensionless?
539
+ x = x.magnitude
540
+ end
541
+ elsif y.kind_of?(Measure)
542
+ raise ArgumentError,"Invalid dimensions for atan2 argument #{y.u}" unless y.dimensionless?
543
+ y = y.magnitude
544
+ end
545
+ Units.u{::Math.atan2(x,y)*rad}
546
+ end
547
+
548
+ end
549
+
550
+ # Units definitions
551
+
552
+ # declare SI base units
553
+ si_units :mass, :kg # m
554
+ si_units :length, :m # l
555
+ si_units :time, :s # t
556
+ si_units :electric_current, :A # I
557
+ si_units :temperature, :K # T
558
+ si_units :luminous_intensity, :cd # Iv
559
+ si_units :amount_of_substance, :mol # n
560
+
561
+ # base units definitions
562
+ define :g, 'gram', :mass, 1E-3
563
+ define :m, 'meter', :length, 1.0
564
+ define :s, 'second', :time, 1.0
565
+ define :A, 'ampere', :electric_current, 1.0
566
+ define :K, 'kelvin', :temperature, 1.0
567
+ define :cd,'candela',:luminous_intensity, 1.0
568
+ define :mol,'mole', :amount_of_substance, 1.0
569
+
570
+ define :min, 'minute', 60, :s
571
+ define :h, 'hour', 60, :min
572
+ define :d, 'day', 24, :h
573
+ define :mi, 'mile', 1.609344, :km
574
+ define :in, 'inch', 2.54, :cm
575
+ define :ft, 'foot', 0.3048, :m
576
+ # in is a Ruby keyword; to avoid having to use self.in we'll define:
577
+ define :inch, 'inch', 1, :in
578
+
579
+ # declare derived quantities without named units
580
+ si_units :speed, u{m/s}
581
+ si_units :acceleration, u{m/s**2}
582
+ si_units :area, u{m**2}
583
+ si_units :volume, u{m**3}
584
+
585
+ # named derived units
586
+ define :W, 'Watt', :power, u{kg*m**2/s**3} # J/s
587
+ define :Hz, 'herz', :frequency, u{1/s}
588
+ define :N, 'newton', :force, u{m*kg/s**2}
589
+ define :Pa, 'pascal', :pressure, u{N/m**2}
590
+ define :J, 'joule', :energy, u{N*m}
591
+ define :C, 'coulomb', :electric_charge, u{s*A}
592
+ define :V, 'volt', :voltage, u{W/A}
593
+ define :F, 'farad', :electric_capacitance, u{C/V}
594
+ define :Ω, 'ohm', :electric_resistance, u{V/A} # ohm: Ω omega: Ω
595
+ define :S, 'siemens', :electric_condctance, u{1/Ω}
596
+ define :Wb,'weber', :magnetic_flux, u{J/A}
597
+ define :T, 'tesla', :magnetic_field, u{N/(A*m)}
598
+ define :H, 'henry', :inductance, u{Wb/A}
599
+ define :rad, 'radian', :angle, u{m/m} # maybe more useful as base unit: define :rad, 'radian', :angle, 1.0
600
+ define :sr, 'steradian', :solid_angle, u{m**2/m**2}
601
+ define :lm, 'lumen', :luminous_flux, u{cd*sr}
602
+ define :lx, 'lux', :illuminance, u{lm/m**2}
603
+ define :Bq, 'bequerel', :radioactivity, u{1/s}
604
+ define :Gy, 'gray', :absorbed_dose, u{J/kg}
605
+ define :Sv, 'sievert', :equivalent_dose, u{J/kg}
606
+ define :kat, 'katal', :catalytic_activity, u{mol/s}
607
+
608
+ define :°C, 'degree Celsius', 1, :K, +273.15
609
+ define :°F, 'degree Fahrenheit', 5.0/9.0, :K, +459.67
610
+ define :R, 'rankine', 5.0/9.0, :K
611
+
612
+ define :l, 'litre', :volume, u{dm**3}
613
+ define :L, 'litre', 1, :l
614
+
615
+ define :°, 'degree', ::Math::PI/180.0, :rad
616
+ define :′, 'arc-minute', ::Math::PI/180.0/60.0, :rad
617
+ define :″, 'arc-second', ::Math::PI/180.0/3600.0, :rad
618
+ # not so cool, but easier to type alternatives:
619
+ define :deg, 'degree', 1, :°
620
+ define :arcmin, 'arc-minute', 1, :′
621
+ define :arcsec, 'arc-second', 1, :″
622
+
623
+ define :g0, 'standard gravity', :acceleration, u{9.80665*m/s**2}
624
+
625
+ define :bar, 'bar', :pressure, u{1E5*Pa}
626
+ define :atm, 'atmosphere', :pressure, u{101325.0*Pa}
627
+ define :mWC, 'meters of water column', :pressure, u{1E3*kg*g0/m**2}
628
+ define :Torr, 'torricelli', :pressure, u{atm/760}
629
+ define :mHg, 'mHg', :pressure, u{13.5951E3*kg*g0/m**2}
630
+
631
+ # define :kp, 'kilopond', :force, u{kg*g0} # or define pond?
632
+ define :gf, 'gram-force', :force, u{g*g0} # kilopond kp = kgf
633
+
634
+ end # Units
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'units-system'
8
+
9
+ class Test::Unit::TestCase
10
+ end
@@ -0,0 +1,71 @@
1
+ require 'helper'
2
+
3
+ class TestUnitsSystem < Test::Unit::TestCase
4
+
5
+ should "be possible to define Measures with a units block" do
6
+ assert_equal Units::Measure, Units.units{m}.class
7
+ assert_equal 1.0, Units.units{m}.magnitude
8
+ assert_equal [:m, 1], Units.units{m}.units[:length]
9
+ end
10
+
11
+ should "be possible to define Measures with a shorthand u block" do
12
+ assert_equal Units::Measure, Units.u{m}.class
13
+ assert_equal 1.0, Units.u{m}.magnitude
14
+ assert_equal [:m, 1], Units.u{m}.units[:length]
15
+ end
16
+
17
+ should "determine units dimension correctly" do
18
+ assert_equal :length, Units.u{m}.dimension
19
+ assert_equal :length, Units.u{mi}.dimension
20
+ assert_equal :mass, Units.u{g}.dimension
21
+ assert_equal :time, Units.u{s}.dimension
22
+ assert_equal :electric_current, Units.u{A}.dimension
23
+ assert_equal :temperature, Units.u{K}.dimension
24
+ assert_equal :luminous_intensity, Units.u{cd}.dimension
25
+ assert_equal :amount_of_substance, Units.u{mol}.dimension
26
+ assert_equal :power, Units.u{W}.dimension
27
+ assert_equal :pressure, Units.u{Pa}.dimension
28
+ assert_equal :mass, Units.u{kg}.dimension
29
+ assert_equal :length, Units.u{km}.dimension
30
+ assert_equal :length, Units.u{mm}.dimension
31
+ assert_equal :length, Units.u{Mm}.dimension
32
+ end
33
+
34
+ should "allow unit arithmetic" do
35
+ assert_equal :speed, Units.u{m/s}.dimension
36
+ assert_equal :length, Units.u{m**2/m}.dimension
37
+ assert_equal :length, Units.u{(m*m)/m}.dimension
38
+ assert_equal :volume, Units.u{m*m**2}.dimension
39
+ assert_equal :force, Units.u{m*kg/s**2}.dimension
40
+ end
41
+
42
+ should "convert simple units correctly" do
43
+ assert_equal 1E5, Units.u{100*km.in(m)}
44
+ assert_equal 10, Units.u{10000*m.in(km)}
45
+ assert_equal 254, Units.u{100*self.in.in(cm)}
46
+ end
47
+
48
+ should "convert compound units correctly" do
49
+ assert_equal 75, Units.u{(270*km/h).in(m/s)}
50
+ assert_equal 270, Units.u{(75*m/s).in(km/h)}
51
+ end
52
+
53
+ should "add units correctly" do
54
+ assert_equal 5, Units.u{3*m+2*m}.magnitude
55
+ assert_equal 5, Units.u{3*kg+2*kg}.magnitude
56
+ end
57
+
58
+ should "add units correctly with implied conversions" do
59
+ assert_equal 5, Units.u{3*m+200*cm}.magnitude
60
+ assert_equal [:m,1],Units.u{3*m+200*cm}.units[:length]
61
+ assert_equal 85, Units.u{10*m/s + 270*km/h}.magnitude
62
+ end
63
+
64
+ should "choke on inconsistent units" do
65
+ assert_raise(RuntimeError){Units.units{m+kg}}
66
+ assert_raise(RuntimeError){Units.units{m/s+m/s**2}}
67
+ assert_raise(RuntimeError){Units.units{(m).to(kg)}}
68
+ assert_raise(RuntimeError){Units.units{(m/s).to(m/s**2)}}
69
+ end
70
+
71
+ end
@@ -0,0 +1,55 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{units-system}
8
+ s.version = "0.0.1"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Javier Goizueta"]
12
+ s.date = %q{2009-12-18}
13
+ s.description = %q{Experimental unit conversion & arithmetic for Ruby 1.9}
14
+ s.email = %q{jgoizueta@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION",
26
+ "lib/units-system.rb",
27
+ "test/helper.rb",
28
+ "test/test_units-system.rb",
29
+ "units-system.gemspec"
30
+ ]
31
+ s.homepage = %q{http://github.com/jgoizueta/units-system}
32
+ s.rdoc_options = ["--charset=UTF-8"]
33
+ s.require_paths = ["lib"]
34
+ s.required_ruby_version = Gem::Requirement.new(">= 1.9.1")
35
+ s.rubygems_version = %q{1.3.5}
36
+ s.summary = %q{Arithmetic with units of measure}
37
+ s.test_files = [
38
+ "test/helper.rb",
39
+ "test/test_units-system.rb"
40
+ ]
41
+
42
+ if s.respond_to? :specification_version then
43
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
44
+ s.specification_version = 3
45
+
46
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
47
+ s.add_development_dependency(%q<thoughtbot-shoulda>, [">= 0"])
48
+ else
49
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
50
+ end
51
+ else
52
+ s.add_dependency(%q<thoughtbot-shoulda>, [">= 0"])
53
+ end
54
+ end
55
+
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: units-system
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Javier Goizueta
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-12-18 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: thoughtbot-shoulda
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Experimental unit conversion & arithmetic for Ruby 1.9
26
+ email: jgoizueta@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION
41
+ - lib/units-system.rb
42
+ - test/helper.rb
43
+ - test/test_units-system.rb
44
+ - units-system.gemspec
45
+ has_rdoc: true
46
+ homepage: http://github.com/jgoizueta/units-system
47
+ licenses: []
48
+
49
+ post_install_message:
50
+ rdoc_options:
51
+ - --charset=UTF-8
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ version: 1.9.1
59
+ version:
60
+ required_rubygems_version: !ruby/object:Gem::Requirement
61
+ requirements:
62
+ - - ">="
63
+ - !ruby/object:Gem::Version
64
+ version: "0"
65
+ version:
66
+ requirements: []
67
+
68
+ rubyforge_project:
69
+ rubygems_version: 1.3.5
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: Arithmetic with units of measure
73
+ test_files:
74
+ - test/helper.rb
75
+ - test/test_units-system.rb