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 +4 -4
- data/README.md +2 -2
- data/eqn.gemspec +1 -1
- data/lib/eqn.rb +4 -1
- data/lib/eqn/calculator.rb +9 -6
- data/lib/eqn/comparation.rb +5 -3
- data/lib/eqn/eqn_node.rb +19 -0
- data/lib/eqn/expression.rb +33 -24
- data/lib/eqn/function.rb +21 -16
- data/lib/eqn/number.rb +21 -19
- data/lib/eqn/parser.rb +6 -12
- data/lib/eqn/terminal.rb +15 -10
- data/lib/eqn/version.rb +1 -1
- metadata +3 -3
- data/lib/eqn/engine.rb +0 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fce3040108d20ba8a47b915c6878c541f0c845c3
|
4
|
+
data.tar.gz: b612f8b6d206e04a545c1577f8032ace2c6f4b43
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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 (
|
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`)
|
data/eqn.gemspec
CHANGED
@@ -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
|
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
|
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)
|
data/lib/eqn/calculator.rb
CHANGED
@@ -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.
|
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
|
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('=').
|
21
|
-
elsif (var = method.to_s.match(/^[A-Za-z]+$/).to_s.
|
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
|
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
|
data/lib/eqn/comparation.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
module Eqn
|
2
|
-
class
|
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
|
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
|
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
|
data/lib/eqn/eqn_node.rb
ADDED
@@ -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
|
data/lib/eqn/expression.rb
CHANGED
@@ -1,38 +1,47 @@
|
|
1
1
|
module Eqn
|
2
|
-
class
|
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
|
8
|
-
elements.
|
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
|
-
|
12
|
-
|
13
|
-
|
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
|
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
|
-
|
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
|
data/lib/eqn/function.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Eqn
|
2
2
|
module Function
|
3
|
-
class
|
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
|
14
|
-
|
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
|
18
|
-
value.send(
|
19
|
+
if term?
|
20
|
+
value.send(self.class::ROUND_METHOD)
|
19
21
|
else
|
20
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
40
|
-
super(:floor, vars)
|
41
|
-
end
|
46
|
+
ROUND_METHOD = :floor
|
42
47
|
end
|
43
48
|
end
|
44
49
|
end
|
data/lib/eqn/number.rb
CHANGED
@@ -1,16 +1,27 @@
|
|
1
1
|
module Eqn
|
2
|
-
class
|
2
|
+
# Node class for a simple number.
|
3
|
+
class Number < EqnNode
|
3
4
|
def value(vars = {})
|
4
5
|
base = elements.shift.value(vars)
|
5
|
-
|
6
|
-
base
|
7
|
-
|
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
|
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?
|
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
|
24
|
-
|
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
|
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
|
data/lib/eqn/parser.rb
CHANGED
@@ -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(
|
5
|
+
def parse(equation)
|
5
6
|
parser = EqnParser.new
|
6
7
|
|
7
|
-
# Pass the
|
8
|
-
|
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
|
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
|
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
|
data/lib/eqn/terminal.rb
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
module Eqn
|
2
2
|
module Terminal
|
3
|
-
class
|
3
|
+
# Node class for a variable.
|
4
|
+
class Variable < EqnNode
|
4
5
|
def value(vars = {})
|
5
|
-
val = vars[text_value.
|
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?
|
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
|
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
|
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
|
31
|
+
# Node class for an operator.
|
32
|
+
class Op < EqnNode
|
29
33
|
def left_associative?
|
30
|
-
is_a?
|
34
|
+
is_a?(LeftAssociativeOp)
|
31
35
|
end
|
32
36
|
|
33
37
|
def value(_vars = {})
|
34
|
-
text_value == '^' ? :** : text_value.
|
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
|
45
|
+
# Node class for a comparation operator.
|
46
|
+
class CompOp < EqnNode
|
42
47
|
def value(_vars = {})
|
43
|
-
text_value == '=' ? :== : text_value.
|
48
|
+
text_value == '=' ? :== : text_value.to_sym
|
44
49
|
end
|
45
50
|
end
|
46
51
|
end
|
data/lib/eqn/version.rb
CHANGED
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.
|
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-
|
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/
|
145
|
+
- lib/eqn/eqn_node.rb
|
146
146
|
- lib/eqn/errors.rb
|
147
147
|
- lib/eqn/expression.rb
|
148
148
|
- lib/eqn/function.rb
|