unit 0.2.1 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,233 @@
1
+ # -*- coding: utf-8 -*-
2
+ class Unit < Numeric
3
+ attr_reader :value, :normalized, :unit, :system
4
+
5
+ def initialize(value, unit, system)
6
+ @system = system
7
+ @value = value
8
+ @unit = unit.dup
9
+ @normalized = nil
10
+ reduce!
11
+ end
12
+
13
+ def initialize_copy(other)
14
+ @system = other.system
15
+ @value = other.value
16
+ @unit = other.unit.dup
17
+ @normalized = other.normalized
18
+ end
19
+
20
+ # Converts to base units
21
+ def normalize
22
+ @normalized ||= dup.normalize!
23
+ end
24
+
25
+ # Converts to base units
26
+ def normalize!
27
+ if @normalized != self
28
+ begin
29
+ last_unit = @unit
30
+ @unit = []
31
+ last_unit.each do |factor, unit, exp|
32
+ @value *= @system.factor[factor][:value] ** exp if factor != :one
33
+ if Numeric === unit
34
+ @unit << [:one, unit, exp]
35
+ else
36
+ @unit += Unit.power_unit(@system.unit[unit][:def], exp)
37
+ end
38
+ end
39
+ end while last_unit != @unit
40
+ reduce!
41
+ @normalized = self
42
+ end
43
+ self
44
+ end
45
+
46
+ def *(other)
47
+ a, b = coerce(other)
48
+ Unit.new(a.value * b.value, a.unit + b.unit, system)
49
+ end
50
+
51
+ def /(other)
52
+ a, b = coerce(other)
53
+ Unit.new(Integer === a.value && Integer === b.value ? Rational(a.value, b.value) : a.value / b.value,
54
+ a.unit + Unit.power_unit(b.unit, -1), system)
55
+ end
56
+
57
+ def +(other)
58
+ raise TypeError, "Incompatible units: #{self.inspect} and #{other.inspect}" if !compatible?(other)
59
+ a, b = coerce(other)
60
+ a, b = a.normalize, b.normalize
61
+ Unit.new(a.value + b.value, a.unit, system).in(self)
62
+ end
63
+
64
+ def **(exp)
65
+ raise TypeError if Unit === exp
66
+ Unit.new(value ** exp, Unit.power_unit(unit, exp), system)
67
+ end
68
+
69
+ def -(other)
70
+ self + (-other)
71
+ end
72
+
73
+ def -@
74
+ Unit.new(-value, unit, system)
75
+ end
76
+
77
+ def ==(other)
78
+ a, b = coerce(other)
79
+ a, b = a.normalize, b.normalize
80
+ a.value == b.value && a.unit == b.unit
81
+ end
82
+
83
+ def <=>(other)
84
+ a, b = coerce(other)
85
+ a, b = a.normalize, b.normalize
86
+ a.value <=> b.value if a.unit == b.unit
87
+ end
88
+
89
+ # Number without dimension
90
+ def dimensionless?
91
+ normalize.unit.empty?
92
+ end
93
+
94
+ alias unitless? dimensionless?
95
+
96
+ # Compatible units can be added
97
+ def compatible?(other)
98
+ a, b = coerce(other)
99
+ a, b = a.normalize, b.normalize
100
+ a.unit == b.unit
101
+ end
102
+
103
+ alias compatible_with? compatible?
104
+
105
+ # Convert to other unit
106
+ def in(unit)
107
+ a, b = coerce(unit)
108
+ conversion = Unit.new(1, b.unit, system)
109
+ (a / conversion).normalize * conversion
110
+ end
111
+
112
+ def inspect
113
+ unit.empty? ? %{Unit("#{value}")} : %{Unit("#{value} #{unit_string('.')}")}
114
+ end
115
+
116
+ def to_s
117
+ unit.empty? ? value.to_s : "#{value} #{unit_string('·')}"
118
+ end
119
+
120
+ def to_tex
121
+ unit.empty? ? value.to_s : "\SI{#{value}}{#{unit_string('.')}}"
122
+ end
123
+
124
+ def to_i
125
+ @value.to_i
126
+ end
127
+
128
+ def to_f
129
+ @value.to_f
130
+ end
131
+
132
+ def approx
133
+ to_f.unit(unit)
134
+ end
135
+
136
+ def coerce(val)
137
+ [self, Unit.to_unit(val, system)]
138
+ end
139
+
140
+ def self.to_unit(object, system = nil)
141
+ system ||= Unit.default_system
142
+ case object
143
+ when Unit
144
+ raise TypeError, 'Different unit system' if object.system != system
145
+ object
146
+ when Array
147
+ system.validate_unit(object)
148
+ Unit.new(1, object, system)
149
+ when String, Symbol
150
+ unit = system.parse_unit(object.to_s)
151
+ system.validate_unit(unit)
152
+ Unit.new(1, unit, system)
153
+ when Numeric
154
+ Unit.new(object, [], system)
155
+ else
156
+ raise TypeError, "#{object.class} has no unit support"
157
+ end
158
+ end
159
+
160
+ private
161
+
162
+ def unit_string(sep)
163
+ (unit_list(@unit.select {|factor, name, exp| exp >= 0 }) +
164
+ unit_list(@unit.select {|factor, name, exp| exp < 0 })).join(sep)
165
+ end
166
+
167
+ def unit_list(list)
168
+ units = []
169
+ list.each do |factor, name, exp|
170
+ unit = ''
171
+ unit << @system.factor[factor][:symbol] if factor != :one
172
+ unit << @system.unit[name][:symbol]
173
+ unit << '^' << exp.to_s if exp != 1
174
+ units << unit
175
+ end
176
+ units.sort
177
+ end
178
+
179
+ def self.power_unit(unit, pow)
180
+ unit.map {|factor, name, exp| [factor, name, exp * pow] }
181
+ end
182
+
183
+ # Reduce units and factors
184
+ def reduce!
185
+ # Remove numbers from units
186
+ numbers = @unit.select {|factor, unit, exp| Numeric === unit }
187
+ @unit -= numbers
188
+ numbers.each do |factor, number, exp|
189
+ raise RuntimeError, 'Numeric unit with factor' if factor != :one
190
+ @value *= number ** exp
191
+ end
192
+
193
+ # Reduce units
194
+ @unit.sort!
195
+ i, current = 1, 0
196
+ while i < @unit.size do
197
+ while i < @unit.size && @unit[current][0] == @unit[i][0] && @unit[current][1] == @unit[i][1]
198
+ @unit[current] = @unit[current].dup
199
+ @unit[current][2] += @unit[i][2]
200
+ i += 1
201
+ end
202
+ if @unit[current][2] == 0
203
+ @unit.slice!(current, i - current)
204
+ else
205
+ @unit.slice!(current + 1, i - current - 1)
206
+ current += 1
207
+ end
208
+ i = current + 1
209
+ end
210
+
211
+ # Reduce factors
212
+ @unit.each_with_index do |(factor1, unit1, exp1), k|
213
+ next if exp1 < 0
214
+ @unit.each_with_index do |(factor2, unit2, exp2), j|
215
+ if exp2 < 0 && exp2 == -exp1
216
+ q, r = @system.factor[factor1][:value].divmod @system.factor[factor2][:value]
217
+ if r == 0 && new_factor = @system.factor_value[q]
218
+ @unit[k] = @unit[k].dup
219
+ @unit[j] = @unit[j].dup
220
+ @unit[k][0] = new_factor
221
+ @unit[j][0] = :one
222
+ end
223
+ end
224
+ end
225
+ end
226
+
227
+ self
228
+ end
229
+
230
+ class<< self
231
+ attr_accessor :default_system
232
+ end
233
+ end
@@ -0,0 +1,9 @@
1
+ # Units use symbols which must be sortable (Fix for Ruby 1.8)
2
+ unless :test.respond_to? :<=>
3
+ class Symbol
4
+ include Comparable
5
+ def <=>(other)
6
+ self.to_i <=> other.to_i
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ def Unit(*args)
2
+ value = Numeric === args.first ? args.shift : 1
3
+ value = Rational(value, args.shift) if Numeric === args.first
4
+
5
+ system = args.index {|x| Unit::System === x }
6
+ system = system ? args.delete_at(system) : Unit.default_system
7
+
8
+ unit = args.index {|x| String === x }
9
+ unit = system.parse_unit(args.delete_at(unit)) if unit
10
+
11
+ unless unit
12
+ unit = args.index {|x| Array === x }
13
+ unit = args.delete_at(unit) if unit
14
+ end
15
+
16
+ unit ||= []
17
+ system.validate_unit(unit)
18
+
19
+ raise ArgumentError, 'wrong number of arguments' unless args.empty?
20
+
21
+ Unit.new(value, unit, system)
22
+ end
@@ -0,0 +1,23 @@
1
+ class Numeric
2
+ def unit(unit, system = nil)
3
+ Unit.to_unit(unit, system) * self
4
+ end
5
+
6
+ def method_missing(name, system = nil)
7
+ Unit.to_unit(Unit.method_name_to_unit(name), system) * self
8
+ end
9
+ end
10
+
11
+ class Unit < Numeric
12
+ def self.method_name_to_unit(name)
13
+ name.to_s.sub(/^per_/, '1/').gsub('_per_', '/').gsub('_', ' ')
14
+ end
15
+
16
+ def method_missing(name)
17
+ if name.to_s =~ /^in_/
18
+ self.in(Unit.method_name_to_unit($'))
19
+ else
20
+ Unit.to_unit(Unit.method_name_to_unit(name), system) * self
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,157 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'yaml'
3
+
4
+ class Unit < Numeric
5
+ class System
6
+ attr_reader :name, :unit, :unit_symbol, :factor, :factor_symbol, :factor_value
7
+
8
+ def initialize(name)
9
+ @name = name
10
+ @unit = {}
11
+ @unit_symbol = {}
12
+
13
+ # one is internal trivial factor
14
+ @factor = {:one => {:symbol => 'one', :value => 1} }
15
+ @factor_symbol = {'one' => :one}
16
+ @factor_value = {1 => :one}
17
+
18
+ yield(self) if block_given?
19
+ end
20
+
21
+ def load(input)
22
+ case input
23
+ when IO
24
+ data = YAML.load(input.read)
25
+ when String
26
+ if File.exist?(input)
27
+ data = YAML.load_file(input)
28
+ else
29
+ data = YAML.load_file(File.join(File.dirname(__FILE__), 'systems', "#{input}.yml"))
30
+ end
31
+ when Symbol
32
+ data = YAML.load_file(File.join(File.dirname(__FILE__), 'systems', "#{input}.yml"))
33
+ end
34
+
35
+ (data['factors'] || {}).each do |name, factor|
36
+ name = name.to_sym
37
+ symbols = [factor['sym'] || []].flatten
38
+ factor['def'] =~ /^(\d+)\^(-?\d+)$/
39
+ base = $1.to_i
40
+ exp = $2.to_i
41
+ value = base ** exp
42
+ $stderr.puts "Factor #{name} already defined" if @factor[name]
43
+ @factor[name] = { :symbol => symbols.first, :value => value }
44
+ symbols.each do |sym|
45
+ $stderr.puts "Factor symbol #{sym} for #{name} already defined" if @factor_symbol[name]
46
+ @factor_symbol[sym] = name
47
+ end
48
+ @factor_symbol[name.to_s] = @factor_value[value] = name
49
+ end
50
+
51
+ (data['units'] || {}).each do |name, unit|
52
+ name = name.to_sym
53
+ symbols = [unit['sym'] || []].flatten
54
+ $stderr.puts "Unit #{name} already defined" if @unit[name]
55
+ @unit[name] = { :symbol => symbols.first, :def => parse_unit(unit['def']) }
56
+ symbols.each do |sym|
57
+ $stderr.puts "Unit symbol #{sym} for #{name} already defined" if @unit_symbol[name]
58
+ @unit_symbol[sym] = name
59
+ end
60
+ @unit_symbol[name.to_s] = name
61
+ end
62
+
63
+ @unit.each {|name, unit| validate_unit(unit[:def]) }
64
+
65
+ true
66
+ end
67
+
68
+ def validate_unit(units)
69
+ units.each do |factor, unit, exp|
70
+ #raise TypeError, 'Factor must be symbol' if !(Symbol === factor)
71
+ #raise TypeError, 'Unit must be symbol' if !(Numeric === unit || Symbol === unit)
72
+ #raise TypeError, 'Exponent must be numeric' if !(Numeric === exp)
73
+ raise TypeError, "Undefined factor #{factor}" if !@factor[factor]
74
+ raise TypeError, "Undefined unit #{unit}" if !(Numeric === unit || @unit[unit])
75
+ end
76
+ end
77
+
78
+ def parse_unit(expr)
79
+ stack, result, implicit_mul = [], [], false
80
+ expr.to_s.scan(TOKENIZER).each do |tok|
81
+ if tok == '('
82
+ stack << '('
83
+ implicit_mul = false
84
+ elsif tok == ')'
85
+ compute(result, stack.pop) while !stack.empty? && stack.last != '('
86
+ raise(SyntaxError, 'Unexpected token )') if stack.empty?
87
+ stack.pop
88
+ implicit_mul = true
89
+ elsif OPERATOR.key?(tok)
90
+ compute(result, stack.pop) while !stack.empty? && stack.last != '(' && OPERATOR[stack.last][1] >= OPERATOR[tok][1]
91
+ stack << OPERATOR[tok][0]
92
+ implicit_mul = false
93
+ else
94
+ val = case tok
95
+ when REAL then [[:one, tok.to_f, 1]]
96
+ when DEC then [[:one, tok.to_i, 1]]
97
+ when SYMBOL then symbol_to_unit(tok)
98
+ end
99
+ stack << '*' if implicit_mul
100
+ implicit_mul = true
101
+ result << val
102
+ end
103
+ end
104
+ compute(result, stack.pop) while !stack.empty?
105
+ result.last
106
+ end
107
+
108
+ private
109
+
110
+ REAL = /^-?(?:(?:\d*\.\d+|\d+\.\d*)(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+)$/
111
+ DEC = /^-?\d+$/
112
+ SYMBOL = /^[a-zA-Z_°'"][\w_°'"]*$/
113
+ OPERATOR = { '/' => ['/', 1], '*' => ['*', 1], '·' => ['*', 1], '^' => ['^', 2], '**' => ['^', 2] }
114
+ OPERATOR_TOKENS = OPERATOR.keys.sort_by {|x| -x.size }. map {|x| Regexp.quote(x) }
115
+ VALUE_TOKENS = [REAL.source[1..-2], DEC.source[1..-2], SYMBOL.source[1..-2]]
116
+ TOKENIZER = Regexp.new((OPERATOR_TOKENS + VALUE_TOKENS + ['\\(', '\\)']).join('|'))
117
+
118
+ def lookup_symbol(symbol)
119
+ if unit_symbol[symbol]
120
+ [[:one, unit_symbol[symbol], 1]]
121
+ else
122
+ found = factor_symbol.keys.find do |sym|
123
+ symbol[0..sym.size-1] == sym && unit_symbol[symbol[sym.size..-1]]
124
+ end
125
+ [[factor_symbol[found], unit_symbol[symbol[found.size..-1]], 1]] if found
126
+ end
127
+ end
128
+
129
+ def symbol_to_unit(symbol)
130
+ lookup_symbol(symbol) ||
131
+ (symbol[-1..-1] == 's' ? lookup_symbol(symbol[0..-2]) : nil) || # Try english plural
132
+ [[:one, symbol.to_sym, 1]]
133
+ end
134
+
135
+ def compute(result, op)
136
+ b = result.pop
137
+ a = result.pop || raise(SyntaxError, "Unexpected token #{op}")
138
+ result << case op
139
+ when '*' then a + b
140
+ when '/' then a + Unit.power_unit(b, -1)
141
+ when '^' then Unit.power_unit(a, b[0][1])
142
+ else raise SyntaxError, "Unexpected token #{op}"
143
+ end
144
+ end
145
+
146
+ public
147
+
148
+ SI = new('SI') do |system|
149
+ system.load(:si)
150
+ system.load(:binary)
151
+ system.load(:degree)
152
+ system.load(:time)
153
+ end
154
+
155
+ Unit.default_system = SI
156
+ end
157
+ end
@@ -0,0 +1,33 @@
1
+ units:
2
+ bit:
3
+ sym: bit
4
+ def: bit
5
+ byte:
6
+ sym: B
7
+ def: 8 * bit
8
+
9
+ factors:
10
+ kibi:
11
+ sym: Ki
12
+ def: 2^10
13
+ mebi:
14
+ sym: Mi
15
+ def: 2^20
16
+ gibi:
17
+ sym: Gi
18
+ def: 2^30
19
+ tebi:
20
+ sym: Ti
21
+ def: 2^40
22
+ pebi:
23
+ sym: Pi
24
+ def: 2^50
25
+ exbi:
26
+ sym: Ei
27
+ def: 2^60
28
+ zebi:
29
+ sym: Zi
30
+ def: 2^70
31
+ yobi:
32
+ sym: Yi
33
+ def: 2^80