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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +110 -0
  8. data/Rakefile +21 -0
  9. data/data/base_unit.yaml +43 -0
  10. data/data/derived_unit.yaml +3394 -0
  11. data/data/prefix.yaml +121 -0
  12. data/lib/unitwise.rb +25 -0
  13. data/lib/unitwise/atom.rb +84 -0
  14. data/lib/unitwise/base.rb +45 -0
  15. data/lib/unitwise/composable.rb +25 -0
  16. data/lib/unitwise/errors.rb +7 -0
  17. data/lib/unitwise/expression.rb +25 -0
  18. data/lib/unitwise/expression/composer.rb +41 -0
  19. data/lib/unitwise/expression/decomposer.rb +42 -0
  20. data/lib/unitwise/expression/matcher.rb +41 -0
  21. data/lib/unitwise/expression/parser.rb +53 -0
  22. data/lib/unitwise/expression/transformer.rb +22 -0
  23. data/lib/unitwise/ext.rb +2 -0
  24. data/lib/unitwise/ext/numeric.rb +13 -0
  25. data/lib/unitwise/ext/string.rb +5 -0
  26. data/lib/unitwise/function.rb +51 -0
  27. data/lib/unitwise/functional.rb +21 -0
  28. data/lib/unitwise/measurement.rb +89 -0
  29. data/lib/unitwise/prefix.rb +17 -0
  30. data/lib/unitwise/scale.rb +61 -0
  31. data/lib/unitwise/standard.rb +26 -0
  32. data/lib/unitwise/standard/base.rb +73 -0
  33. data/lib/unitwise/standard/base_unit.rb +21 -0
  34. data/lib/unitwise/standard/derived_unit.rb +49 -0
  35. data/lib/unitwise/standard/extras.rb +17 -0
  36. data/lib/unitwise/standard/function.rb +35 -0
  37. data/lib/unitwise/standard/prefix.rb +17 -0
  38. data/lib/unitwise/standard/scale.rb +25 -0
  39. data/lib/unitwise/term.rb +106 -0
  40. data/lib/unitwise/unit.rb +89 -0
  41. data/lib/unitwise/version.rb +3 -0
  42. data/test/test_helper.rb +7 -0
  43. data/test/unitwise/atom_test.rb +122 -0
  44. data/test/unitwise/base_test.rb +6 -0
  45. data/test/unitwise/expression/decomposer_test.rb +36 -0
  46. data/test/unitwise/expression/matcher_test.rb +42 -0
  47. data/test/unitwise/expression/parser_test.rb +91 -0
  48. data/test/unitwise/ext/numeric_test.rb +46 -0
  49. data/test/unitwise/ext/string_test.rb +13 -0
  50. data/test/unitwise/function_test.rb +42 -0
  51. data/test/unitwise/measurement_test.rb +168 -0
  52. data/test/unitwise/prefix_test.rb +25 -0
  53. data/test/unitwise/term_test.rb +44 -0
  54. data/test/unitwise/unit_test.rb +57 -0
  55. data/test/unitwise_test.rb +7 -0
  56. data/unitwise.gemspec +28 -0
  57. 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
@@ -0,0 +1,2 @@
1
+ require 'unitwise/ext/numeric'
2
+ require 'unitwise/ext/string'
@@ -0,0 +1,13 @@
1
+ class Numeric
2
+ def convert(unit)
3
+ Unitwise::Measurement.new(self, unit)
4
+ end
5
+
6
+ def method_missing(meth, *args, &block)
7
+ if Unitwise::Expression.decompose(meth)
8
+ self.convert(meth)
9
+ else
10
+ super(meth, *args, &block)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ class String
2
+ def to_slug
3
+ self.downcase.strip.gsub(/\s/, '_').gsub(/\W/, '')
4
+ end
5
+ end
@@ -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