eqn 1.6.2 → 1.6.3

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: 7a298576c822eca16cdaf7281ec1344ea37f19a4
4
- data.tar.gz: 3408ed487ad7cae4ff268559d1e71bf769221d90
3
+ metadata.gz: fce3040108d20ba8a47b915c6878c541f0c845c3
4
+ data.tar.gz: b612f8b6d206e04a545c1577f8032ace2c6f4b43
5
5
  SHA512:
6
- metadata.gz: 2898eec4da5863b38c44460eb4d4fd980f4e71b9f481a92f5f09d14581399c313525dc324b3a5e2bf55768073e5b300c97687be4e8355132301d8ac1e4d9f1b0
7
- data.tar.gz: 372020127e6141058da57c09fdccde57d8332a930dc0a59d9b76621b8f033d7b4b2f602d495af541dc8a1d311081d2e6610753fb95173bd52cea84efcf39dfff
6
+ metadata.gz: c9fdd51eb3ffd6b8acee105627279a70c886da6cce6bc1d4813ef78de5143aa81b8748d974230cfb74bba279f0b7e8e460ede46f00b4ce8e8acb8a43c98f5162
7
+ data.tar.gz: 8e862df9a84a75763e52cebeca227c7bd5221fbd8daaff4b70c5f5b607db663958cd883ff60ac0775f7cffbd7dfec076e184966608f7d84e2a493a7e3844a6ad
data/README.md CHANGED
@@ -128,7 +128,7 @@ Rounds the number down (i.e. floor function). Optionally, pass a number of decim
128
128
 
129
129
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `bin/console` for an interactive prompt that will allow you to experiment.
130
130
 
131
- The gem uses rspec for testing; run `rspec` from the application root to execute the tests.
131
+ The gem uses rspec for testing and appraisal to test against multiple versions of treetop (the only runtime dependency). From the command line, run `bundle exec appraisal install` to install gems and `bundle exec appraisal rspec` to run the complete test suite. (You can still run `bundle exec rspec` to only test against the latest version of treetop while developing.)
132
132
 
133
133
  ## Authorship
134
134
 
@@ -136,7 +136,7 @@ Written by Zach Schneider for [Aha!, the world's #1 product roadmap software](ht
136
136
 
137
137
  ## Contributing
138
138
 
139
- 1. Fork it ( https://github.com/schneidmaster/eqn/fork )
139
+ 1. Fork it ([https://github.com/schneidmaster/eqn/fork](https://github.com/schneidmaster/eqn/fork))
140
140
  2. Create your feature branch (`git checkout -b my-new-feature`)
141
141
  3. Commit your changes (`git commit -am 'Add some feature'`)
142
142
  4. Push to the branch (`git push origin my-new-feature`)
@@ -4,7 +4,7 @@ lib = File.expand_path('../lib', __FILE__)
4
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
5
  require 'eqn/version'
6
6
 
7
- config_files = %w[.rspec .rubocop.yml .ruby-version Appraisals circle.yml]
7
+ config_files = %w(.rspec .rubocop.yml .ruby-version Appraisals circle.yml)
8
8
 
9
9
  Gem::Specification.new do |spec|
10
10
  spec.name = 'eqn'
data/lib/eqn.rb CHANGED
@@ -3,5 +3,8 @@ require 'treetop'
3
3
  # Load the parser from the Treetop grammar.
4
4
  Treetop.load(File.join(__dir__, 'eqn.treetop'))
5
5
 
6
- # Load eqn classes.
6
+ # Load custom eqn syntax node.
7
+ require File.join(__dir__, 'eqn', 'eqn_node')
8
+
9
+ # Load other eqn classes.
7
10
  Dir.glob File.join(__dir__, 'eqn', '**', '*.rb'), &method(:require)
@@ -1,4 +1,5 @@
1
1
  module Eqn
2
+ # Primary calculator class used for performing calculations or creating eqn instances.
2
3
  class Calculator
3
4
  def initialize(eqn, vars = {})
4
5
  @eqn = eqn
@@ -9,16 +10,16 @@ module Eqn
9
10
  if key_or_hash.is_a?(Hash)
10
11
  @vars.merge!(key_or_hash)
11
12
  else
12
- @vars[key_or_hash.intern] = value
13
+ @vars[key_or_hash.to_sym] = value
13
14
  end
14
15
  end
15
16
 
16
- def method_missing(method, *args)
17
- if %i[calculate calc valid?].include?(method)
17
+ def method_missing(method, *args) # rubocop:disable AbcSize
18
+ if %i(calculate calc valid?).include?(method)
18
19
  self.class.send(method, @eqn, @vars)
19
20
  elsif (match = method.to_s.match(/^[A-Za-z]+=$/))
20
- @vars[match.to_s.delete('=').intern] = args.first
21
- elsif (var = method.to_s.match(/^[A-Za-z]+$/).to_s.intern) && @vars.key?(var)
21
+ @vars[match.to_s.delete('=').to_sym] = args.first
22
+ elsif (var = method.to_s.match(/^[A-Za-z]+$/).to_s.to_sym) && @vars.key?(var)
22
23
  @vars[var]
23
24
  else
24
25
  super
@@ -26,7 +27,9 @@ module Eqn
26
27
  end
27
28
 
28
29
  def respond_to_missing?(method, _p2 = false)
29
- %i[calculate calc valid?].include?(method) || method.to_s.match(/^[A-Za-z]+=$/) || (var = method.to_s.match(/^[A-Za-z]+$/).to_s.intern) && @vars.key?(var)
30
+ %i(calculate calc valid?).include?(method) ||
31
+ method.to_s.match(/^[A-Za-z]+=$/) ||
32
+ (var = method.to_s.match(/^[A-Za-z]+$/).to_s.to_sym) && @vars.key?(var)
30
33
  end
31
34
 
32
35
  class << self
@@ -1,15 +1,17 @@
1
1
  module Eqn
2
- class Comparation < Treetop::Runtime::SyntaxNode
2
+ # Node class for a comparison between expressions.
3
+ class Comparation < EqnNode
3
4
  def value(vars = {})
4
5
  val_one = elements.shift.value(vars)
5
- if elements.empty?
6
+ if term?
6
7
  val_one
7
8
  else
8
9
  val_one.send(*elements.shift.value(vars))
9
10
  end
10
11
  end
11
12
 
12
- class CompGroup < Treetop::Runtime::SyntaxNode
13
+ # Node class for the operator and expression being compared.
14
+ class CompGroup < EqnNode
13
15
  def value(vars = {})
14
16
  [elements.shift.value(vars), elements.shift.value(vars)]
15
17
  end
@@ -0,0 +1,19 @@
1
+ module Eqn
2
+ # Eqn parser node with helper methods for cleaning.
3
+ class EqnNode < Treetop::Runtime::SyntaxNode
4
+ def term?
5
+ elements.nil? || elements.empty?
6
+ end
7
+
8
+ def clean_tree!
9
+ # Return if node is a terminal.
10
+ return if term?
11
+
12
+ # Delete any cruft syntax nodes.
13
+ elements.delete_if { |node| !node.is_a?(EqnNode) }
14
+
15
+ # Recurse over any elements with their own children.
16
+ elements.each(&:clean_tree!)
17
+ end
18
+ end
19
+ end
@@ -1,38 +1,47 @@
1
1
  module Eqn
2
- class Expression < Treetop::Runtime::SyntaxNode
2
+ # Node class for an expression.
3
+ class Expression < EqnNode
3
4
  def left_associative?
4
5
  elements.any? && elements.last.left_associative?
5
6
  end
6
7
 
7
- def term?
8
- elements.empty?
8
+ def value(vars = {})
9
+ base = elements.shift.value(vars)
10
+
11
+ # Aggressively consume left associative operators to maintain associativity.
12
+ base = consume_while_left_associative(base, vars)
13
+
14
+ # Apply next right-associative operator (if any) or return.
15
+ apply_next_operator(base, vars)
9
16
  end
10
17
 
11
- def value(vars = {})
12
- if elements.count == 1
13
- elements.shift.value(vars)
18
+ private
19
+
20
+ def consume_while_left_associative(base, vars)
21
+ return base unless left_associative?
22
+ base = apply_next_operator(base, vars)
23
+ consume_while_left_associative(base, vars)
24
+ end
25
+
26
+ def apply_next_operator(base, vars)
27
+ return base if term?
28
+ left_associative = elements.last.left_associative?
29
+ op, num_expr = elements.shift.value(vars)
30
+ if left_associative
31
+ apply_left_associative(base, vars, op, num_expr)
14
32
  else
15
- base = elements.shift.value(vars)
16
-
17
- # Aggressively consume left associative operators to maintain associativity.
18
- while left_associative?
19
- op, num_expr = elements.shift.value(vars)
20
- num_expr_operand = num_expr.elements.shift
21
- base = base.send(op, num_expr_operand.value(vars))
22
- elements.push num_expr.elements.shift unless num_expr.term?
23
- end
24
-
25
- # Apply next right-associative operator (if any) or return.
26
- if term?
27
- base
28
- else
29
- op, num_expr = elements.shift.value(vars)
30
- base.send(op, num_expr.value(vars))
31
- end
33
+ base.send(op, num_expr.value(vars))
32
34
  end
33
35
  end
34
36
 
35
- class ExprGroup < Treetop::Runtime::SyntaxNode
37
+ def apply_left_associative(base, vars, op, num_expr)
38
+ num_expr_operand = num_expr.elements.shift
39
+ elements.push(num_expr.elements.shift) unless num_expr.term?
40
+ base.send(op, num_expr_operand.value(vars))
41
+ end
42
+
43
+ # Node class for the operator and latter component of an expression.
44
+ class ExprGroup < EqnNode
36
45
  def left_associative?
37
46
  elements.first.left_associative?
38
47
  end
@@ -1,6 +1,7 @@
1
1
  module Eqn
2
2
  module Function
3
- class If < Treetop::Runtime::SyntaxNode
3
+ # Node class for the if function.
4
+ class If < EqnNode
4
5
  def value(vars = {})
5
6
  comp_val = elements.shift.value(vars)
6
7
  ls = elements.shift.value(vars)
@@ -10,35 +11,39 @@ module Eqn
10
11
  end
11
12
  end
12
13
 
13
- class RoundBase < Treetop::Runtime::SyntaxNode
14
- def value(fn, vars)
14
+ # Base node class for round functions.
15
+ class RoundBase < EqnNode
16
+ def value(vars)
15
17
  value = elements.shift.value(vars)
16
18
  raise ZeroDivisionError if value.is_a?(Float) && (value.abs == Float::INFINITY || value.nan?)
17
- if elements.empty?
18
- value.send(fn)
19
+ if term?
20
+ value.send(self.class::ROUND_METHOD)
19
21
  else
20
- decimals = elements.shift.value(vars)
21
- (value * 10**decimals).send(fn).to_f / 10**decimals
22
+ round_to_precision(vars, value)
22
23
  end
23
24
  end
25
+
26
+ private
27
+
28
+ def round_to_precision(vars, value)
29
+ decimals = elements.shift.value(vars)
30
+ (value * 10**decimals).send(self.class::ROUND_METHOD).to_f / 10**decimals
31
+ end
24
32
  end
25
33
 
34
+ # Node class for the round function.
26
35
  class Round < RoundBase
27
- def value(vars = {})
28
- super(:round, vars)
29
- end
36
+ ROUND_METHOD = :round
30
37
  end
31
38
 
39
+ # Node class for the roundup function.
32
40
  class RoundUp < RoundBase
33
- def value(vars = {})
34
- super(:ceil, vars)
35
- end
41
+ ROUND_METHOD = :ceil
36
42
  end
37
43
 
44
+ # Node class for the rounddown function.
38
45
  class RoundDown < RoundBase
39
- def value(vars = {})
40
- super(:floor, vars)
41
- end
46
+ ROUND_METHOD = :floor
42
47
  end
43
48
  end
44
49
  end
@@ -1,16 +1,27 @@
1
1
  module Eqn
2
- class Number < Treetop::Runtime::SyntaxNode
2
+ # Node class for a simple number.
3
+ class Number < EqnNode
3
4
  def value(vars = {})
4
5
  base = elements.shift.value(vars)
5
- # Apply any exponent.
6
- base *= elements.shift.value(vars) unless elements.empty?
7
- base
6
+
7
+ return base if term?
8
+
9
+ if instance_of?(Float)
10
+ # Apply any decimal if a float.
11
+ base + elements.shift.value(vars)
12
+ else
13
+ # Apply any exponent if a simple number.
14
+ base * elements.shift.value(vars)
15
+ end
8
16
  end
9
17
 
10
- class SignedNumber < Treetop::Runtime::SyntaxNode
18
+ class Float < Number; end
19
+
20
+ # Node class for a signed number.
21
+ class SignedNumber < EqnNode
11
22
  def value(vars = {})
12
23
  # Store sign if any.
13
- sign_negative = elements.shift.negative? if elements.first.is_a? Terminal::Sign
24
+ sign_negative = elements.shift.negative? if elements.first.is_a?(Terminal::Sign)
14
25
 
15
26
  # Evaluate float.
16
27
  value = elements.shift.value(vars)
@@ -20,24 +31,15 @@ module Eqn
20
31
  end
21
32
  end
22
33
 
23
- class Float < Treetop::Runtime::SyntaxNode
24
- def value(vars = {})
25
- base = elements.shift.value(vars)
26
-
27
- # Add any decimal.
28
- base += elements.shift.value(vars) unless elements.empty?
29
-
30
- base
31
- end
32
- end
33
-
34
- class Decimal < Treetop::Runtime::SyntaxNode
34
+ # Node class for the decimal part of a non-integer.
35
+ class Decimal < EqnNode
35
36
  def value(_vars = {})
36
37
  elements.shift.dec_value
37
38
  end
38
39
  end
39
40
 
40
- class Exponent < Treetop::Runtime::SyntaxNode
41
+ # Node class for the exponent part of a number.
42
+ class Exponent < EqnNode
41
43
  def value(vars = {})
42
44
  10**elements.shift.value(vars)
43
45
  end
@@ -1,25 +1,19 @@
1
1
  module Eqn
2
+ # Primary parser class to convert a string equation to a tree of nodes.
2
3
  class Parser
3
4
  class << self
4
- def parse(data)
5
+ def parse(equation)
5
6
  parser = EqnParser.new
6
7
 
7
- # Pass the data over to the parser instance.
8
- tree = parser.parse(data)
8
+ # Pass the equation over to the parser instance.
9
+ root_node = parser.parse(equation)
9
10
 
10
11
  # Raise any errors.
11
- raise ParseError, "Parse error at offset: #{parser.index} -- #{parser.failure_reason}" if tree.nil?
12
+ raise ParseError, "Parse error at offset: #{parser.index} -- #{parser.failure_reason}" if root_node.nil?
12
13
 
13
14
  # Remove extraneous nodes and return tree.
14
- clean_tree(tree)
15
- end
16
-
17
- private
15
+ root_node.clean_tree!
18
16
 
19
- def clean_tree(root_node)
20
- return if root_node.elements.nil?
21
- root_node.elements.delete_if { |node| node.class == Treetop::Runtime::SyntaxNode }
22
- root_node.elements.each(&method(:clean_tree))
23
17
  root_node
24
18
  end
25
19
  end
@@ -1,15 +1,17 @@
1
1
  module Eqn
2
2
  module Terminal
3
- class Variable < Treetop::Runtime::SyntaxNode
3
+ # Node class for a variable.
4
+ class Variable < EqnNode
4
5
  def value(vars = {})
5
- val = vars[text_value.intern]
6
+ val = vars[text_value.to_sym]
6
7
  raise NoVariableValueError, "No value given for: #{text_value}" unless val
7
- raise NonNumericVariableError, "Variable #{text_value} value is nonnumeric: #{val}" unless val.is_a? Numeric
8
+ raise NonNumericVariableError, "Variable #{text_value} value is nonnumeric: #{val}" unless val.is_a?(Numeric)
8
9
  val
9
10
  end
10
11
  end
11
12
 
12
- class Digits < Treetop::Runtime::SyntaxNode
13
+ # Node class for a group of numbers.
14
+ class Digits < EqnNode
13
15
  def dec_value
14
16
  ".#{text_value}".to_f
15
17
  end
@@ -19,28 +21,31 @@ module Eqn
19
21
  end
20
22
  end
21
23
 
22
- class Sign < Treetop::Runtime::SyntaxNode
24
+ # Node class for a number sign.
25
+ class Sign < EqnNode
23
26
  def negative?
24
27
  text_value == '-'
25
28
  end
26
29
  end
27
30
 
28
- class Op < Treetop::Runtime::SyntaxNode
31
+ # Node class for an operator.
32
+ class Op < EqnNode
29
33
  def left_associative?
30
- is_a? LeftAssociativeOp
34
+ is_a?(LeftAssociativeOp)
31
35
  end
32
36
 
33
37
  def value(_vars = {})
34
- text_value == '^' ? :** : text_value.intern
38
+ text_value == '^' ? :** : text_value.to_sym
35
39
  end
36
40
 
37
41
  class LeftAssociativeOp < Op; end
38
42
  class RightAssociativeOp < Op; end
39
43
  end
40
44
 
41
- class CompOp < Treetop::Runtime::SyntaxNode
45
+ # Node class for a comparation operator.
46
+ class CompOp < EqnNode
42
47
  def value(_vars = {})
43
- text_value == '=' ? :== : text_value.intern
48
+ text_value == '=' ? :== : text_value.to_sym
44
49
  end
45
50
  end
46
51
  end
@@ -1,3 +1,3 @@
1
1
  module Eqn
2
- VERSION = '1.6.2'.freeze
2
+ VERSION = '1.6.3'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eqn
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 1.6.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Zach Schneider
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-05-04 00:00:00.000000000 Z
11
+ date: 2017-06-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: treetop
@@ -142,7 +142,7 @@ files:
142
142
  - lib/eqn.treetop
143
143
  - lib/eqn/calculator.rb
144
144
  - lib/eqn/comparation.rb
145
- - lib/eqn/engine.rb
145
+ - lib/eqn/eqn_node.rb
146
146
  - lib/eqn/errors.rb
147
147
  - lib/eqn/expression.rb
148
148
  - lib/eqn/function.rb
@@ -1,9 +0,0 @@
1
- module Eqn
2
- if defined? Rails
3
- class Engine < ::Rails::Engine
4
- initializer 'eqn' do
5
- config.autoload_paths += Dir["#{config.root}/lib/**/"]
6
- end
7
- end
8
- end
9
- end