unitwise 0.3.0 → 0.3.1
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 +4 -4
- data/README.md +22 -13
- data/lib/unitwise.rb +20 -5
- data/lib/unitwise/atom.rb +56 -16
- data/lib/unitwise/base.rb +8 -5
- data/lib/unitwise/composable.rb +19 -0
- data/lib/unitwise/expression/composer.rb +2 -8
- data/lib/unitwise/expression/decomposer.rb +15 -14
- data/lib/unitwise/expression/parser.rb +3 -3
- data/lib/unitwise/expression/transformer.rb +7 -3
- data/lib/unitwise/ext.rb +1 -0
- data/lib/unitwise/functional.rb +45 -9
- data/lib/unitwise/measurement.rb +11 -2
- data/lib/unitwise/prefix.rb +1 -1
- data/lib/unitwise/scale.rb +2 -2
- data/lib/unitwise/term.rb +9 -13
- data/lib/unitwise/unit.rb +8 -2
- data/lib/unitwise/version.rb +1 -1
- data/simple_bench.rb +21 -0
- data/test/unitwise/atom_test.rb +8 -0
- data/test/unitwise/expression/decomposer_test.rb +16 -7
- data/test/unitwise/expression/parser_test.rb +3 -3
- data/test/unitwise/functional_test.rb +17 -0
- data/test/unitwise/measurement_test.rb +21 -8
- data/test/unitwise/prefix_test.rb +8 -0
- data/unitwise.gemspec +3 -2
- metadata +27 -13
- data/lib/unitwise/function.rb +0 -51
- data/test/unitwise/function_test.rb +0 -42
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a382c394857814457fe921d7556aa91211d9a752
|
4
|
+
data.tar.gz: 24ecd6c8f3fcd11a281cc0fb860462f2b9766b4c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a23988ac0f39b46d1f325bb4766e13c3ce025eaee602f5946426e4258b07b9b32f4752298ce78160f049c6be05b7699cfbefaf63a451763559de5fecf2336cbc
|
7
|
+
data.tar.gz: 75f123f3fd5195239f728efc037ce2038dece84b647df8234fca6391b4a9280f0e6d6fad614502737daf62976795253c099fa03a4a5c459837f8b92feaedfaa1
|
data/README.md
CHANGED
@@ -38,9 +38,8 @@ require 'unitwise/ext'
|
|
38
38
|
|
39
39
|
### Conversion
|
40
40
|
|
41
|
-
|
42
|
-
compatible unit
|
43
|
-
`convert(unit)` method.
|
41
|
+
Unitwise is able to convert any unit within the UCUM spec to any other
|
42
|
+
compatible unit.
|
44
43
|
|
45
44
|
```ruby
|
46
45
|
distance = Unitwise(5, 'kilometer')
|
@@ -50,7 +49,8 @@ distance.convert('mile')
|
|
50
49
|
# => <Unitwise::Measurement 3.106849747474748 mile>
|
51
50
|
```
|
52
51
|
|
53
|
-
The prettier version of `convert(unit)` is just calling the unit
|
52
|
+
The prettier version of `convert(unit)` is just calling the unit as a method
|
53
|
+
name:
|
54
54
|
|
55
55
|
```ruby
|
56
56
|
distance = 26.2.mile
|
@@ -70,8 +70,8 @@ It also has the ability to compare measurements with the same or different units
|
|
70
70
|
1.meter > 1.yard # => true
|
71
71
|
```
|
72
72
|
|
73
|
-
Again, you have to compare compatible units.
|
74
|
-
|
73
|
+
Again, you have to compare compatible units. For example, comparing two
|
74
|
+
temperatures will work, comparing a mass to a length would fail.
|
75
75
|
|
76
76
|
### SI abbreviations
|
77
77
|
|
@@ -115,7 +115,7 @@ You can multiply or divide measurements and numbers.
|
|
115
115
|
|
116
116
|
```ruby
|
117
117
|
110.volt * 2
|
118
|
-
=> <Unitwise::Measurement 220 volt>
|
118
|
+
# => <Unitwise::Measurement 220 volt>
|
119
119
|
```
|
120
120
|
|
121
121
|
You can multiply or divide measurements with measurements. Here is a fun example
|
@@ -162,14 +162,23 @@ and foot:
|
|
162
162
|
3.convert('[in_br]') == 3.convert('[in_i]') # => false
|
163
163
|
```
|
164
164
|
|
165
|
-
###
|
165
|
+
### Available Units
|
166
166
|
|
167
|
-
|
168
|
-
|
167
|
+
If you are looking for a particular unit (or 'atom' in the UCUM spec), chances
|
168
|
+
are that it is in here. There is a rudimentary search function for both
|
169
|
+
`Unitwise::Atom` and `Unitwise::Prefix`.
|
169
170
|
|
170
|
-
|
171
|
-
|
172
|
-
|
171
|
+
```ruby
|
172
|
+
Unitwise::Atom.search('fathom')
|
173
|
+
# => [ ... ]
|
174
|
+
Unitwise::Prefix.search('milli')
|
175
|
+
# => [ ... ]
|
176
|
+
```
|
177
|
+
|
178
|
+
You can also get the official list from the UCUM website in XML format at
|
179
|
+
[unitsofmeasure.org/ucum-essence.xml](http://unitsofmeasure.org/ucum-essence.xml)
|
180
|
+
or a YAML version within this repo
|
181
|
+
[github.com/joshwlewis/unitwise/tree/master/data](//github.com/joshwlewis/unitwise/tree/master/data).
|
173
182
|
|
174
183
|
|
175
184
|
## Installation
|
data/lib/unitwise.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'liner'
|
2
|
+
|
1
3
|
require "unitwise/version"
|
2
4
|
require 'unitwise/base'
|
3
5
|
require 'unitwise/expression'
|
@@ -9,24 +11,37 @@ require 'unitwise/atom'
|
|
9
11
|
require 'unitwise/prefix'
|
10
12
|
require 'unitwise/term'
|
11
13
|
require 'unitwise/unit'
|
12
|
-
require 'unitwise/function'
|
13
14
|
require 'unitwise/errors'
|
14
15
|
|
16
|
+
# Unitwise is a library for performing mathematical operations and conversions
|
17
|
+
# on all units defined by the [Unified Code for Units of Measure(UCUM).
|
15
18
|
module Unitwise
|
19
|
+
# The system path for the installed gem
|
20
|
+
# @api private
|
16
21
|
def self.path
|
17
22
|
@path ||= File.dirname(File.dirname(__FILE__))
|
18
23
|
end
|
19
24
|
|
25
|
+
# A helper to get the location of a yaml data file
|
26
|
+
# @api private
|
20
27
|
def self.data_file(key)
|
21
28
|
File.join path, "data", "#{key}.yaml"
|
22
29
|
end
|
23
30
|
end
|
24
31
|
|
25
|
-
|
26
|
-
|
27
|
-
|
32
|
+
# Measurement initializer shorthand. Use this to instantiate new measurements.
|
33
|
+
# @param first [Numeric, String] Either a numeric value or a unit expression
|
34
|
+
# @param last [String, Nil] Either a unit expression, or nil
|
35
|
+
# @return [Unitwise::Measurement]
|
36
|
+
# @example
|
37
|
+
# Unitwise(20, 'mile') # => #<Unitwise::Measurement 20 mile>
|
38
|
+
# Unitwise('km') # => #<Unitwise::Measurement 1 km>
|
39
|
+
# @api public
|
40
|
+
def Unitwise(first, last=nil)
|
41
|
+
if last
|
42
|
+
Unitwise::Measurement.new(first, last)
|
28
43
|
else
|
29
|
-
Unitwise::Measurement.new(1,
|
44
|
+
Unitwise::Measurement.new(1, first)
|
30
45
|
end
|
31
46
|
end
|
32
47
|
|
data/lib/unitwise/atom.rb
CHANGED
@@ -1,56 +1,92 @@
|
|
1
1
|
module Unitwise
|
2
|
+
# Atoms are the most basic elements in Unitwise. They are named coded and
|
3
|
+
# scaled units without prefixes, multipliers, exponents, etc. Examples are
|
4
|
+
# 'meter', 'hour', 'pound force'.
|
2
5
|
class Atom < Base
|
3
|
-
|
4
|
-
attr_writer :dim
|
6
|
+
liner :classification, :property, :metric, :special, :arbitrary, :dim
|
5
7
|
|
6
8
|
include Unitwise::Composable
|
7
9
|
|
8
10
|
class << self
|
11
|
+
# Array of hashes representing atom properties.
|
12
|
+
# @api private
|
9
13
|
def data
|
10
14
|
@data ||= data_files.reduce([]){|m,f| m += YAML::load File.open(f)}
|
11
15
|
end
|
12
16
|
|
17
|
+
# Data files containing atom data
|
18
|
+
# @api private
|
13
19
|
def data_files
|
14
20
|
%w(base_unit derived_unit).map{|type| Unitwise.data_file type}
|
15
21
|
end
|
16
22
|
end
|
17
23
|
|
24
|
+
# Determine if an atom is base level. All atoms that are not base are
|
25
|
+
# defined directly or indirectly in reference to a base atom.
|
26
|
+
# @return [true, false]
|
27
|
+
# @api public
|
18
28
|
def base?
|
19
29
|
scale.nil? && !@dim.nil?
|
20
30
|
end
|
21
31
|
|
32
|
+
# Determine if an atom is derived. Derived atoms are defined with respect
|
33
|
+
# to other atoms.
|
34
|
+
# @return [true, false]
|
35
|
+
# @api public
|
22
36
|
def derived?
|
23
37
|
!base?
|
24
38
|
end
|
25
39
|
|
40
|
+
# Determine if an atom is metric. Metric atoms can be combined with metric
|
41
|
+
# prefixes.
|
42
|
+
# @return [true, false]
|
43
|
+
# @api public
|
26
44
|
def metric?
|
27
45
|
base? ? true : !!metric
|
28
46
|
end
|
29
47
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
48
|
+
# Determine if a unit is special. Special atoms are not defined on a
|
49
|
+
# traditional ratio scale.
|
50
|
+
# @return [true, false]
|
51
|
+
# @api public
|
34
52
|
def special?
|
35
53
|
!!special
|
36
54
|
end
|
37
55
|
|
56
|
+
# Determine if a unit is arbitrary. Arbitrary atoms are not of any specific
|
57
|
+
# dimension and have no general meaning, therefore cannot be compared with
|
58
|
+
# any other unit.
|
59
|
+
# @return [true, false]
|
60
|
+
# @api public
|
38
61
|
def arbitrary?
|
39
62
|
!!arbitrary
|
40
63
|
end
|
41
64
|
|
65
|
+
# Determine how far away a unit is from a base unit.
|
66
|
+
# @return [Integer]
|
67
|
+
# @api public
|
42
68
|
def depth
|
43
69
|
base? ? 0 : scale.depth + 1
|
44
70
|
end
|
45
71
|
|
72
|
+
# Determine if this is the last atom in the scale chain
|
73
|
+
# @return [true, false]
|
74
|
+
# @api public
|
46
75
|
def terminal?
|
47
76
|
depth <= 3
|
48
77
|
end
|
49
78
|
|
79
|
+
# A representation of an atoms composition. Used to determine if two
|
80
|
+
# different atoms are compatible.
|
81
|
+
# @return [String]
|
82
|
+
# @api public
|
50
83
|
def dim
|
51
84
|
terminal? ? @dim || property : composition_string
|
52
85
|
end
|
53
86
|
|
87
|
+
# Set the atom's scale. It can be set as a Scale or a Functional
|
88
|
+
# @return [Unitwise::Functional, Unitwise::Scale]
|
89
|
+
# @api public
|
54
90
|
def scale=(attrs)
|
55
91
|
@scale = if attrs[:function_code]
|
56
92
|
Functional.new(attrs[:value], attrs[:unit_code], attrs[:function_code])
|
@@ -59,25 +95,29 @@ module Unitwise
|
|
59
95
|
end
|
60
96
|
end
|
61
97
|
|
98
|
+
# Get a numeric value that can be used to with other atoms to compare with
|
99
|
+
# or operate on. Base units have a scalar of 1.
|
100
|
+
# @return [Numeric]
|
101
|
+
# @api public
|
62
102
|
def scalar
|
63
103
|
base? ? 1 : scale.scalar
|
64
104
|
end
|
65
105
|
|
66
|
-
|
67
|
-
|
106
|
+
# Get a functional value that can be used with other atoms to compare with
|
107
|
+
# or operate on.
|
108
|
+
# @param x [Numeric] The number to convert to or convert from
|
109
|
+
# @param forward [true, false] Convert to or convert from
|
110
|
+
# @return [Numeric] The converted value
|
111
|
+
def functional(x=scalar, forward=true)
|
112
|
+
scale.functional(x, forward)
|
68
113
|
end
|
69
114
|
|
115
|
+
# An atom may have a complex scale with several base atoms at various
|
116
|
+
# depths. This method returns all of this atoms base level terms.
|
117
|
+
# @return [Array] An array containing base Unitwise::Term
|
70
118
|
def root_terms
|
71
119
|
base? ? [Term.new(atom: self)] : scale.root_terms
|
72
120
|
end
|
73
121
|
|
74
|
-
def to_s
|
75
|
-
"#{primary_code} (#{names.join('|')})"
|
76
|
-
end
|
77
|
-
|
78
|
-
def inspect
|
79
|
-
"<#{self.class} #{to_s}>"
|
80
|
-
end
|
81
|
-
|
82
122
|
end
|
83
123
|
end
|
data/lib/unitwise/base.rb
CHANGED
@@ -1,8 +1,7 @@
|
|
1
1
|
require 'yaml'
|
2
2
|
module Unitwise
|
3
3
|
class Base
|
4
|
-
|
5
|
-
attr_reader :names, :scale
|
4
|
+
liner :names, :primary_code, :secondary_code, :symbol, :scale
|
6
5
|
|
7
6
|
def self.all
|
8
7
|
@all ||= data.map{|d| self.new d }
|
@@ -27,9 +26,9 @@ module Unitwise
|
|
27
26
|
end
|
28
27
|
end
|
29
28
|
|
30
|
-
def
|
31
|
-
|
32
|
-
|
29
|
+
def self.search(term)
|
30
|
+
self.all.select do |i|
|
31
|
+
i.search_strings.any? { |string| string =~ /#{term}/i }
|
33
32
|
end
|
34
33
|
end
|
35
34
|
|
@@ -43,5 +42,9 @@ module Unitwise
|
|
43
42
|
end
|
44
43
|
end
|
45
44
|
|
45
|
+
def search_strings
|
46
|
+
[primary_code, secondary_code, names, slugs, symbol].flatten.compact
|
47
|
+
end
|
48
|
+
|
46
49
|
end
|
47
50
|
end
|
data/lib/unitwise/composable.rb
CHANGED
@@ -1,30 +1,49 @@
|
|
1
1
|
module Unitwise
|
2
|
+
# Composable provides methods for determing the composition of unit, term, or
|
3
|
+
# measurement. This is used to establish compatibility between units, terms,
|
4
|
+
# or measurements.
|
2
5
|
module Composable
|
3
6
|
|
7
|
+
# @api private
|
4
8
|
def self.included(base)
|
5
9
|
base.send :include, Comparable
|
6
10
|
end
|
7
11
|
|
12
|
+
# A representation of a unit based on the atoms it's derived from.
|
13
|
+
# @return [SignedMultiset]
|
14
|
+
# @api public
|
8
15
|
def composition
|
9
16
|
root_terms.reduce(SignedMultiset.new) do |s, t|
|
10
17
|
s.increment(t.atom.dim, t.exponent) if t.atom; s
|
11
18
|
end
|
12
19
|
end
|
13
20
|
|
21
|
+
# Define a default #dim for included classes.
|
22
|
+
# @return [String]
|
23
|
+
# @api public
|
14
24
|
def dim
|
15
25
|
composition_string
|
16
26
|
end
|
17
27
|
|
28
|
+
# A string representation of a unit based on the atoms it's derived from
|
29
|
+
# @return [String]
|
30
|
+
# @api public
|
18
31
|
def composition_string
|
19
32
|
composition.sort.map do |k,v|
|
20
33
|
v == 1 ? k.to_s : "#{k}#{v}"
|
21
34
|
end.join('.')
|
22
35
|
end
|
23
36
|
|
37
|
+
# Determine if this instance is similar to or compatible with other
|
38
|
+
# @return [true false]
|
39
|
+
# @api public
|
24
40
|
def similar_to?(other)
|
25
41
|
self.composition == other.composition
|
26
42
|
end
|
27
43
|
|
44
|
+
# Compare whether the instance is greater, less than or equal to other.
|
45
|
+
# @return [-1 0 1]
|
46
|
+
# @api public
|
28
47
|
def <=>(other)
|
29
48
|
if other.respond_to?(:composition) && similar_to?(other)
|
30
49
|
scalar <=> other.scalar
|
@@ -2,14 +2,8 @@ module Unitwise
|
|
2
2
|
module Expression
|
3
3
|
class Composer
|
4
4
|
attr_reader :terms
|
5
|
-
def initialize(
|
6
|
-
|
7
|
-
@terms = input.terms
|
8
|
-
elsif input.respond_to?(:each)
|
9
|
-
@terms = input
|
10
|
-
else
|
11
|
-
@terms = Expression.decompose(input.to_s)
|
12
|
-
end
|
5
|
+
def initialize(terms)
|
6
|
+
@terms = terms
|
13
7
|
end
|
14
8
|
|
15
9
|
def set
|
@@ -2,8 +2,10 @@ module Unitwise
|
|
2
2
|
module Expression
|
3
3
|
class Decomposer
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
METHODS = [:primary_code, :secondary_code, :names, :slugs, :symbol]
|
6
|
+
|
7
|
+
PARSERS = METHODS.reduce({}) do |hash, method|
|
8
|
+
hash[method] = Parser.new(method); hash
|
7
9
|
end
|
8
10
|
|
9
11
|
TRANSFORMER = Transformer.new
|
@@ -17,23 +19,22 @@ module Unitwise
|
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
20
|
-
def
|
21
|
-
PARSERS.reduce(nil) do |
|
22
|
-
if
|
23
|
-
return
|
22
|
+
def transform
|
23
|
+
PARSERS.reduce(nil) do |foo, (method, parser)|
|
24
|
+
if parsed = parser.parse(expression) rescue next
|
25
|
+
return TRANSFORMER.apply(parsed, key: method)
|
24
26
|
end
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
28
|
-
def transform
|
29
|
-
@transform ||= TRANSFORMER.apply(parse)
|
30
|
-
end
|
31
|
-
|
32
30
|
def terms
|
33
|
-
|
34
|
-
transform
|
35
|
-
|
36
|
-
|
31
|
+
@terms ||= begin
|
32
|
+
transformation = transform
|
33
|
+
if transformation.respond_to?(:terms)
|
34
|
+
transformation.terms
|
35
|
+
else
|
36
|
+
Array(transformation)
|
37
|
+
end
|
37
38
|
end
|
38
39
|
end
|
39
40
|
|
@@ -30,18 +30,18 @@ module Unitwise
|
|
30
30
|
|
31
31
|
rule (:number) { fixnum | integer }
|
32
32
|
|
33
|
-
rule (:exponent) {
|
33
|
+
rule (:exponent) { integer.as(:exponent) }
|
34
34
|
|
35
35
|
rule (:factor) { number.as(:factor) }
|
36
36
|
|
37
37
|
rule (:operator) { (str('.') | str('/')).as(:operator) }
|
38
38
|
|
39
39
|
rule (:term) do
|
40
|
-
((simpleton | factor) >> exponent.maybe >> annotation.maybe).as(:term)
|
40
|
+
((factor >> simpleton | simpleton | factor) >> exponent.maybe >> annotation.maybe).as(:term)
|
41
41
|
end
|
42
42
|
|
43
43
|
rule (:group) do
|
44
|
-
(str('(') >> expression.as(:nested) >> str(')') >> exponent.maybe).as(:group)
|
44
|
+
(factor.maybe >> str('(') >> expression.as(:nested) >> str(')') >> exponent.maybe).as(:group)
|
45
45
|
end
|
46
46
|
|
47
47
|
rule (:expression) do
|
@@ -1,12 +1,13 @@
|
|
1
1
|
module Unitwise
|
2
2
|
module Expression
|
3
3
|
class Transformer < Parslet::Transform
|
4
|
+
|
4
5
|
rule(integer: simple(:i)) { i.to_i }
|
5
6
|
rule(fixnum: simple(:f)) { f.to_f }
|
6
7
|
|
7
|
-
rule(prefix_code: simple(:c)) { Prefix.
|
8
|
-
rule(atom_code: simple(:c))
|
9
|
-
rule(term: subtree(:h))
|
8
|
+
rule(prefix_code: simple(:c)) { |ctx| Prefix.find_by(ctx[:key], ctx[:c]) }
|
9
|
+
rule(atom_code: simple(:c)) { |ctx| Atom.find_by(ctx[:key], ctx[:c]) }
|
10
|
+
rule(term: subtree(:h)) { Term.new(h) }
|
10
11
|
|
11
12
|
rule(operator: simple(:o), right: simple(:r)) do
|
12
13
|
o == '/' ? r ** -1 : r
|
@@ -18,6 +19,9 @@ module Unitwise
|
|
18
19
|
|
19
20
|
rule(left: simple(:l)) { l }
|
20
21
|
|
22
|
+
rule(group: { factor: simple(:f) , nested: simple(:n), exponent: simple(:e) }) do
|
23
|
+
( n ** e ) * f
|
24
|
+
end
|
21
25
|
rule(group: { nested: simple(:n) , exponent: simple(:e)}) { n ** e }
|
22
26
|
|
23
27
|
rule(group: { nested: simple(:n) }) { n }
|
data/lib/unitwise/ext.rb
CHANGED
data/lib/unitwise/functional.rb
CHANGED
@@ -1,20 +1,56 @@
|
|
1
1
|
module Unitwise
|
2
2
|
class Functional < Scale
|
3
|
+
extend Math
|
3
4
|
|
4
|
-
|
5
|
+
def self._cel(x, forward=true)
|
6
|
+
forward ? x - 273.15 : x + 273.15
|
7
|
+
end
|
5
8
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
+
def self._degf(x, forward=true)
|
10
|
+
forward ? 9.0 * x / 5.0 - 459.67 : 5.0 / 9 * (x + 459.67)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self._hpX(x, forward=true)
|
14
|
+
forward ? -log10(x) : 10 ** -x
|
15
|
+
end
|
16
|
+
|
17
|
+
def self._hpC(x, forward=true)
|
18
|
+
forward ? -log(x) / log(100) : 100 ** -x
|
19
|
+
end
|
20
|
+
|
21
|
+
def self._tan100(x, forward=true)
|
22
|
+
forward ? 100 * tan(x) : atan(x / 100)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self._ph(x, forward=true)
|
26
|
+
_hpX(x,forward)
|
9
27
|
end
|
10
28
|
|
11
|
-
def
|
12
|
-
|
13
|
-
|
29
|
+
def self._ld(x, forward=true)
|
30
|
+
forward ? log2(x) : 2 ** x
|
31
|
+
end
|
32
|
+
|
33
|
+
def self._ln(x, forward=true)
|
34
|
+
forward ? log(x) : Math::E ** x
|
35
|
+
end
|
36
|
+
|
37
|
+
def self._lg(x, forward = true)
|
38
|
+
forward ? log10(x) : 10 ** x
|
39
|
+
end
|
40
|
+
|
41
|
+
def self._2lg(x, forward = true)
|
42
|
+
forward ? 2 * log10(x) : 10 ** ( x / 2 )
|
43
|
+
end
|
44
|
+
|
45
|
+
attr_reader :function_name
|
46
|
+
|
47
|
+
def initialize(value, unit, function_name)
|
48
|
+
super(value, unit)
|
49
|
+
@function_name = function_name
|
14
50
|
end
|
15
51
|
|
16
|
-
def functional(x=scalar,
|
17
|
-
|
52
|
+
def functional(x=scalar, forward = true)
|
53
|
+
self.class.send(:"_#{function_name}", x, forward)
|
18
54
|
end
|
19
55
|
|
20
56
|
end
|
data/lib/unitwise/measurement.rb
CHANGED
@@ -41,6 +41,15 @@ module Unitwise
|
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
44
|
+
def coerce(other)
|
45
|
+
case other
|
46
|
+
when Numeric
|
47
|
+
return self.class.new(other, '1'), self
|
48
|
+
else
|
49
|
+
raise TypeError, "#{self.class} can't be coerced into #{other.class}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
44
53
|
def method_missing(meth, *args, &block)
|
45
54
|
if Unitwise::Expression.decompose(meth)
|
46
55
|
self.convert(meth)
|
@@ -58,9 +67,9 @@ module Unitwise
|
|
58
67
|
def converted_value(other_unit)
|
59
68
|
if unit.special?
|
60
69
|
if other_unit.special?
|
61
|
-
other_unit.functional functional(value,
|
70
|
+
other_unit.functional functional(value, false)
|
62
71
|
else
|
63
|
-
functional(value,
|
72
|
+
functional(value, false) / other_unit.scalar
|
64
73
|
end
|
65
74
|
else
|
66
75
|
if other_unit.special?
|
data/lib/unitwise/prefix.rb
CHANGED
data/lib/unitwise/scale.rb
CHANGED
data/lib/unitwise/term.rb
CHANGED
@@ -1,18 +1,10 @@
|
|
1
1
|
require 'signed_multiset'
|
2
2
|
module Unitwise
|
3
3
|
class Term
|
4
|
-
|
5
|
-
attr_writer :factor, :exponent
|
6
|
-
attr_accessor :annotation
|
4
|
+
liner :atom_code, :prefix_code, :atom, :prefix, :factor, :exponent, :annotation
|
7
5
|
|
8
6
|
include Unitwise::Composable
|
9
7
|
|
10
|
-
def initialize(attributes)
|
11
|
-
attributes.each do |k,v|
|
12
|
-
public_send :"#{k}=", v
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
8
|
def prefix_code
|
17
9
|
@prefix_code ||= (@prefix.primary_code if @prefix)
|
18
10
|
end
|
@@ -53,8 +45,8 @@ module Unitwise
|
|
53
45
|
(factor * (prefix ? prefix.scalar : 1) * (atom ? atom.scalar : 1)) ** exponent
|
54
46
|
end
|
55
47
|
|
56
|
-
def functional(x=scalar,
|
57
|
-
(factor * (prefix ? prefix.scalar : 1)) * (atom ? atom.functional(x,
|
48
|
+
def functional(x=scalar, forward=true)
|
49
|
+
(factor * (prefix ? prefix.scalar : 1)) * (atom ? atom.functional(x, forward) : 1) ** exponent
|
58
50
|
end
|
59
51
|
|
60
52
|
def root_terms
|
@@ -76,16 +68,20 @@ module Unitwise
|
|
76
68
|
def *(other)
|
77
69
|
if other.respond_to?(:terms)
|
78
70
|
Unit.new(other.terms << self)
|
79
|
-
|
71
|
+
elsif other.respond_to?(:atom)
|
80
72
|
Unit.new([self, other])
|
73
|
+
elsif other.is_a?(Numeric)
|
74
|
+
self.class.new(to_hash.merge(factor: factor * other))
|
81
75
|
end
|
82
76
|
end
|
83
77
|
|
84
78
|
def /(other)
|
85
79
|
if other.respond_to?(:terms)
|
86
80
|
Unit.new(other.terms.map{|t| t ** -1} << self)
|
87
|
-
|
81
|
+
elsif other.respond_to?(:atom)
|
88
82
|
Unit.new([self, other ** -1])
|
83
|
+
elsif other.is_a?(Numeric)
|
84
|
+
self.class.new(to_hash.merge(factor: factor / other))
|
89
85
|
end
|
90
86
|
end
|
91
87
|
|
data/lib/unitwise/unit.rb
CHANGED
@@ -28,8 +28,8 @@ module Unitwise
|
|
28
28
|
terms.count == 1 && terms.all?(&:special?)
|
29
29
|
end
|
30
30
|
|
31
|
-
def functional(x=scalar,
|
32
|
-
terms.first.functional(x,
|
31
|
+
def functional(x=scalar, forward=true)
|
32
|
+
terms.first.functional(x, forward)
|
33
33
|
end
|
34
34
|
|
35
35
|
def dup
|
@@ -59,6 +59,10 @@ module Unitwise
|
|
59
59
|
def *(other)
|
60
60
|
if other.respond_to?(:terms)
|
61
61
|
self.class.new(terms + other.terms)
|
62
|
+
elsif other.respond_to?(:atom)
|
63
|
+
self.class.new(terms << other)
|
64
|
+
elsif other.is_a?(Numeric)
|
65
|
+
self.class.new(terms.map{ |t| t * other })
|
62
66
|
else
|
63
67
|
raise TypeError, "Can't multiply #{inspect} by #{other}."
|
64
68
|
end
|
@@ -67,6 +71,8 @@ module Unitwise
|
|
67
71
|
def /(other)
|
68
72
|
if other.respond_to?(:terms)
|
69
73
|
self.class.new(terms + other.terms.map{ |t| t ** -1})
|
74
|
+
elsif other.respond_to?(:atom)
|
75
|
+
self.class.new(terms << other ** -1)
|
70
76
|
else
|
71
77
|
raise TypeError, "Can't divide #{inspect} by #{other}."
|
72
78
|
end
|
data/lib/unitwise/version.rb
CHANGED
data/simple_bench.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'unitwise'
|
2
|
+
require 'ruby-units'
|
3
|
+
require 'benchmark'
|
4
|
+
|
5
|
+
Benchmark.bm(10) do |x|
|
6
|
+
|
7
|
+
x.report('unitwise') do
|
8
|
+
100000.times do |x|
|
9
|
+
expression = "mm#{x % 20}"
|
10
|
+
Unitwise::Measurement.new(x, expression)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
x.report('ruby-units') do
|
15
|
+
100000.times do |x|
|
16
|
+
expression = "mm^#{x % 20}"
|
17
|
+
Unit(x, expression)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
data/test/unitwise/atom_test.rb
CHANGED
@@ -23,6 +23,14 @@ describe Unitwise::Atom do
|
|
23
23
|
end
|
24
24
|
end
|
25
25
|
|
26
|
+
describe "::search" do
|
27
|
+
it "must search for atoms" do
|
28
|
+
result = subject.search("inch")
|
29
|
+
result.count.must_equal 10
|
30
|
+
result.must_include subject.find("inch")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
26
34
|
let(:second) { Unitwise::Atom.find("s") }
|
27
35
|
let(:yard) { Unitwise::Atom.find("[yd_i]")}
|
28
36
|
let(:pi) { Unitwise::Atom.find("[pi]")}
|
@@ -3,13 +3,6 @@ require 'test_helper'
|
|
3
3
|
describe Unitwise::Expression::Decomposer do
|
4
4
|
subject { Unitwise::Expression::Decomposer }
|
5
5
|
|
6
|
-
describe "PARSERS" do
|
7
|
-
it "should return multiple parsers" do
|
8
|
-
subject::PARSERS.must_be_instance_of Array
|
9
|
-
subject::PARSERS.sample.must_be_instance_of Unitwise::Expression::Parser
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
6
|
describe "#terms" do
|
14
7
|
it "should accept codes" do
|
15
8
|
fts = subject.new("[ft_i]/s").terms
|
@@ -31,6 +24,22 @@ describe Unitwise::Expression::Decomposer do
|
|
31
24
|
saff = subject.new("<i>g<sub>n</sub></i>").terms
|
32
25
|
saff.count.must_equal 1
|
33
26
|
end
|
27
|
+
it "should accept complex units" do
|
28
|
+
complex = subject.new("(mg.(km/s)3/J)2.Pa").terms
|
29
|
+
complex.count.must_equal 5
|
30
|
+
end
|
31
|
+
it "should accept more complex units" do
|
32
|
+
complex = subject.new("4.1(mm/2s3)4.7.3J-2").terms
|
33
|
+
complex.count.must_equal 3
|
34
|
+
end
|
35
|
+
it "should accept weird units" do
|
36
|
+
frequency = subject.new("/s").terms
|
37
|
+
frequency.count.must_equal 1
|
38
|
+
end
|
39
|
+
it "should accept units with a factor and unit" do
|
40
|
+
oddity = subject.new("2ms2").terms
|
41
|
+
oddity.count.must_equal 1
|
42
|
+
end
|
34
43
|
end
|
35
44
|
|
36
45
|
end
|
@@ -36,11 +36,11 @@ describe Unitwise::Expression::Parser do
|
|
36
36
|
end
|
37
37
|
|
38
38
|
describe "#exponent" do
|
39
|
-
it "must match positives
|
39
|
+
it "must match positives integers" do
|
40
40
|
subject.exponent.parse('4')[:exponent].must_equal(integer: '4')
|
41
41
|
end
|
42
|
-
it "must match
|
43
|
-
subject.exponent.parse('-5
|
42
|
+
it "must match negative integers" do
|
43
|
+
subject.exponent.parse('-5')[:exponent].must_equal(integer: '-5')
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
describe Unitwise::Functional do
|
4
|
+
subject { Unitwise::Functional }
|
5
|
+
%w{cel degf hpX hpC tan100 ph ld ln lg 2lg}.each do |function|
|
6
|
+
function = :"_#{function}"
|
7
|
+
describe function do
|
8
|
+
it 'should convert back and forth' do
|
9
|
+
number = rand.round(5)
|
10
|
+
there = subject.send function, number, true
|
11
|
+
back_again = subject.send function, there, false
|
12
|
+
back_again.round(5).must_equal number
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
@@ -37,15 +37,15 @@ describe Unitwise::Measurement do
|
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
40
|
-
let(:mph)
|
41
|
-
let(:kmh)
|
40
|
+
let(:mph) { Unitwise::Measurement.new(60, '[mi_i]/h') }
|
41
|
+
let(:kmh) { Unitwise::Measurement.new(100, 'km/h') }
|
42
42
|
let(:mile) { Unitwise::Measurement.new(3, '[mi_i]') }
|
43
|
-
let(:hpm)
|
44
|
-
let(:cui)
|
45
|
-
let(:cel)
|
46
|
-
let(:k)
|
47
|
-
let(:f)
|
48
|
-
let(:r)
|
43
|
+
let(:hpm) { Unitwise::Measurement.new(6, 'h/[mi_i]') }
|
44
|
+
let(:cui) { Unitwise::Measurement.new(12, "[in_i]3") }
|
45
|
+
let(:cel) { Unitwise::Measurement.new(22, 'Cel') }
|
46
|
+
let(:k) { Unitwise::Measurement.new(373.15, 'K') }
|
47
|
+
let(:f) { Unitwise::Measurement.new(98.6, '[degF]')}
|
48
|
+
let(:r) { Unitwise::Measurement.new(491.67, '[degR]') }
|
49
49
|
|
50
50
|
describe "#scalar" do
|
51
51
|
it "must return value relative to terminal atoms" do
|
@@ -153,6 +153,19 @@ describe Unitwise::Measurement do
|
|
153
153
|
exp.value.must_equal 0.037037037037037035
|
154
154
|
exp.unit.to_s.must_equal "1/[mi_i]3"
|
155
155
|
end
|
156
|
+
it "must not raise to a weird power" do
|
157
|
+
-> { mile ** 'weird' }.must_raise TypeError
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
describe "#coerce" do
|
162
|
+
let(:meter) { Unitwise::Measurement.new(1, 'm') }
|
163
|
+
it "must coerce numerics" do
|
164
|
+
meter.coerce(5).must_equal [ Unitwise::Measurement.new(5, '1'), meter ]
|
165
|
+
end
|
166
|
+
it "should raise an error for other crap" do
|
167
|
+
-> { meter.coerce("foo") }.must_raise TypeError
|
168
|
+
end
|
156
169
|
end
|
157
170
|
|
158
171
|
describe "#method_missing" do
|
data/unitwise.gemspec
CHANGED
@@ -17,8 +17,9 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^test/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
-
gem.add_dependency "
|
21
|
-
gem.add_dependency "
|
20
|
+
gem.add_dependency "liner", "~> 0.2.1"
|
21
|
+
gem.add_dependency "signed_multiset", "~> 0.2.0"
|
22
|
+
gem.add_dependency "parslet", "~> 1.5.0"
|
22
23
|
|
23
24
|
gem.add_development_dependency "minitest"
|
24
25
|
gem.add_development_dependency "rake"
|
metadata
CHANGED
@@ -1,43 +1,57 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: unitwise
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Josh Lewis
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-02-
|
11
|
+
date: 2014-02-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: liner
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.2.1
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.2.1
|
13
27
|
- !ruby/object:Gem::Dependency
|
14
28
|
name: signed_multiset
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
16
30
|
requirements:
|
17
|
-
- -
|
31
|
+
- - ~>
|
18
32
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
33
|
+
version: 0.2.0
|
20
34
|
type: :runtime
|
21
35
|
prerelease: false
|
22
36
|
version_requirements: !ruby/object:Gem::Requirement
|
23
37
|
requirements:
|
24
|
-
- -
|
38
|
+
- - ~>
|
25
39
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
40
|
+
version: 0.2.0
|
27
41
|
- !ruby/object:Gem::Dependency
|
28
42
|
name: parslet
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
30
44
|
requirements:
|
31
|
-
- -
|
45
|
+
- - ~>
|
32
46
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
47
|
+
version: 1.5.0
|
34
48
|
type: :runtime
|
35
49
|
prerelease: false
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
37
51
|
requirements:
|
38
|
-
- -
|
52
|
+
- - ~>
|
39
53
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
54
|
+
version: 1.5.0
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: minitest
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -138,7 +152,6 @@ files:
|
|
138
152
|
- lib/unitwise/expression/transformer.rb
|
139
153
|
- lib/unitwise/ext.rb
|
140
154
|
- lib/unitwise/ext/numeric.rb
|
141
|
-
- lib/unitwise/function.rb
|
142
155
|
- lib/unitwise/functional.rb
|
143
156
|
- lib/unitwise/measurement.rb
|
144
157
|
- lib/unitwise/prefix.rb
|
@@ -154,6 +167,7 @@ files:
|
|
154
167
|
- lib/unitwise/term.rb
|
155
168
|
- lib/unitwise/unit.rb
|
156
169
|
- lib/unitwise/version.rb
|
170
|
+
- simple_bench.rb
|
157
171
|
- test/test_helper.rb
|
158
172
|
- test/unitwise/atom_test.rb
|
159
173
|
- test/unitwise/base_test.rb
|
@@ -161,7 +175,7 @@ files:
|
|
161
175
|
- test/unitwise/expression/matcher_test.rb
|
162
176
|
- test/unitwise/expression/parser_test.rb
|
163
177
|
- test/unitwise/ext/numeric_test.rb
|
164
|
-
- test/unitwise/
|
178
|
+
- test/unitwise/functional_test.rb
|
165
179
|
- test/unitwise/measurement_test.rb
|
166
180
|
- test/unitwise/prefix_test.rb
|
167
181
|
- test/unitwise/term_test.rb
|
@@ -201,7 +215,7 @@ test_files:
|
|
201
215
|
- test/unitwise/expression/matcher_test.rb
|
202
216
|
- test/unitwise/expression/parser_test.rb
|
203
217
|
- test/unitwise/ext/numeric_test.rb
|
204
|
-
- test/unitwise/
|
218
|
+
- test/unitwise/functional_test.rb
|
205
219
|
- test/unitwise/measurement_test.rb
|
206
220
|
- test/unitwise/prefix_test.rb
|
207
221
|
- test/unitwise/term_test.rb
|
data/lib/unitwise/function.rb
DELETED
@@ -1,51 +0,0 @@
|
|
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
|
@@ -1,42 +0,0 @@
|
|
1
|
-
require 'test_helper'
|
2
|
-
|
3
|
-
describe Unitwise::Function do
|
4
|
-
subject { Unitwise::Function }
|
5
|
-
|
6
|
-
it '::all must be an Array' do
|
7
|
-
subject.all.must_be_instance_of Array
|
8
|
-
end
|
9
|
-
|
10
|
-
it '::add must add to :all' do
|
11
|
-
defined = subject.add('foo(2 3N)', ->{ 4 }, ->{ 6 })
|
12
|
-
subject.all.must_include(defined)
|
13
|
-
end
|
14
|
-
|
15
|
-
it "::find must find by name" do
|
16
|
-
defined = subject.add("foo", -> { 3 }, -> { 7 })
|
17
|
-
subject.find("foo").must_equal(defined)
|
18
|
-
end
|
19
|
-
|
20
|
-
let (:fp) { Unitwise::Function.new('foo(1 4J)', ->(x){ x + 1}, ->(x){ x - 1 }) }
|
21
|
-
|
22
|
-
it 'must have a #name' do
|
23
|
-
fp.name.must_equal('foo(1 4J)')
|
24
|
-
end
|
25
|
-
|
26
|
-
it 'must have a direct lambda' do
|
27
|
-
fp.direct.call(1).must_equal 2
|
28
|
-
end
|
29
|
-
|
30
|
-
it 'must have a inverse lambda' do
|
31
|
-
fp.inverse.call(1).must_equal 0
|
32
|
-
end
|
33
|
-
|
34
|
-
it 'must have a direct scalar' do
|
35
|
-
fp.functional(1, 1).must_equal 2
|
36
|
-
end
|
37
|
-
|
38
|
-
it 'must have an inverse scalar' do
|
39
|
-
fp.functional(1, -1).must_equal 0
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|