unit 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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