units-system 0.0.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/.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