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.
- 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
|