unit 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/lib/unit.rb +5 -484
- data/lib/unit/class.rb +233 -0
- data/lib/unit/compatibility.rb +9 -0
- data/lib/unit/constructor.rb +22 -0
- data/lib/unit/dsl.rb +23 -0
- data/lib/unit/system.rb +157 -0
- data/lib/unit/systems/binary.yml +33 -0
- data/lib/{systems → unit/systems}/degree.yml +0 -0
- data/lib/{systems → unit/systems}/imperial.yml +0 -0
- data/lib/{systems → unit/systems}/misc.yml +0 -0
- data/lib/{systems → unit/systems}/scientific.yml +0 -0
- data/lib/{systems → unit/systems}/si.yml +41 -61
- data/lib/{systems → unit/systems}/time.yml +0 -0
- data/lib/unit/version.rb +3 -0
- data/test/error_test.rb +16 -0
- data/test/system_test.rb +24 -0
- data/test/unit_test.rb +40 -5
- data/test/yml/filename.yml +4 -0
- data/test/yml/io.yml +4 -0
- data/unit.gemspec +1 -1
- metadata +19 -9
- data/lib/systems/binary.yml +0 -41
- data/lib/units.rb +0 -1
data/lib/unit/class.rb
ADDED
@@ -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,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
|
data/lib/unit/dsl.rb
ADDED
@@ -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
|
data/lib/unit/system.rb
ADDED
@@ -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
|