units-system 0.0.5 → 0.1.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/Gemfile +10 -0
- data/Gemfile.lock +29 -0
- data/README.rdoc +2 -2
- data/Rakefile +21 -30
- data/VERSION +1 -1
- data/lib/units-system.rb +8 -648
- data/lib/units/definitions.rb +99 -0
- data/lib/units/math.rb +49 -0
- data/lib/units/measure.rb +263 -0
- data/lib/units/prefixes.rb +42 -0
- data/lib/units/system.rb +236 -0
- data/test/test_units-system.rb +46 -1
- data/units-system.gemspec +40 -28
- metadata +109 -55
- data/.gitignore +0 -21
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Units
|
4
|
+
|
5
|
+
# Units definitions
|
6
|
+
|
7
|
+
# declare SI base units
|
8
|
+
si_units :mass, :kg # m
|
9
|
+
si_units :length, :m # l
|
10
|
+
si_units :time, :s # t
|
11
|
+
si_units :electric_current, :A # I
|
12
|
+
si_units :temperature, :K # T
|
13
|
+
si_units :luminous_intensity, :cd # Iv
|
14
|
+
si_units :amount_of_substance, :mol # n
|
15
|
+
|
16
|
+
# define base units
|
17
|
+
define :g, 'gram', :mass, 1E-3
|
18
|
+
define :m, 'meter', :length, 1.0
|
19
|
+
define :s, 'second', :time, 1.0
|
20
|
+
define :A, 'ampere', :electric_current, 1.0
|
21
|
+
define :K, 'kelvin', :temperature, 1.0
|
22
|
+
define :cd,'candela',:luminous_intensity, 1.0
|
23
|
+
define :mol,'mole', :amount_of_substance, 1.0
|
24
|
+
|
25
|
+
# declare derived quantities with no named units
|
26
|
+
si_units :speed, u{m/s}
|
27
|
+
si_units :acceleration, u{m/s**2}
|
28
|
+
si_units :area, u{m**2}
|
29
|
+
si_units :volume, u{m**3}
|
30
|
+
|
31
|
+
# derived quantities with named units
|
32
|
+
define :W, 'Watt', :power, u{kg*m**2/s**3} # J/s
|
33
|
+
define :Hz, 'herz', :frequency, u{1/s}
|
34
|
+
define :N, 'newton', :force, u{m*kg/s**2}
|
35
|
+
define :Pa, 'pascal', :pressure, u{N/m**2}
|
36
|
+
define :J, 'joule', :energy, u{N*m}
|
37
|
+
define :C, 'coulomb', :electric_charge, u{s*A}
|
38
|
+
define :V, 'volt', :voltage, u{W/A}
|
39
|
+
define :F, 'farad', :electric_capacitance, u{C/V}
|
40
|
+
define :Ω, 'ohm', :electric_resistance, u{V/A} # ohm: Ω omega: Ω
|
41
|
+
define :S, 'siemens', :electric_condctance, u{1/Ω}
|
42
|
+
define :Wb, 'weber', :magnetic_flux, u{J/A}
|
43
|
+
define :T, 'tesla', :magnetic_field, u{N/(A*m)}
|
44
|
+
define :H, 'henry', :inductance, u{Wb/A}
|
45
|
+
define :rad, 'radian', :angle, u{m/m}
|
46
|
+
define :sr, 'steradian', :solid_angle, u{m**2/m**2}
|
47
|
+
define :lm, 'lumen', :luminous_flux, u{cd*sr}
|
48
|
+
define :lx, 'lux', :illuminance, u{lm/m**2}
|
49
|
+
define :Bq, 'bequerel', :radioactivity, u{1/s}
|
50
|
+
define :Gy, 'gray', :absorbed_dose, u{J/kg}
|
51
|
+
define :Sv, 'sievert', :equivalent_dose, u{J/kg}
|
52
|
+
define :kat, 'katal', :catalytic_activity, u{mol/s}
|
53
|
+
|
54
|
+
# Other units
|
55
|
+
|
56
|
+
define :min, 'minute', 60, :s
|
57
|
+
define :h, 'hour', 60, :min
|
58
|
+
define :d, 'day', 24, :h
|
59
|
+
define :mi, 'mile', 1.609344, :km
|
60
|
+
define :in, 'inch', 2.54, :cm
|
61
|
+
define :ft, 'foot', 0.3048, :m
|
62
|
+
define :inch, 'inch', 1, :in # alternative to avoid having to use self.in (in is a Ruby keyword)
|
63
|
+
define :lb, 'pound', 0.45359237, :kg
|
64
|
+
|
65
|
+
define :°C, 'degree Celsius', 1, :K, +273.15
|
66
|
+
define :°F, 'degree Fahrenheit', 5.0/9.0, :K, +459.67
|
67
|
+
define :R, 'rankine', 5.0/9.0, :K
|
68
|
+
|
69
|
+
define :l, 'litre', u{dm**3}
|
70
|
+
define :L, 'litre', 1, :l
|
71
|
+
|
72
|
+
define :°, 'degree', ::Math::PI/180.0, :rad
|
73
|
+
define :′, 'arc-minute', ::Math::PI/180.0/60.0, :rad
|
74
|
+
define :″, 'arc-second', ::Math::PI/180.0/3600.0, :rad
|
75
|
+
# not so cool, but easier to type alternatives:
|
76
|
+
define :deg, 'degree', 1, :°
|
77
|
+
define :arcmin, 'arc-minute', 1, :′
|
78
|
+
define :arcsec, 'arc-second', 1, :″
|
79
|
+
|
80
|
+
define :g0, 'standard gravity', u{9.80665*m/s**2}
|
81
|
+
|
82
|
+
define :bar, 'bar', 1E5, :Pa
|
83
|
+
define :atm, 'atmosphere', 101325.0, :Pa
|
84
|
+
define :mWC, 'meters of water column', u{1E3*kg*g0/m**2}
|
85
|
+
define :Torr, 'torricelli', u{atm/760}
|
86
|
+
define :mHg, 'mHg', u{13.5951E3*kg*g0/m**2}
|
87
|
+
|
88
|
+
# define :kp, 'kilopond', :force, u{kg*g0} # or define pond?
|
89
|
+
define :gf, 'gram-force', u{g*g0} # kilopond kp = kgf
|
90
|
+
define :lbf, 'pound-force', u{lb*g0}
|
91
|
+
|
92
|
+
define :dyn, 'dyne', 10, :µN # u{1*g*cm/s**2}
|
93
|
+
define :galUS, 'U.S. liquid gallon', u{231*self.in**3}
|
94
|
+
define :galUK, 'Imperial gallon', 4.546092, :l
|
95
|
+
define :hp, 'horsepower', u{550*ft*lbf/s}
|
96
|
+
|
97
|
+
define :psi, 'pounds-force per square inch', u{lbf/self.in**2}
|
98
|
+
|
99
|
+
end # Units
|
data/lib/units/math.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Units
|
4
|
+
|
5
|
+
module Math
|
6
|
+
|
7
|
+
[:sin, :cos, :tan].each do |fun|
|
8
|
+
define_method fun do |x|
|
9
|
+
x = Units.u{x.in(rad)} if x.kind_of?(Measure)
|
10
|
+
::Math.send(fun, x)
|
11
|
+
end
|
12
|
+
module_function fun
|
13
|
+
end
|
14
|
+
|
15
|
+
[:asin, :acos, :atan].each do |fun|
|
16
|
+
define_method fun do |x|
|
17
|
+
if x.kind_of?(Measure)
|
18
|
+
raise ArgumentError,"Invalid dimensions for #{fun} argument" unless x.dimensionless?
|
19
|
+
x = x.magnitude
|
20
|
+
end
|
21
|
+
Units.u{::Math.send(fun, x)*rad}
|
22
|
+
end
|
23
|
+
module_function fun
|
24
|
+
end
|
25
|
+
|
26
|
+
module_function
|
27
|
+
def atan2(x,y)
|
28
|
+
if x.kind_of?(Measure)
|
29
|
+
if y.kind_of?(Measure)
|
30
|
+
if x.base.to_si.units != y.base.to_si.units
|
31
|
+
raise ArgumentError,"Inconsistent units for atan2 arguments #{x.u}, #{y.u}"
|
32
|
+
end
|
33
|
+
# or x = x.to_si.magnitude, y=y.to_si.magnitude
|
34
|
+
y = y.in(x.units)
|
35
|
+
x = x.magnitude
|
36
|
+
else
|
37
|
+
raise ArgumentError,"Invalid dimensions for atan2 argument #{x.u}" unless x.dimensionless?
|
38
|
+
x = x.magnitude
|
39
|
+
end
|
40
|
+
elsif y.kind_of?(Measure)
|
41
|
+
raise ArgumentError,"Invalid dimensions for atan2 argument #{y.u}" unless y.dimensionless?
|
42
|
+
y = y.magnitude
|
43
|
+
end
|
44
|
+
Units.u{::Math.atan2(x,y)*rad}
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end # Units
|
@@ -0,0 +1,263 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Units
|
4
|
+
|
5
|
+
class Measure
|
6
|
+
|
7
|
+
def initialize(*args)
|
8
|
+
if args.size==0
|
9
|
+
mag = 1.0
|
10
|
+
units = {}
|
11
|
+
elsif args.size==1
|
12
|
+
case args.first
|
13
|
+
when Numeric
|
14
|
+
mag = args.first
|
15
|
+
units = {}
|
16
|
+
else
|
17
|
+
mag = 1.0
|
18
|
+
units = args.first
|
19
|
+
end
|
20
|
+
elsif args.size==2
|
21
|
+
mag, units = args
|
22
|
+
else
|
23
|
+
raise ArgumentError, "wrong number of arguments (#{args.size} for 0, 1 or 2)"
|
24
|
+
end
|
25
|
+
|
26
|
+
@magnitude = mag # rename to value?
|
27
|
+
case units
|
28
|
+
when Symbol
|
29
|
+
uinfo = Units.unit(units)
|
30
|
+
if uinfo
|
31
|
+
if uinfo.dim
|
32
|
+
@units = {uinfo.dim=>[units,1]}
|
33
|
+
else
|
34
|
+
@magnitude *= uinfo.factor
|
35
|
+
@units = {}
|
36
|
+
end
|
37
|
+
else
|
38
|
+
raise ArgumentError,"Invalid symbol for Measure definition: #{units.inspect} "
|
39
|
+
end
|
40
|
+
when Measure
|
41
|
+
@magnitude *= units.magnitude
|
42
|
+
@units = units.units
|
43
|
+
else
|
44
|
+
@units = units
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
include ModalSupport::StateEquivalent
|
49
|
+
include ModalSupport::BracketConstructor
|
50
|
+
|
51
|
+
attr_reader :magnitude, :units
|
52
|
+
|
53
|
+
# represent in text using Ruby notation
|
54
|
+
def to_s
|
55
|
+
return @magnitude.to_s if magnitude?
|
56
|
+
u_descr = Units.units_descr(@units)
|
57
|
+
"#{@magnitude}*#{u_descr}"
|
58
|
+
end
|
59
|
+
|
60
|
+
# more verbose description (not grammatically perfect)
|
61
|
+
def describe
|
62
|
+
return @magnitude.to_s if magnitude?
|
63
|
+
u_descr = Units.units_descr(@units, true)
|
64
|
+
"#{@magnitude} #{u_descr}"
|
65
|
+
end
|
66
|
+
|
67
|
+
# more natural concise text representation
|
68
|
+
def abr
|
69
|
+
self.to_s.gsub('**','^').tr('*',' ')
|
70
|
+
end
|
71
|
+
|
72
|
+
# decompose compound units
|
73
|
+
def detailed_units(all_levels = false)
|
74
|
+
mag = @magnitude
|
75
|
+
prev_units = self.units
|
76
|
+
units = {}
|
77
|
+
loop do
|
78
|
+
compound = false
|
79
|
+
prev_units.each_pair do |dim, (unit,mul)|
|
80
|
+
ud = Units.unit(unit)
|
81
|
+
if ud.decomposition
|
82
|
+
compound = true
|
83
|
+
mag *= ud.decomposition.magnitude
|
84
|
+
ud.decomposition.units.each_pair do |d, (u,m)|
|
85
|
+
mag *= self.class.combine(units, d, u, m)
|
86
|
+
end
|
87
|
+
else
|
88
|
+
mag *= self.class.combine(units, dim, unit, mul)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
if all_levels && compound
|
92
|
+
prev_units = units
|
93
|
+
units = {}
|
94
|
+
else
|
95
|
+
break
|
96
|
+
end
|
97
|
+
end
|
98
|
+
Measure.new mag, units
|
99
|
+
end
|
100
|
+
|
101
|
+
# decompose to base units
|
102
|
+
def base
|
103
|
+
detailed_units true
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.combine(units, dim, unit, mult)
|
107
|
+
factor = 1
|
108
|
+
if units[dim]
|
109
|
+
u,m = units[dim]
|
110
|
+
if u!=unit
|
111
|
+
factor *= Units.conversion_factor(unit, u)**mult
|
112
|
+
end
|
113
|
+
units[dim] = [u, m+mult]
|
114
|
+
else
|
115
|
+
units[dim] = [unit, mult]
|
116
|
+
end
|
117
|
+
factor
|
118
|
+
end
|
119
|
+
|
120
|
+
def /(other)
|
121
|
+
self * (other.kind_of?(Numeric) ? 1.0/other : other.inverse)
|
122
|
+
end
|
123
|
+
|
124
|
+
def inspect
|
125
|
+
"Measure(#{@magnitude.inspect}, #{@units.inspect})"
|
126
|
+
end
|
127
|
+
|
128
|
+
def *(other)
|
129
|
+
case other
|
130
|
+
when Numeric
|
131
|
+
mag = self.magnitude*other
|
132
|
+
units = self.units
|
133
|
+
else
|
134
|
+
mag = self.magnitude*other.magnitude
|
135
|
+
units = {}
|
136
|
+
(self.units.keys | other.units.keys).each do |dim|
|
137
|
+
other_u = other.units[dim] || [nil,0]
|
138
|
+
this_u = self.units[dim] || [other_u.first,0]
|
139
|
+
# mag *= Units.conversion_factor(this_u.first, other_u.first) if other_u.first
|
140
|
+
mult = this_u.last+other_u.last
|
141
|
+
mag *= Units.conversion_factor(other_u.first, this_u.first)**(other_u.last) if other_u.first
|
142
|
+
units[dim] = [this_u.first, mult]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
units.reject!{|dim,(u,m)| m==0}
|
146
|
+
Measure.new(mag, units)
|
147
|
+
end
|
148
|
+
|
149
|
+
def +(other)
|
150
|
+
Measure.new self.magnitude+other.to(self.units).magnitude, self.units
|
151
|
+
end
|
152
|
+
|
153
|
+
def -(other)
|
154
|
+
self + (-other)
|
155
|
+
end
|
156
|
+
|
157
|
+
def **(n)
|
158
|
+
raise ArgumentError,"Only integer powers are allowed for magnitudes" unless n.kind_of?(Integer)
|
159
|
+
units = @units.dup
|
160
|
+
units.each_pair do |dim, (u, m)|
|
161
|
+
units[dim] = [u, m*n]
|
162
|
+
end
|
163
|
+
Measure.new @magnitude, units
|
164
|
+
end
|
165
|
+
|
166
|
+
def inverse
|
167
|
+
#Measure.new(1.0/@magnitude, @units.map_hash{|unit,mult| [unit, -mult]})
|
168
|
+
units = {}
|
169
|
+
@units.each_pair do |dim, (unit, mult)|
|
170
|
+
units[dim] = [unit, -mult]
|
171
|
+
end
|
172
|
+
Measure.new(1.0/@magnitude, units)
|
173
|
+
end
|
174
|
+
|
175
|
+
def -@
|
176
|
+
Measure.new(-@magnitude, units)
|
177
|
+
end
|
178
|
+
|
179
|
+
def in(other, mode=:absolute)
|
180
|
+
other = Measure.new(1.0, other) unless other.kind_of?(Measure)
|
181
|
+
other = other.base
|
182
|
+
this = self.base
|
183
|
+
dims = this.units.keys | other.units.keys
|
184
|
+
mag = this.magnitude/other.magnitude
|
185
|
+
dims.each do |dim|
|
186
|
+
if !this.units[dim] || !other.units[dim] ||
|
187
|
+
(this.units[dim].last != other.units[dim].last)
|
188
|
+
raise "Inconsistent units #{Units.units_descr(this.units)} #{Units.units_descr(other.units)}"
|
189
|
+
end
|
190
|
+
this_u, mult = this.units[dim]
|
191
|
+
other_u = other.units[dim].first
|
192
|
+
mag *= Units.conversion_factor(this_u, other_u)**mult
|
193
|
+
end
|
194
|
+
if mode!=:relative && dims.size==1 && this.units[dims.first].last==1
|
195
|
+
# consider "level" conversion for biased units (otherwise consider interval or difference values)
|
196
|
+
mag += Units.conversion_bias(this.units[dims.first].first, other.units[dims.first].first)
|
197
|
+
end
|
198
|
+
mag
|
199
|
+
end
|
200
|
+
|
201
|
+
def to(units, mode=:absolute)
|
202
|
+
units = units.u if units.kind_of?(Measure)
|
203
|
+
Measure.new self.in(units, mode), units
|
204
|
+
end
|
205
|
+
|
206
|
+
def si_units
|
207
|
+
units = {}
|
208
|
+
@units.each_pair do |dim, (unit, mult)|
|
209
|
+
si_unit = SI_UNITS[dim]
|
210
|
+
if si_unit.kind_of?(Measure)
|
211
|
+
si_unit.units.each_pair do |d, (u,m)|
|
212
|
+
self.class.combine(units, d, u, m*mult)
|
213
|
+
end
|
214
|
+
else
|
215
|
+
self.class.combine(units, dim, si_unit, mult)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
units
|
219
|
+
end
|
220
|
+
|
221
|
+
def to_si
|
222
|
+
to(si_units)
|
223
|
+
end
|
224
|
+
|
225
|
+
def coerce(other)
|
226
|
+
[Measure.new(other, {}), self]
|
227
|
+
end
|
228
|
+
|
229
|
+
def u # dimension? unit? only_units? strip_units? units_measure?
|
230
|
+
Measure.new(1.0, self.units)
|
231
|
+
end
|
232
|
+
|
233
|
+
# dimension (quantity)
|
234
|
+
def dimension
|
235
|
+
q = nil
|
236
|
+
u = self.base.si_units
|
237
|
+
SI_UNITS.each_pair do |dim, unit|
|
238
|
+
unit = Measure.new(1.0, unit) unless unit.kind_of?(Measure)
|
239
|
+
if unit.base.si_units == u
|
240
|
+
q = dim
|
241
|
+
break
|
242
|
+
end
|
243
|
+
end
|
244
|
+
q
|
245
|
+
end
|
246
|
+
|
247
|
+
def dimensionless?
|
248
|
+
base.units.reject{|d,(u,m)| m==0}.empty?
|
249
|
+
end
|
250
|
+
|
251
|
+
# less strict dimensionless condition (e.g. an angle is not a pure magnitude in this sense)
|
252
|
+
def magnitude?
|
253
|
+
self.units.reject{|d,(u,m)| m==0}.empty?
|
254
|
+
end
|
255
|
+
|
256
|
+
end # Measure
|
257
|
+
|
258
|
+
def Measure(*args)
|
259
|
+
Measure[*args]
|
260
|
+
end
|
261
|
+
module_function :Measure
|
262
|
+
|
263
|
+
end # Units
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Units
|
4
|
+
|
5
|
+
PREFIXES = {
|
6
|
+
:y=>[1E-24, 'yocto'],
|
7
|
+
:z=>[1E-21, 'zepto'],
|
8
|
+
:a=>[1E-18, 'atto'],
|
9
|
+
:f=>[1E-15, 'femto'],
|
10
|
+
:p=>[1E-12, 'pico'],
|
11
|
+
:n=>[1E-09, 'nano'],
|
12
|
+
:"\302\265"=>[1E-06, 'micro'], # ASCII alternative: u; define it?
|
13
|
+
:m=>[1E-03, 'milli'],
|
14
|
+
:c=>[1E-02, 'centi'],
|
15
|
+
:d=>[1E-01, 'deci'],
|
16
|
+
:da=>[1E1, 'deca'],
|
17
|
+
:h=>[1E02, 'hecto'],
|
18
|
+
:k=>[1E03, 'kilo'],
|
19
|
+
:M=>[1E06, 'mega'],
|
20
|
+
:G=>[1E09, 'giga'],
|
21
|
+
:T=>[1E12, 'tera'],
|
22
|
+
:P=>[1E15, 'peta'],
|
23
|
+
:E=>[1E18, 'exa'],
|
24
|
+
:Z=>[1E21, 'zetta'],
|
25
|
+
:Y=>[1E24, 'yotta']
|
26
|
+
}
|
27
|
+
|
28
|
+
def self.prefix_factor(prefix)
|
29
|
+
pd = PREFIXES[prefix.to_sym]
|
30
|
+
pd && pd.first
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.prefix_name(prefix)
|
34
|
+
pd = PREFIXES[prefix.to_sym]
|
35
|
+
pd && pd.last
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.prefix_factor_and_name(prefix)
|
39
|
+
PREFIXES[prefix]
|
40
|
+
end
|
41
|
+
|
42
|
+
end # Units
|