unit 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown ADDED
@@ -0,0 +1,21 @@
1
+ README
2
+ ======
3
+
4
+ Units introduces computation with units to ruby.
5
+
6
+ Usage
7
+ -----
8
+
9
+ require 'unit'
10
+ puts 1.meter.in_kilometer
11
+ puts 1.MeV.in_joule
12
+ puts 10.KiB / 1.second
13
+ puts 10.KiB_per_second
14
+ puts Unit('1 m/s^2')
15
+
16
+ See the test cases for more examples.
17
+
18
+ Authors
19
+ -------
20
+
21
+ Daniel Mendler
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ task :default => :test
2
+
3
+ desc 'Run tests with bacon'
4
+ task :test => FileList['test/*_test.rb'] do |t|
5
+ sh "bacon -q -Ilib:test #{t.prerequisites.join(' ')}"
6
+ end
@@ -0,0 +1,41 @@
1
+ units:
2
+ bit:
3
+ sym: bit
4
+ def: bit
5
+ byte:
6
+ sym: B
7
+ def: 8 * bit
8
+
9
+ prefixes:
10
+ kibi:
11
+ sym: Ki
12
+ base: 2
13
+ exp: 10
14
+ mebi:
15
+ sym: Mi
16
+ base: 2
17
+ exp: 20
18
+ gibi:
19
+ sym: Gi
20
+ base: 2
21
+ exp: 30
22
+ tebi:
23
+ sym: Ti
24
+ base: 2
25
+ exp: 40
26
+ pebi:
27
+ sym: Pi
28
+ base: 2
29
+ exp: 50
30
+ exbi:
31
+ sym: Ei
32
+ base: 2
33
+ exp: 60
34
+ zebi:
35
+ sym: Zi
36
+ base: 2
37
+ exp: 70
38
+ yobi:
39
+ sym: Yi
40
+ base: 2
41
+ exp: 80
@@ -0,0 +1,13 @@
1
+ units:
2
+ pi:
3
+ sym: π
4
+ def: 3.14159265358979323846
5
+ degree:
6
+ sym: [°, deg]
7
+ def: pi radiant / 180
8
+ arcminute:
9
+ sym: \'
10
+ def: degree / 60
11
+ arcsecond:
12
+ sym: \"
13
+ def: arcminute / 60
@@ -0,0 +1,37 @@
1
+ units:
2
+ mil:
3
+ sym: mil
4
+ def: 25400 nanometer
5
+ inch:
6
+ sym: [in, inches]
7
+ def: 1000 mil
8
+ foot:
9
+ sym: [ft, feet]
10
+ def: 12 inch
11
+ yard:
12
+ sym: yd
13
+ def: 3 foot
14
+ mile:
15
+ sym: mi
16
+ def: 5280 foot
17
+ nauticalmile:
18
+ sym: nmi
19
+ def: 6080 foot
20
+
21
+ acre:
22
+ sym: ac
23
+ def: 43560 foot^2
24
+
25
+ ounce:
26
+ sym: oz
27
+ def: 28349523125 nanogram
28
+ pound:
29
+ sym: lb
30
+ def: 16 ounce
31
+
32
+ mph:
33
+ sym: mph
34
+ def: mile / hour
35
+ knot:
36
+ sym: kt
37
+ def: nauticalmile / hour
@@ -0,0 +1,22 @@
1
+ units:
2
+ are:
3
+ sym: a
4
+ def: 100 m^2
5
+
6
+ ton:
7
+ sym: t
8
+ def: 1 megagram
9
+ carat:
10
+ def: 200 milligram
11
+
12
+ liter:
13
+ sym: [l, L]
14
+ def: 1 decimeter^3
15
+
16
+ calorie:
17
+ sym: cal
18
+ def: 4184 millijoule
19
+
20
+ fahrenheit:
21
+ sym: [°F, ℉]
22
+ def: 1.8 celsius
@@ -0,0 +1,25 @@
1
+ units:
2
+ astronomicalunit:
3
+ sym: [au, AU, a.u., ua]
4
+ def: 149597900000 meter
5
+ lightsecond:
6
+ sym: ls
7
+ def: 299792500 meter
8
+ lightminute:
9
+ sym: lm
10
+ def: 17987550000 meter
11
+ lightyear:
12
+ sym: ly
13
+ def: 9460528000000000 meter
14
+ parsec:
15
+ sym: pc
16
+ def: 30856780000000000 meter
17
+ angstroem:
18
+ sym: Å
19
+ def: 100 picometer
20
+ electronvolt:
21
+ sym: eV
22
+ def: 1.602176487e-19 joule
23
+ atomicmass:
24
+ sym: [u, Da]
25
+ def: 1.660538782e-27 kilogram
@@ -0,0 +1,175 @@
1
+ units:
2
+ # SI base units
3
+ meter:
4
+ sym: m
5
+ def: meter
6
+ # use gram instead of kilogram to avoid millikilogram etc
7
+ gram:
8
+ sym: g
9
+ def: gram
10
+ second:
11
+ sym: s
12
+ def: second
13
+ ampere:
14
+ sym: A
15
+ def: ampere
16
+ kelvin:
17
+ sym: K
18
+ def: kelvin
19
+ mole:
20
+ sym: mol
21
+ def: mole
22
+ candela:
23
+ sym: cd
24
+ def: candela
25
+
26
+ # Derived SI units
27
+ radiant:
28
+ sym: [rad, radian]
29
+ def: 1
30
+ steradiant:
31
+ sym: sr
32
+ def: 1
33
+ hertz:
34
+ sym: Hz
35
+ def: 1 / second
36
+ newton:
37
+ sym: N
38
+ def: kilogram * meter / second ^ 2
39
+ pascal:
40
+ sym: Pa
41
+ def: newton / meter ^ 2
42
+ joule:
43
+ sym: J
44
+ def: newton * meter
45
+ watt:
46
+ sym: W
47
+ def: joule * second
48
+ coloumb:
49
+ sym: C
50
+ def: ampere * second
51
+ volt:
52
+ sym: V
53
+ def: joule / coloumb
54
+ farad:
55
+ sym: F
56
+ def: coloumb / volt
57
+ ohm:
58
+ sym: Ω
59
+ def: volt / ampere
60
+ siemens:
61
+ sym: S
62
+ def: 1 / ohm
63
+ weber:
64
+ sym: Wb
65
+ def: volt / second
66
+ tesla:
67
+ sym: T
68
+ def: weber / meter ^ 2
69
+ henry:
70
+ sym: H
71
+ def: weber / ampere
72
+ celsius:
73
+ sym: [°C, ℃]
74
+ def: kelvin
75
+ lumen:
76
+ sym: lm
77
+ def: candela * steradiant
78
+ lux:
79
+ sym: lx
80
+ def: lm / meter ^ 2
81
+ becquerel:
82
+ sym: Bq
83
+ def: hertz
84
+ gray:
85
+ sym: Gy
86
+ def: joule / kilogram
87
+ sievert:
88
+ sym: Sv
89
+ def: gray
90
+ katal:
91
+ sym: kat
92
+ def: mole / second
93
+
94
+ prefixes:
95
+ # SI prefices
96
+ yotta:
97
+ sym: Y
98
+ base: 10
99
+ exp: 24
100
+ zetta:
101
+ sym: Z
102
+ base: 10
103
+ exp: 21
104
+ exa:
105
+ sym: E
106
+ base: 10
107
+ exp: 18
108
+ peta:
109
+ sym: P
110
+ base: 10
111
+ exp: 15
112
+ tera:
113
+ sym: T
114
+ base: 10
115
+ exp: 12
116
+ giga:
117
+ sym: G
118
+ base: 10
119
+ exp: 9
120
+ mega:
121
+ sym: M
122
+ base: 10
123
+ exp: 6
124
+ kilo:
125
+ sym: k
126
+ base: 10
127
+ exp: 3
128
+ hecto:
129
+ sym: h
130
+ base: 10
131
+ exp: 2
132
+ deca:
133
+ sym: da
134
+ base: 10
135
+ exp: 1
136
+ deci:
137
+ sym: d
138
+ base: 10
139
+ exp: -1
140
+ centi:
141
+ sym: c
142
+ base: 10
143
+ exp: -2
144
+ milli:
145
+ sym: m
146
+ base: 10
147
+ exp: -3
148
+ micro:
149
+ sym: µ
150
+ base: 10
151
+ exp: -6
152
+ nano:
153
+ sym: n
154
+ base: 10
155
+ exp: -9
156
+ pico:
157
+ sym: p
158
+ base: 10
159
+ exp: -12
160
+ femto:
161
+ sym: f
162
+ base: 10
163
+ exp: -15
164
+ atto:
165
+ sym: a
166
+ base: 10
167
+ exp: -18
168
+ zepto:
169
+ sym: z
170
+ base: 10
171
+ exp: -21
172
+ yokto:
173
+ sym: y
174
+ base: 10
175
+ exp: -24
@@ -0,0 +1,15 @@
1
+ units:
2
+ minute:
3
+ sym: min
4
+ def: 60 * second
5
+ hour:
6
+ sym: h
7
+ def: 60 * minute
8
+ day:
9
+ sym: d
10
+ def: 24 * hour
11
+ week:
12
+ def: 7 * day
13
+ year:
14
+ sym: a
15
+ def: 365 * day
data/lib/unit.rb ADDED
@@ -0,0 +1,482 @@
1
+ # encoding: utf-8
2
+ require 'yaml'
3
+
4
+ class Unit < Numeric
5
+ VERSION = '0.2.0'
6
+
7
+ attr_reader :numerator, :denominator, :unit, :normalized, :system
8
+
9
+ def initialize(numerator, denominator, unit, system)
10
+ @system = system
11
+ @numerator = numerator
12
+ @denominator = denominator
13
+ @unit = unit.dup
14
+ @normalized = nil
15
+ reduce!
16
+ end
17
+
18
+ def initialize_copy(other)
19
+ @system = other.system
20
+ @numerator = other.numerator
21
+ @denominator = other.denominator
22
+ @unit = other.unit.dup
23
+ @normalized = other.normalized
24
+ end
25
+
26
+ # Converts to base units
27
+ def normalize
28
+ @normalized ||= dup.normalize!
29
+ end
30
+
31
+ # Converts to base units
32
+ def normalize!
33
+ if @normalized != self
34
+ begin
35
+ last_unit = @unit
36
+ @unit = []
37
+ last_unit.each do |prefix, unit, exp|
38
+ if prefix != :one
39
+ if exp >= 0
40
+ @numerator *= @system.prefix[prefix][:value] ** exp
41
+ else
42
+ @denominator *= @system.prefix[prefix][:value] ** -exp
43
+ end
44
+ end
45
+ if @system.unit[unit]
46
+ @unit += Unit.power_unit(@system.unit[unit][:def], exp)
47
+ else
48
+ @unit << [:one, unit, exp]
49
+ end
50
+ end
51
+ end while last_unit != @unit
52
+ reduce!
53
+ @normalized = self
54
+ end
55
+ self
56
+ end
57
+
58
+ def *(other)
59
+ a, b = coerce(other)
60
+ Unit.new(a.numerator * b.numerator, a.denominator * b.denominator, a.unit + b.unit, system)
61
+ end
62
+
63
+ def /(other)
64
+ a, b = coerce(other)
65
+ Unit.new(a.numerator * b.denominator, a.denominator * b.numerator, a.unit + Unit.power_unit(b.unit, -1), system)
66
+ end
67
+
68
+ def +(other)
69
+ raise TypeError, 'Incompatible units' if !compatible?(other)
70
+ a, b = coerce(other)
71
+ a, b = a.normalize, b.normalize
72
+ Unit.new(a.numerator * b.denominator + b.numerator * a.denominator, a.denominator * b.denominator, a.unit, system).in(self)
73
+ end
74
+
75
+ def **(exp)
76
+ raise TypeError if Unit === exp
77
+ Unit.new(numerator ** exp, denominator ** exp, Unit.power_unit(unit, exp), system)
78
+ end
79
+
80
+ def -(other)
81
+ self + (-other)
82
+ end
83
+
84
+ def -@
85
+ Unit.new(-numerator, denominator, unit, system)
86
+ end
87
+
88
+ def ==(other)
89
+ a, b = coerce(other)
90
+ a, b = a.normalize, b.normalize
91
+ a.numerator == b.numerator && a.denominator == b.denominator && a.unit == b.unit
92
+ end
93
+
94
+ # Number without dimension
95
+ def dimensionless?
96
+ normalize.unit.empty?
97
+ end
98
+
99
+ alias unitless? dimensionless?
100
+
101
+ # Compatible units can be added
102
+ def compatible?(other)
103
+ a, b = coerce(other)
104
+ a, b = a.normalize, b.normalize
105
+ (a.unit == b.unit) || (a == 0 || b == 0)
106
+ end
107
+
108
+ alias compatible_with? compatible?
109
+
110
+ # Convert to other unit
111
+ def in(unit)
112
+ unit = unit.to_unit(system)
113
+ (self / unit).normalize * unit
114
+ end
115
+
116
+ def inspect
117
+ unit.empty? ? %{Unit("#{number_string}")} : %{Unit("#{number_string} #{unit_string('.')}")}
118
+ end
119
+
120
+ def to_s
121
+ unit.empty? ? number_string : "#{number_string} #{unit_string('·')}"
122
+ end
123
+
124
+ def to_tex
125
+ unit.empty? ? number_string : "\SI{#{number_string}}{#{unit_string('.')}}"
126
+ end
127
+
128
+ def to_i
129
+ (@numerator / @denominator).to_i
130
+ end
131
+
132
+ def to_f
133
+ @numerator.to_f / @denominator.to_f
134
+ end
135
+
136
+ def approx
137
+ to_f.unit(unit)
138
+ end
139
+
140
+ def to_unit(system = nil)
141
+ system ||= Unit::System::DEFAULT
142
+ raise TypeError, 'Different unit system' if @system != system
143
+ self
144
+ end
145
+
146
+ def coerce(val)
147
+ raise TypeError, 'No unit support' if !val.respond_to? :to_unit
148
+ [self, val.to_unit(system)]
149
+ end
150
+
151
+ def method_missing(name)
152
+ if name.to_s[0..2] == 'in_'
153
+ self.in(Unit.method_name_to_unit(name))
154
+ else
155
+ super
156
+ end
157
+ end
158
+
159
+ def self.method_name_to_unit(name)
160
+ name.to_s.sub(/^in_/, '').sub(/^per_/, '1/').gsub('_per_', '/').gsub('_', ' ')
161
+ end
162
+
163
+ private
164
+
165
+ def number_string
166
+ @numerator.to_s << (@denominator == 1 ? '' : "/#{@denominator}")
167
+ end
168
+
169
+ def unit_string(sep)
170
+ (unit_list(@unit.select {|prefix, name, exp| exp >= 0 }) +
171
+ unit_list(@unit.select {|prefix, name, exp| exp < 0 })).join(sep)
172
+ end
173
+
174
+ def unit_list(list)
175
+ units = []
176
+ list.each do |prefix, name, exp|
177
+ unit = ''
178
+ unit << (@system.prefix[prefix] ? @system.prefix[prefix][:symbol] : prefix.to_s) if prefix != :one
179
+ unit << (@system.unit[name] ? @system.unit[name][:symbol] : name.to_s)
180
+ unit << '^' << exp.to_s if exp != 1
181
+ units << unit
182
+ end
183
+ units.sort
184
+ end
185
+
186
+ def self.power_unit(unit, pow)
187
+ unit.map {|prefix, name, exp| [prefix, name, exp * pow] }
188
+ end
189
+
190
+ # Reduce units and prefixes
191
+ def reduce!
192
+ # Remove numbers from units
193
+ numbers = @unit.select {|prefix, unit, exp| Numeric === unit }
194
+ @unit -= numbers
195
+ numbers.each do |prefix, number, exp|
196
+ raise RuntimeError, 'Numeric unit with prefix' if prefix != :one
197
+ if exp >= 0
198
+ @numerator *= number ** exp
199
+ else
200
+ @denominator *= number ** -exp
201
+ end
202
+ end
203
+
204
+ # Reduce number
205
+ if Integer === @numerator && Integer === @denominator
206
+ r = Rational(@numerator, @denominator)
207
+ @numerator = r.numerator
208
+ @denominator = r.denominator
209
+ else
210
+ r = @numerator / @denominator
211
+ if Rational === r
212
+ @numerator = r.numerator
213
+ @denominator = r.denominator
214
+ else
215
+ @numerator = r
216
+ @denominator = 1
217
+ end
218
+ end
219
+
220
+ if @numerator == 0
221
+ @denominator = 1
222
+ @unit.clear
223
+ end
224
+
225
+ # Reduce units
226
+ @unit.sort!
227
+ i, current = 1, 0
228
+ while i < @unit.size do
229
+ while i < @unit.size && @unit[current][0] == @unit[i][0] && @unit[current][1] == @unit[i][1]
230
+ @unit[current] = @unit[current].dup
231
+ @unit[current][2] += @unit[i][2]
232
+ i += 1
233
+ end
234
+ if @unit[current][2] == 0
235
+ @unit.slice!(current, i - current)
236
+ else
237
+ @unit.slice!(current + 1, i - current - 1)
238
+ current += 1
239
+ end
240
+ i = current + 1
241
+ end
242
+
243
+ # Reduce prefixes
244
+ @unit.each_with_index do |(prefix1, unit1, exp1), k|
245
+ next if exp1 < 0
246
+ @unit.each_with_index do |(prefix2, unit2, exp2), j|
247
+ if exp2 < 0 && exp2 == -exp1
248
+ q, r = @system.prefix[prefix1][:value].divmod @system.prefix[prefix2][:value]
249
+ if r == 0 && new_prefix = @system.prefix_value[q]
250
+ @unit[k] = @unit[k].dup
251
+ @unit[j] = @unit[j].dup
252
+ @unit[k][0] = new_prefix
253
+ @unit[j][0] = :one
254
+ end
255
+ end
256
+ end
257
+ end
258
+
259
+ self
260
+ end
261
+
262
+ public
263
+
264
+ class System
265
+ attr_reader :name, :unit, :unit_symbol, :prefix, :prefix_symbol, :prefix_value
266
+
267
+ def initialize(name, &block)
268
+ @name = name
269
+ @unit = {}
270
+ @unit_symbol = {}
271
+
272
+ # one is internal trivial prefix
273
+ @prefix = {:one => {:symbol => 'one', :value => 1} }
274
+ @prefix_symbol = {'one' => :one}
275
+ @prefix_value = {1 => :one}
276
+
277
+ block.call(self) if block
278
+ end
279
+
280
+ def load(filename)
281
+ data = YAML.load_file(File.join(File.dirname(__FILE__), 'systems', "#{filename}.yml"))
282
+
283
+ (data['prefixes'] || {}).each do |name, prefix|
284
+ name = name.to_sym
285
+ symbols = [prefix['sym'] || []].flatten
286
+ base = prefix['base']
287
+ exp = prefix['exp']
288
+ value = base ** exp
289
+ $stderr.puts "Prefix #{name} already defined" if @prefix[name]
290
+ @prefix[name] = { :symbol => symbols.first, :value => value }
291
+ symbols.each do |sym|
292
+ $stderr.puts "Prefix symbol #{sym} for #{name} already defined" if @prefix_symbol[name]
293
+ @prefix_symbol[sym] = name
294
+ end
295
+ @prefix_symbol[name.to_s] = @prefix_value[value] = name
296
+ end
297
+
298
+ (data['units'] || {}).each do |name, unit|
299
+ name = name.to_sym
300
+ symbols = [unit['sym'] || []].flatten
301
+ $stderr.puts "Unit #{name} already defined" if @unit[name]
302
+ @unit[name] = { :symbol => symbols.first, :def => parse_unit(unit['def']) }
303
+ symbols.each do |sym|
304
+ $stderr.puts "Unit symbol #{sym} for #{name} already defined" if @unit_symbol[name]
305
+ @unit_symbol[sym] = name
306
+ end
307
+ @unit_symbol[name.to_s] = name
308
+ end
309
+
310
+ @unit.each {|name, unit| validate_unit(unit[:def]) }
311
+ end
312
+
313
+ def validate_unit(units)
314
+ units.each do |prefix, unit, exp|
315
+ #raise TypeError, 'Prefix must be symbol' if !(Symbol === prefix)
316
+ #raise TypeError, 'Unit must be symbol' if !(Numeric === unit || Symbol === unit)
317
+ #raise TypeError, 'Exponent must be numeric' if !(Numeric === exp)
318
+ raise TypeError, "Undefined prefix #{prefix}" if !@prefix[prefix]
319
+ raise TypeError, "Undefined unit #{unit}" if !(Numeric === unit || @unit[unit])
320
+ end
321
+ end
322
+
323
+ def parse_unit(expr)
324
+ stack, result, implicit_mul = [], [], false
325
+ expr.to_s.scan(TOKENIZER).each do |tok|
326
+ if tok == '('
327
+ stack << '('
328
+ implicit_mul = false
329
+ elsif tok == ')'
330
+ compute(result, stack.pop) while !stack.empty? && stack.last != '('
331
+ raise(SyntaxError, 'Unexpected token )') if stack.empty?
332
+ stack.pop
333
+ implicit_mul = true
334
+ elsif OPERATOR.key?(tok)
335
+ compute(result, stack.pop) while !stack.empty? && stack.last != '(' && OPERATOR[stack.last][1] >= OPERATOR[tok][1]
336
+ stack << OPERATOR[tok][0]
337
+ implicit_mul = false
338
+ else
339
+ val = case tok
340
+ when REAL then [[:one, tok.to_f, 1]]
341
+ when DEC then [[:one, tok.to_i, 1]]
342
+ when SYMBOL then symbol_to_unit(tok)
343
+ end
344
+ stack << '*' if implicit_mul
345
+ implicit_mul = true
346
+ result << val
347
+ end
348
+ end
349
+ compute(result, stack.pop) while !stack.empty?
350
+ result.last
351
+ end
352
+
353
+ private
354
+
355
+ REAL = /^-?(?:(?:\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+)$/
356
+ DEC = /^-?\d+$/
357
+ SYMBOL = /^[a-zA-Z_°'"][\w_°'"]*$/
358
+ OPERATOR = { '/' => ['/', 1], '*' => ['*', 1], '·' => ['*', 1], '^' => ['^', 2], '**' => ['^', 2] }
359
+ OPERATOR_TOKENS = OPERATOR.keys.sort_by {|x| -x.size }. map {|x| Regexp.quote(x) }
360
+ VALUE_TOKENS = [REAL.source[1..-2], DEC.source[1..-2], SYMBOL.source[1..-2]]
361
+ TOKENIZER = Regexp.new((OPERATOR_TOKENS + VALUE_TOKENS + ['\\(', '\\)']).join('|'))
362
+
363
+ def lookup_symbol(symbol)
364
+ if unit_symbol[symbol]
365
+ [[:one, unit_symbol[symbol], 1]]
366
+ else
367
+ found = prefix_symbol.keys.find do |sym|
368
+ symbol[0..sym.size-1] == sym && unit_symbol[symbol[sym.size..-1]]
369
+ end
370
+ [[prefix_symbol[found], unit_symbol[symbol[found.size..-1]], 1]] if found
371
+ end
372
+ end
373
+
374
+ def symbol_to_unit(symbol)
375
+ lookup_symbol(symbol) ||
376
+ (symbol[-1..-1] == 's' ? lookup_symbol(symbol[0..-2]) : nil) || # Try english plural
377
+ [[:one, symbol.to_sym, 1]]
378
+ end
379
+
380
+ def compute(result, op)
381
+ b = result.pop
382
+ a = result.pop
383
+ result << case op
384
+ when '*' then a + b
385
+ when '/' then a + Unit.power_unit(b, -1)
386
+ when '^' then Unit.power_unit(a, b[0][1])
387
+ else raise SyntaxError, "Unexpected token #{op}"
388
+ end
389
+ end
390
+
391
+ public
392
+
393
+ SI = new('SI') do |system|
394
+ system.load(:si)
395
+ system.load(:binary)
396
+ system.load(:degree)
397
+ system.load(:time)
398
+ end
399
+
400
+ DEFAULT = SI
401
+ end
402
+ end
403
+
404
+ def Unit(*args)
405
+ numerator = Numeric === args.first ? args.shift : 1
406
+ denominator = Numeric === args.first ? args.shift : 1
407
+
408
+ system = args.index {|x| Unit::System === x }
409
+ system = system ? args.delete_at(system) : Unit::System::DEFAULT
410
+
411
+ unit = args.index {|x| String === x }
412
+ unit = system.parse_unit(args.delete_at(unit)) if unit
413
+
414
+ unless unit
415
+ unit = args.index {|x| Array === x }
416
+ unit = args.delete_at(unit) if unit
417
+ end
418
+
419
+ unit ||= []
420
+ system.validate_unit(unit)
421
+
422
+ raise ArgumentError, 'wrong number of arguments' unless args.empty?
423
+
424
+ Unit.new(numerator, denominator, unit, system)
425
+ end
426
+
427
+ class Numeric
428
+ def to_unit(system = nil)
429
+ system ||= Unit::System::DEFAULT
430
+ Unit.new(self, 1, [], system)
431
+ end
432
+
433
+ def method_missing(name, *args)
434
+ Unit.method_name_to_unit(name).to_unit(*args) * self
435
+ rescue TypeError => ex
436
+ super
437
+ end
438
+
439
+ def unit(unit, system = nil)
440
+ unit.to_unit(system) * self
441
+ end
442
+ end
443
+
444
+ class Rational
445
+ def to_unit(system = nil)
446
+ system ||= Unit::System::DEFAULT
447
+ Unit.new(numerator, denominator, [], system)
448
+ end
449
+ end
450
+
451
+ class String
452
+ def to_unit(system = nil)
453
+ system ||= Unit::System::DEFAULT
454
+ unit = system.parse_unit(self)
455
+ system.validate_unit(unit)
456
+ Unit.new(1, 1, unit, system)
457
+ end
458
+ end
459
+
460
+ class Symbol
461
+ def to_unit(system = nil)
462
+ to_s.to_unit(system)
463
+ end
464
+ end
465
+
466
+ class Array
467
+ def to_unit(system = nil)
468
+ system ||= Unit::System::DEFAULT
469
+ system.validate_unit(self)
470
+ Unit.new(1, 1, self, system)
471
+ end
472
+ end
473
+
474
+ # Units use symbols which must be sortable (Fix for Ruby 1.8)
475
+ if !:test.respond_to? :<=>
476
+ class Symbol
477
+ include Comparable
478
+ def <=>(other)
479
+ self.to_i <=> other.to_i
480
+ end
481
+ end
482
+ end
data/lib/units.rb ADDED
@@ -0,0 +1 @@
1
+ require 'unit'
data/test/unit_test.rb ADDED
@@ -0,0 +1,109 @@
1
+ # encoding: utf-8
2
+ require 'unit'
3
+
4
+ Unit::System::DEFAULT.load(:scientific)
5
+ Unit::System::DEFAULT.load(:imperial)
6
+ Unit::System::DEFAULT.load(:misc)
7
+
8
+ describe 'Unit' do
9
+ it 'should support multiplication' do
10
+ (Unit(2, 'm') * Unit(3, 'm')).should.equal Unit(6, 'm^2')
11
+ (Unit(2, 'm') * 3).should.equal Unit(6, 'm')
12
+ (Unit(2, 'm') * Rational(3, 4)).should.equal Unit(3, 2, 'm')
13
+ (Unit(2, 'm') * 0.5).should.equal Unit(1.0, 'm')
14
+ end
15
+
16
+ it 'should support division' do
17
+ (Unit(2, 'm') / Unit(3, 'm^2')).should.equal Unit(2, 3, '1/m')
18
+ (Unit(2, 'm') / 3).should.equal Unit(2, 3, 'm')
19
+ (Unit(2, 'm') / Rational(3, 4)).should.equal Unit(8, 3, 'm')
20
+ (Unit(2, 'm') / 0.5).should.equal Unit(4.0, 'm')
21
+ end
22
+
23
+ it 'should support addition' do
24
+ (Unit(42, 'm') + Unit(1, 'km')).should.equal Unit(1042, 'm')
25
+ (Unit(1, 'm') - Unit(1, 'cm')).should.equal Unit(99, 100, 'm')
26
+ end
27
+
28
+ it 'should check unit compatiblity' do
29
+ should.raise TypeError do
30
+ (Unit(42, 'm') + Unit(1, 's'))
31
+ end
32
+ should.raise TypeError do
33
+ (Unit(42, 'g') + Unit(1, 'm'))
34
+ end
35
+ end
36
+
37
+ it 'should support exponentiation' do
38
+ (Unit(2, 'm') ** 3).should.equal Unit(8, 'm^3')
39
+ (Unit(9, 'm^2') ** 0.5).should.equal Unit(3.0, 'm')
40
+ (Unit(9, 'm^2') ** Rational(1, 2)).should.equal Unit(3, 'm')
41
+ (Unit(2, 'm') ** 1.3).should.equal Unit(2 ** 1.3, 'm^1.3')
42
+ end
43
+
44
+ it 'should not allow units as exponent' do
45
+ should.raise TypeError do
46
+ Unit(42, 'g') ** Unit(1, 'm')
47
+ end
48
+ end
49
+
50
+ it 'should provide method sugar' do
51
+ 1.meter.should.equal Unit('1 meter')
52
+ 1.meter_per_second.should.equal Unit('1 m/s')
53
+ 1.meter.in_kilometer.should.equal Unit('1 m').in('km')
54
+ 1.unit('°C').should.equal Unit(1, '°C')
55
+ end
56
+
57
+ it 'should have a normalizer' do
58
+ 1.joule.normalize.should.equal Unit(1000, 'gram meter^2 / second^2')
59
+ unit = 1.joule.normalize!
60
+ unit.should.equal unit.normalized
61
+ end
62
+
63
+ it 'should convert units' do
64
+ 1.MeV.in_joule.should.equal Unit(1.602176487e-13, 'joule')
65
+ 1.kilometer.in_meter.should.equal Unit(1000, 'meter')
66
+ 1.liter.in('meter^3').should.equal Unit(1, 1000, 'meter^3')
67
+ 1.kilometer_per_hour.in_meter_per_second.should.equal Unit(5, 18, 'meter/second')
68
+ end
69
+
70
+ it 'should have a working compatible? method' do
71
+ 7.meter.compatible?('kilogram').should.equal false
72
+ 3.parsec.compatible_with?('meter').should.equal true
73
+ end
74
+
75
+ it 'should have a pretty string representation' do
76
+ 7.joule.normalize.to_s.should.equal '7000 g·m^2·s^-2'
77
+ end
78
+
79
+ it 'should parse units' do
80
+ Unit(1, 'KiB s^-1').unit.should.equal [[:kibi, :byte, 1], [:one, :second, -1]].sort
81
+ Unit(1, 'KiB/s').unit.should.equal [[:kibi, :byte, 1], [:one, :second, -1]].sort
82
+ Unit(1, 'kilometer^2 / megaelectronvolt^7 * gram centiliter').unit.should.equal [[:kilo, :meter, 2], [:mega, :electronvolt, -7],
83
+ [:one, :gram, 1], [:centi, :liter, 1]].sort
84
+ end
85
+
86
+ it 'should reduce units' do
87
+ 1.joule_per_kilogram.normalize.unit.should.equal [[:one, :meter, 2], [:one, :second, -2]].sort
88
+ 1.megaton_per_kilometer.unit.should.equal [[:kilo, :ton, 1], [:one, :meter, -1]].sort
89
+ end
90
+
91
+ it 'should work with floating point values' do
92
+ #w = (5.2).kilogram
93
+ w = 5.2 * Unit('kilogram')
94
+ w.in_pounds.to_int.should.equal 11
95
+ end
96
+
97
+ it 'should have dimensionless? method' do
98
+ 100.meter.per_km.should.be.dimensionless
99
+ 100.meter.per_km.should.be.unitless
100
+ 42.meter.per_second.should.not.be.unitless
101
+ 100.meter.per_km.should.equal Rational(1, 10).to_unit
102
+ end
103
+
104
+ it 'should be equal to rational if dimensionless' do
105
+ 100.meter.per_km.should.equal Rational(1, 10)
106
+ 100.meter.per_km.approx.should.equal 0.1
107
+ end
108
+ end
109
+
data/unit.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.dirname(__FILE__) + '/lib/unit'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = %q{unit}
6
+ s.version = Unit::VERSION
7
+
8
+ s.authors = ["Daniel Mendler"]
9
+ s.date = %q{2009-05-17}
10
+ s.email = ["mail@daniel-mendler.de"]
11
+ s.extra_rdoc_files = []
12
+
13
+ s.files = `git ls-files`.split("\n")
14
+ s.require_paths = %w(lib)
15
+
16
+ s.has_rdoc = true
17
+ s.rdoc_options = ["--main", "README.txt"]
18
+ s.require_paths = ["lib"]
19
+ s.rubyforge_project = s.name
20
+ s.summary = 'Scientific unit support for ruby for calculations'
21
+ s.homepage = %q{http://github.com/minad/si}
22
+
23
+ s.add_development_dependency('bacon')
24
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: unit
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.2.0
6
+ platform: ruby
7
+ authors:
8
+ - Daniel Mendler
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-05-17 00:00:00 +02:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: bacon
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :development
26
+ version_requirements: *id001
27
+ description:
28
+ email:
29
+ - mail@daniel-mendler.de
30
+ executables: []
31
+
32
+ extensions: []
33
+
34
+ extra_rdoc_files: []
35
+
36
+ files:
37
+ - README.markdown
38
+ - Rakefile
39
+ - lib/systems/binary.yml
40
+ - lib/systems/degree.yml
41
+ - lib/systems/imperial.yml
42
+ - lib/systems/misc.yml
43
+ - lib/systems/scientific.yml
44
+ - lib/systems/si.yml
45
+ - lib/systems/time.yml
46
+ - lib/unit.rb
47
+ - lib/units.rb
48
+ - test/unit_test.rb
49
+ - unit.gemspec
50
+ has_rdoc: true
51
+ homepage: http://github.com/minad/si
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --main
57
+ - README.txt
58
+ require_paths:
59
+ - lib
60
+ required_ruby_version: !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ requirements: []
73
+
74
+ rubyforge_project: unit
75
+ rubygems_version: 1.6.2
76
+ signing_key:
77
+ specification_version: 3
78
+ summary: Scientific unit support for ruby for calculations
79
+ test_files: []
80
+