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 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