unitwise 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b743603a818807533ed423c0317ac33b05976d78
4
- data.tar.gz: ac6d5fb45058fecc3575e771f2e605cbc3e4afe0
3
+ metadata.gz: a382c394857814457fe921d7556aa91211d9a752
4
+ data.tar.gz: 24ecd6c8f3fcd11a281cc0fb860462f2b9766b4c
5
5
  SHA512:
6
- metadata.gz: 39a648b7693240696ff80404452c1b19bd67569dea17a394d84a4b9cf69e4874f3645ca7e83bb4149ce0304a973adfb9f55ed110a9833036e3c418ecaf0b3e97
7
- data.tar.gz: ba6432d868b98e2d0c93d1a821b400a54e9e837e09ee8a13ca41986ccf61a9e1a1586186f04e7cfb40297c6b07ae923109ca6593b634abad2057060148c15af0
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
- Obviously, Unitwise handles simple unit conversion. You can convert to any
42
- compatible unit (Unitwise won't let you convert say inches to pounds) with the
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 name method:
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. Dissimilar units will fail.
74
- For example, comparing two temperatures will work, comparing a mass to a length would fail.
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
- ### List of available units
165
+ ### Available Units
166
166
 
167
- - You can get the official list from the UCUM website in XML format.
168
- [unitsofmeasure.org/ucum-essence.xml](http://unitsofmeasure.org/ucum-essence.xml)
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
- - Unitwise occasionally converts the above XML into YAML for use by this
171
- library.
172
- [github.com/joshwlewis/unitwise/tree/master/data](//github.com/joshwlewis/unitwise/tree/master/data)
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
- def Unitwise(first_arg, last_arg=nil)
26
- if last_arg
27
- Unitwise::Measurement.new(first_arg, last_arg)
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, first_arg)
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
- attr_accessor :classification, :property, :metric, :special, :arbitrary
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
- def nonmetric?
31
- !metric?
32
- end
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
- def functional(x=scalar, direction=1)
67
- scale.functional(x, direction)
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
- attr_accessor :primary_code, :secondary_code, :symbol
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 initialize(attrs)
31
- attrs.each do |k, v|
32
- public_send :"#{k}=", v
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
@@ -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(input)
6
- if input.respond_to?(:terms)
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
- PARSERS = [:primary_code, :secondary_code, :names, :slugs, :symbol].map do |t|
6
- Parser.new(t)
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 parse
21
- PARSERS.reduce(nil) do |m, p|
22
- if prs = p.parse(expression) rescue next
23
- return prs
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
- if transform.respond_to?(:terms)
34
- transform.terms
35
- else
36
- Array(transform)
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) { number.as(: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.find(c) }
8
- rule(atom_code: simple(:c)) { Atom.find(c) }
9
- rule(term: subtree(:h)) { Term.new(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
@@ -1 +1,2 @@
1
+ require 'unitwise'
1
2
  require 'unitwise/ext/numeric'
@@ -1,20 +1,56 @@
1
1
  module Unitwise
2
2
  class Functional < Scale
3
+ extend Math
3
4
 
4
- attr_reader :function
5
+ def self._cel(x, forward=true)
6
+ forward ? x - 273.15 : x + 273.15
7
+ end
5
8
 
6
- def initialize(value, unit, function_name)
7
- super(value, unit)
8
- @function = Function.find(function_name)
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 scalar
12
- puts "Warning: Mathematical operations with special units not reccomended by UCUM."
13
- super()
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, direction=1)
17
- function.functional(x, direction)
52
+ def functional(x=scalar, forward = true)
53
+ self.class.send(:"_#{function_name}", x, forward)
18
54
  end
19
55
 
20
56
  end
@@ -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, -1)
70
+ other_unit.functional functional(value, false)
62
71
  else
63
- functional(value, -1) / other_unit.scalar
72
+ functional(value, false) / other_unit.scalar
64
73
  end
65
74
  else
66
75
  if other_unit.special?
@@ -1,6 +1,6 @@
1
1
  module Unitwise
2
2
  class Prefix < Base
3
- attr_reader :scalar
3
+ liner :scalar
4
4
 
5
5
  def self.data
6
6
  @data ||= YAML::load File.open(data_file)
@@ -29,8 +29,8 @@ module Unitwise
29
29
  unit.special?
30
30
  end
31
31
 
32
- def functional(x=value, direction=1)
33
- unit.functional(x, direction)
32
+ def functional(x=value, forward=true)
33
+ unit.functional(x, forward)
34
34
  end
35
35
 
36
36
  def scalar
data/lib/unitwise/term.rb CHANGED
@@ -1,18 +1,10 @@
1
1
  require 'signed_multiset'
2
2
  module Unitwise
3
3
  class Term
4
- attr_writer :atom_code, :prefix_code, :atom, :prefix
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, direction=1)
57
- (factor * (prefix ? prefix.scalar : 1)) * (atom ? atom.functional(x, direction) : 1) ** exponent
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
- else
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
- else
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, direction=1)
32
- terms.first.functional(x, direction)
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
@@ -1,3 +1,3 @@
1
1
  module Unitwise
2
- VERSION = "0.3.0"
2
+ VERSION = "0.3.1"
3
3
  end
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
@@ -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 and integers" do
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 negatives and fixnums" do
43
- subject.exponent.parse('-5.4')[:exponent].must_equal(fixnum: '-5.4')
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) { Unitwise::Measurement.new(60, '[mi_i]/h') }
41
- let(:kmh) { Unitwise::Measurement.new(100, 'km/h') }
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) { 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]') }
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
@@ -22,4 +22,12 @@ describe Unitwise::Prefix do
22
22
  end
23
23
  end
24
24
 
25
+ describe "#search" do
26
+ it "should search for a prefix" do
27
+ result = subject.search("mi")
28
+ result.count.must_equal 3
29
+ result.must_include subject.find("milli")
30
+ end
31
+ end
32
+
25
33
  end
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 "signed_multiset"
21
- gem.add_dependency "parslet"
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.0
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-01 00:00:00.000000000 Z
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: '0'
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: '0'
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: '0'
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: '0'
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/function_test.rb
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/function_test.rb
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
@@ -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