eqn 1.6.2 → 1.6.3

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