unitwise 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.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +110 -0
- data/Rakefile +21 -0
- data/data/base_unit.yaml +43 -0
- data/data/derived_unit.yaml +3394 -0
- data/data/prefix.yaml +121 -0
- data/lib/unitwise.rb +25 -0
- data/lib/unitwise/atom.rb +84 -0
- data/lib/unitwise/base.rb +45 -0
- data/lib/unitwise/composable.rb +25 -0
- data/lib/unitwise/errors.rb +7 -0
- data/lib/unitwise/expression.rb +25 -0
- data/lib/unitwise/expression/composer.rb +41 -0
- data/lib/unitwise/expression/decomposer.rb +42 -0
- data/lib/unitwise/expression/matcher.rb +41 -0
- data/lib/unitwise/expression/parser.rb +53 -0
- data/lib/unitwise/expression/transformer.rb +22 -0
- data/lib/unitwise/ext.rb +2 -0
- data/lib/unitwise/ext/numeric.rb +13 -0
- data/lib/unitwise/ext/string.rb +5 -0
- data/lib/unitwise/function.rb +51 -0
- data/lib/unitwise/functional.rb +21 -0
- data/lib/unitwise/measurement.rb +89 -0
- data/lib/unitwise/prefix.rb +17 -0
- data/lib/unitwise/scale.rb +61 -0
- data/lib/unitwise/standard.rb +26 -0
- data/lib/unitwise/standard/base.rb +73 -0
- data/lib/unitwise/standard/base_unit.rb +21 -0
- data/lib/unitwise/standard/derived_unit.rb +49 -0
- data/lib/unitwise/standard/extras.rb +17 -0
- data/lib/unitwise/standard/function.rb +35 -0
- data/lib/unitwise/standard/prefix.rb +17 -0
- data/lib/unitwise/standard/scale.rb +25 -0
- data/lib/unitwise/term.rb +106 -0
- data/lib/unitwise/unit.rb +89 -0
- data/lib/unitwise/version.rb +3 -0
- data/test/test_helper.rb +7 -0
- data/test/unitwise/atom_test.rb +122 -0
- data/test/unitwise/base_test.rb +6 -0
- data/test/unitwise/expression/decomposer_test.rb +36 -0
- data/test/unitwise/expression/matcher_test.rb +42 -0
- data/test/unitwise/expression/parser_test.rb +91 -0
- data/test/unitwise/ext/numeric_test.rb +46 -0
- data/test/unitwise/ext/string_test.rb +13 -0
- data/test/unitwise/function_test.rb +42 -0
- data/test/unitwise/measurement_test.rb +168 -0
- data/test/unitwise/prefix_test.rb +25 -0
- data/test/unitwise/term_test.rb +44 -0
- data/test/unitwise/unit_test.rb +57 -0
- data/test/unitwise_test.rb +7 -0
- data/unitwise.gemspec +28 -0
- metadata +213 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
module Unitwise
|
2
|
+
module Expression
|
3
|
+
class Parser < Parslet::Parser
|
4
|
+
attr_reader :key
|
5
|
+
def initialize(key=:primary_code)
|
6
|
+
@key = key
|
7
|
+
end
|
8
|
+
|
9
|
+
root :expression
|
10
|
+
|
11
|
+
rule (:atom) { Matcher.atom(key).as(:atom_code) }
|
12
|
+
rule (:metric_atom) { Matcher.metric_atom(key).as(:atom_code) }
|
13
|
+
rule (:prefix) { Matcher.prefix(key).as(:prefix_code) }
|
14
|
+
|
15
|
+
rule (:simpleton) do
|
16
|
+
(prefix.as(:prefix) >> metric_atom.as(:atom) | atom.as(:atom))
|
17
|
+
end
|
18
|
+
|
19
|
+
rule (:annotation) do
|
20
|
+
str('{') >> match['^}'].repeat.as(:annotation) >> str('}')
|
21
|
+
end
|
22
|
+
|
23
|
+
rule (:digits) { match['0-9'].repeat(1) }
|
24
|
+
|
25
|
+
rule (:integer) { (str('-').maybe >> digits).as(:integer) }
|
26
|
+
|
27
|
+
rule (:fixnum) do
|
28
|
+
(str('-').maybe >> digits >> str('.') >> digits).as(:fixnum)
|
29
|
+
end
|
30
|
+
|
31
|
+
rule (:number) { fixnum | integer }
|
32
|
+
|
33
|
+
rule (:exponent) { number.as(:exponent) }
|
34
|
+
|
35
|
+
rule (:factor) { number.as(:factor) }
|
36
|
+
|
37
|
+
rule (:operator) { (str('.') | str('/')).as(:operator) }
|
38
|
+
|
39
|
+
rule (:term) do
|
40
|
+
((simpleton | factor) >> exponent.maybe >> annotation.maybe).as(:term)
|
41
|
+
end
|
42
|
+
|
43
|
+
rule (:group) do
|
44
|
+
(str('(') >> expression.as(:nested) >> str(')') >> exponent.maybe).as(:group)
|
45
|
+
end
|
46
|
+
|
47
|
+
rule (:expression) do
|
48
|
+
(group | term).as(:left) >> (operator >> expression.as(:right)).maybe
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Unitwise
|
2
|
+
module Expression
|
3
|
+
class Transformer < Parslet::Transform
|
4
|
+
rule(integer: simple(:i)) { i.to_i }
|
5
|
+
rule(fixnum: simple(:f)) { f.to_f }
|
6
|
+
|
7
|
+
rule(prefix_code: simple(:c)) { Prefix.find(c) }
|
8
|
+
rule(atom_code: simple(:c)) { Atom.find(c) }
|
9
|
+
rule(term: subtree(:h)) { Term.new(h) }
|
10
|
+
|
11
|
+
rule(left: simple(:l), operator: simple(:o), right: simple(:r)) do
|
12
|
+
o == '/' ? l / r : l * r
|
13
|
+
end
|
14
|
+
|
15
|
+
rule(left: simple(:l)) { l }
|
16
|
+
|
17
|
+
rule(group: { nested: simple(:n) , exponent: simple(:e)}) { n ** e }
|
18
|
+
|
19
|
+
rule(group: { nested: simple(:n) }) { n }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/unitwise/ext.rb
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
module Unitwise
|
2
|
+
class Function
|
3
|
+
include Math
|
4
|
+
class << self
|
5
|
+
def all
|
6
|
+
@all ||= defaults
|
7
|
+
end
|
8
|
+
|
9
|
+
def defaults
|
10
|
+
[ ["cel", ->(x){ x - 273.15}, ->(x){ x + 273.15} ],
|
11
|
+
["degf", ->(x){9.0 * x / 5.0 - 459.67},->(x){ 5.0/9 * (x + 459.67)}],
|
12
|
+
["hpX", ->(x){ -log10(x) }, ->(x){ 10 ** -x } ],
|
13
|
+
["hpC", ->(x){ -log(x) / log(100) }, ->(x){ 100 ** -x } ],
|
14
|
+
["tan100",->(x){ 100 * tan(x) }, ->(x){ atan(x / 100) } ],
|
15
|
+
["ph", ->(x){ -log10(x) }, ->(x){ 10 ** -x } ],
|
16
|
+
["ld", ->(x){ log2(x) }, ->(x){ 2 ** -x } ],
|
17
|
+
["ln", ->(x){ log(x) }, ->(x){ Math::E ** x } ],
|
18
|
+
["lg", ->(x){ log10(x) }, ->(x){ 10 ** x } ],
|
19
|
+
["2lg", ->(x){ 2 * log10(x) }, ->(x){ (10 ** x) / 2 } ]
|
20
|
+
].map {|args| new(*args) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def add(*args)
|
24
|
+
new_instance = self.new(*args)
|
25
|
+
all << new_instance
|
26
|
+
new_instance
|
27
|
+
end
|
28
|
+
|
29
|
+
def find(name)
|
30
|
+
all.find{|fp| fp.name == name}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :name, :direct, :inverse
|
35
|
+
|
36
|
+
def initialize(name, direct, inverse)
|
37
|
+
@name = name
|
38
|
+
@direct = direct
|
39
|
+
@inverse = inverse
|
40
|
+
end
|
41
|
+
|
42
|
+
def functional(x, direction=1)
|
43
|
+
if direction == 1
|
44
|
+
direct.call(x)
|
45
|
+
elsif direction == -1
|
46
|
+
inverse.call(x)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Unitwise
|
2
|
+
class Functional < Scale
|
3
|
+
|
4
|
+
attr_reader :function
|
5
|
+
|
6
|
+
def initialize(value, unit, function_name)
|
7
|
+
super(value, unit)
|
8
|
+
@function = Function.find(function_name)
|
9
|
+
end
|
10
|
+
|
11
|
+
def scalar
|
12
|
+
puts "Warning: Mathematical operations with special units should be used with caution."
|
13
|
+
super()
|
14
|
+
end
|
15
|
+
|
16
|
+
def functional(x, direction=1)
|
17
|
+
function.functional(x, direction)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
module Unitwise
|
2
|
+
class Measurement < Scale
|
3
|
+
|
4
|
+
def convert(other_unit)
|
5
|
+
other_unit = Unit.new(other_unit)
|
6
|
+
if similar_to?(other_unit)
|
7
|
+
new(converted_value(other_unit), other_unit)
|
8
|
+
else
|
9
|
+
raise ConversionError, "Can't convert #{inspect} to #{other_unit}."
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def *(other)
|
14
|
+
operate(:*, other) || raise(TypeError, "Can't multiply #{inspect} by #{other}.")
|
15
|
+
end
|
16
|
+
|
17
|
+
def /(other)
|
18
|
+
operate(:/, other) || raise(TypeError, "Can't divide #{inspect} by #{other}")
|
19
|
+
end
|
20
|
+
|
21
|
+
def +(other)
|
22
|
+
combine(:+, other) || raise(TypeError, "Can't add #{other} to #{inspect}.")
|
23
|
+
end
|
24
|
+
|
25
|
+
def -(other)
|
26
|
+
combine(:-, other) || raise(TypeError, "Can't subtract #{other} from #{inspect}.")
|
27
|
+
end
|
28
|
+
|
29
|
+
def **(number)
|
30
|
+
if number.is_a?(Numeric)
|
31
|
+
new( value ** number, unit ** number )
|
32
|
+
else
|
33
|
+
raise TypeError, "Can't raise #{inspect} to #{number} power."
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def method_missing(meth, *args, &block)
|
38
|
+
if Unitwise::Expression.decompose(meth)
|
39
|
+
self.convert(meth)
|
40
|
+
else
|
41
|
+
super(meth, *args, &block)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def new(*args)
|
48
|
+
self.class.new(*args)
|
49
|
+
end
|
50
|
+
|
51
|
+
def converted_value(other_unit)
|
52
|
+
if unit.special?
|
53
|
+
if other_unit.special?
|
54
|
+
other_unit.functional functional(value, -1)
|
55
|
+
else
|
56
|
+
functional(value, -1)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
if other_unit.special?
|
60
|
+
other_unit.functional(value)
|
61
|
+
else
|
62
|
+
scalar / other_unit.scalar
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# add or subtract other unit
|
68
|
+
def combine(operator, other)
|
69
|
+
if similar_to?(other)
|
70
|
+
new(value.send(operator, other.convert(unit).value), unit)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# multiply or divide other unit
|
75
|
+
def operate(operator, other)
|
76
|
+
if other.is_a?(Numeric)
|
77
|
+
new(value.send(operator, other), unit)
|
78
|
+
elsif other.respond_to?(:composition)
|
79
|
+
if similar_to?(other)
|
80
|
+
converted = other.convert(unit)
|
81
|
+
new(value.send(operator, converted.value), unit.send(operator, converted.unit))
|
82
|
+
else
|
83
|
+
new(value.send(operator, other.value), unit.send(operator, other.unit))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Unitwise
|
2
|
+
class Prefix < Base
|
3
|
+
attr_reader :scalar
|
4
|
+
|
5
|
+
def self.data
|
6
|
+
@data ||= YAML::load File.open(data_file)
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.data_file
|
10
|
+
Unitwise.data_file 'prefix'
|
11
|
+
end
|
12
|
+
|
13
|
+
def scalar=(value)
|
14
|
+
@scalar = value.to_f
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Unitwise
|
2
|
+
class Scale
|
3
|
+
attr_reader :value
|
4
|
+
|
5
|
+
include Unitwise::Composable
|
6
|
+
|
7
|
+
def initialize(value, unit)
|
8
|
+
@value = value
|
9
|
+
if unit.is_a?(Unit)
|
10
|
+
@unit = unit.dup
|
11
|
+
else
|
12
|
+
@unit = Unit.new(unit.to_s)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def dup
|
17
|
+
self.class.new(value, unit)
|
18
|
+
end
|
19
|
+
|
20
|
+
def atoms
|
21
|
+
unit.atoms
|
22
|
+
end
|
23
|
+
|
24
|
+
def special?
|
25
|
+
unit.special?
|
26
|
+
end
|
27
|
+
|
28
|
+
def functional(value, direction=1)
|
29
|
+
unit.functional(value, direction)
|
30
|
+
end
|
31
|
+
|
32
|
+
def scalar
|
33
|
+
value * unit.scalar
|
34
|
+
end
|
35
|
+
|
36
|
+
def unit
|
37
|
+
@unit ||= Unit.new(@unit_code)
|
38
|
+
end
|
39
|
+
|
40
|
+
def root_terms
|
41
|
+
unit.root_terms
|
42
|
+
end
|
43
|
+
|
44
|
+
def depth
|
45
|
+
unit.depth + 1
|
46
|
+
end
|
47
|
+
|
48
|
+
def terminal?
|
49
|
+
depth <= 3
|
50
|
+
end
|
51
|
+
|
52
|
+
def to_s
|
53
|
+
"#{value} #{unit}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def inspect
|
57
|
+
"<#{self.class} #{to_s}>"
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'nori'
|
3
|
+
require 'unitwise/standard/base'
|
4
|
+
require 'unitwise/standard/prefix'
|
5
|
+
require 'unitwise/standard/base_unit'
|
6
|
+
require 'unitwise/standard/derived_unit'
|
7
|
+
require 'unitwise/standard/scale'
|
8
|
+
require 'unitwise/standard/function'
|
9
|
+
|
10
|
+
module Unitwise
|
11
|
+
module Standard
|
12
|
+
HOST = "unitsofmeasure.org"
|
13
|
+
PATH = "/ucum-essence.xml"
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def body
|
17
|
+
@body ||= Net::HTTP.get HOST, PATH
|
18
|
+
end
|
19
|
+
|
20
|
+
def hash
|
21
|
+
Nori.new.parse(body)["root"]
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'unitwise/standard/extras'
|
2
|
+
module Unitwise::Standard
|
3
|
+
class Base
|
4
|
+
include Unitwise::Standard::Extras
|
5
|
+
|
6
|
+
attr_accessor :attributes
|
7
|
+
|
8
|
+
def self.local_key
|
9
|
+
remote_key
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.all
|
13
|
+
@all ||= read
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.read
|
17
|
+
Unitwise::Standard.hash[remote_key].inject([]){|a,h| a << self.new(h)}
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.hash
|
21
|
+
self.all.map(&:to_hash)
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.path
|
25
|
+
Unitwise.data_file(local_key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.write
|
29
|
+
File.open(path, 'w') do |f|
|
30
|
+
f.write hash.to_yaml
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def initialize(attributes)
|
35
|
+
@attributes = attributes
|
36
|
+
end
|
37
|
+
|
38
|
+
def names
|
39
|
+
if attributes["name"].respond_to?(:map)
|
40
|
+
attributes["name"].map(&:to_s)
|
41
|
+
else
|
42
|
+
attributes["name"].to_s
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def symbol
|
47
|
+
sym = attributes["printSymbol"]
|
48
|
+
if sym.is_a?(Hash)
|
49
|
+
hash_to_markup(sym)
|
50
|
+
elsif sym
|
51
|
+
sym.to_s
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def primary_code
|
56
|
+
attributes["@Code"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def secondary_code
|
60
|
+
attributes["@CODE"]
|
61
|
+
end
|
62
|
+
|
63
|
+
def to_hash
|
64
|
+
[:names, :symbol, :primary_code, :secondary_code].inject({}) do |h,a|
|
65
|
+
if v = self.send(a)
|
66
|
+
h[a] = v
|
67
|
+
end
|
68
|
+
h
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|