romanesco 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +89 -0
- data/lib/romanesco/elements/addition_operator.rb +20 -0
- data/lib/romanesco/elements/binary_operator.rb +36 -0
- data/lib/romanesco/elements/constant_operand.rb +21 -0
- data/lib/romanesco/elements/division_operator.rb +20 -0
- data/lib/romanesco/elements/expression.rb +15 -0
- data/lib/romanesco/elements/multiplication_operator.rb +19 -0
- data/lib/romanesco/elements/operand.rb +15 -0
- data/lib/romanesco/elements/operator.rb +72 -0
- data/lib/romanesco/elements/parentheses_operator.rb +15 -0
- data/lib/romanesco/elements/subtraction_operator.rb +18 -0
- data/lib/romanesco/elements/unary_operator.rb +35 -0
- data/lib/romanesco/elements/variable_operand.rb +26 -0
- data/lib/romanesco/errors.rb +18 -0
- data/lib/romanesco/expression_tree.rb +92 -0
- data/lib/romanesco/expression_tree_builder.rb +15 -0
- data/lib/romanesco/parser.rb +33 -0
- data/lib/romanesco/state_machine.rb +65 -0
- data/lib/romanesco/token.rb +90 -0
- data/lib/romanesco/tokeniser.rb +43 -0
- data/lib/romanesco/validators/character_validator.rb +18 -0
- data/lib/romanesco/validators/parenthesis_count_validator.rb +20 -0
- data/lib/romanesco/validators/validator.rb +22 -0
- data/lib/romanesco/version.rb +3 -0
- data/lib/romanesco.rb +12 -0
- metadata +115 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: efdfbccebe16fe99c864f88845a127cf8cc663a9
|
4
|
+
data.tar.gz: 661732d53c05909b584cb63f26361c2222bb091c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bfb3ec65909612ee9e14b06d5bab01bd22f4da04a8cdd170fc0ac77cec68af689dfb028f23a53f773f1c30710d40a34d14f97759e0fee49872d887cbad0d04d8
|
7
|
+
data.tar.gz: 8619b8a4be699f71f80cb7dc3718c6f8188dd09d65758b82fe56ddc5094e39455d3a3ee7822aaaa74cf90f3be07f17379a6e825a1a7c743221c3640854e8437b
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Charlie Ablett and Craig Taube-Schock (Enspiral Craftworks)
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
# Romanesco
|
2
|
+
|
3
|
+
[![Gem Version](https://badge.fury.io/rb/romanesco.svg)](http://badge.fury.io/rb/romanesco)
|
4
|
+
|
5
|
+
Romanesco allows you to parse simple mathematical expressions, and creates a fully object-oriented expression tree for evaluation.
|
6
|
+
|
7
|
+
You can inject variable values at runtime so that formulae can be used, edited and re-saved as part of an application.
|
8
|
+
|
9
|
+
Currently Romanesco supports the four basic operators (addition, subtraction, multiplication, division) as well as parentheses. It supports the use of constants and named variables.
|
10
|
+
|
11
|
+
Written by Charlie Ablett of Enspiral Craftworks (http://www.enspiral.com) with support from Craig Taube-Schock.
|
12
|
+
|
13
|
+
MIT License.
|
14
|
+
|
15
|
+
So named because Romanesco broccoli is neat-looking and has self-repeating patterns, much like the structure of the resulting expression tree. Also, it's tasty.
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'romanesco'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install romanesco
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
You can use Romanesco as a basic calculator if you want.
|
34
|
+
|
35
|
+
expression = Romanesco.parse("1+10*3")
|
36
|
+
result = expression.evaluate #=> 31.0
|
37
|
+
original_expression = expression.to_s #=> "1 + 10 * 3"
|
38
|
+
|
39
|
+
If you have variables, inject them as follows:
|
40
|
+
|
41
|
+
expression = Romanesco.parse("a * (b + c)")
|
42
|
+
result = expression.evaluate(a: 5, b: 2, c: 10) # => 100.0
|
43
|
+
original_expression_string = expression.to_s # => "a * (b + c)"
|
44
|
+
|
45
|
+
In fact, you can inject anything that responds to the message `evaluate(options)`...
|
46
|
+
|
47
|
+
class FakeClass
|
48
|
+
def evaluate(options)
|
49
|
+
100
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
expression = Romanesco.parse("one_hundred + 2.2")
|
54
|
+
result = expression.evaluate(one_hundred: FakeClass.new) # => 102.2
|
55
|
+
|
56
|
+
... including *other expressions* (Don't worry, in this case we *can* detect infinite loops):
|
57
|
+
|
58
|
+
dangerous_animals = Romanesco.parse("honey_badgers + dangerous_australian_animals")
|
59
|
+
australian = Romanesco.parse("box_jellyfish + snakes")
|
60
|
+
result = dangerous_animals.evaluate(box_jellyfish: 10, snakes: 4, dangerous_australian_animals: australian, honey_badgers: 1) #=> 15
|
61
|
+
|
62
|
+
Get a list of the variables you'll need by calling `required_variables`:
|
63
|
+
|
64
|
+
expression = Romanesco.parse("(lions + tigers) * bears")
|
65
|
+
required = expression.required_variables #=> [:lions, :tigers, :bears]
|
66
|
+
|
67
|
+
If you're missing any variables, Romanesco will let you know:
|
68
|
+
|
69
|
+
expression = Romanesco.parse("maine_coon * japanese_bobtail")
|
70
|
+
|
71
|
+
begin
|
72
|
+
expression.evaluate
|
73
|
+
rescue Romanesco::MissingVariables => e
|
74
|
+
e.missing_variables #=> [:maine_coon, :japanese_bobtail]
|
75
|
+
end
|
76
|
+
|
77
|
+
Otherwise, you can provide a default value for any missing variables:
|
78
|
+
|
79
|
+
expression = Romanesco.parse("maine_coon * japanese_bobtail")
|
80
|
+
|
81
|
+
expression.evaluate({}, 3) #=> 9.0
|
82
|
+
|
83
|
+
## Contributing
|
84
|
+
|
85
|
+
1. Fork it
|
86
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
87
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
88
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
89
|
+
5. Create new Pull Request
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'romanesco/elements/binary_operator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class AdditionOperator < BinaryOperator
|
5
|
+
|
6
|
+
def initialize(symbol)
|
7
|
+
@symbol = '+'
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(options={})
|
11
|
+
left_result, right_result = super(options)
|
12
|
+
left_result + right_result
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_precedence
|
16
|
+
10
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'romanesco/elements/operator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class BinaryOperator < Operator
|
5
|
+
|
6
|
+
attr_accessor :left_operand, :right_operand, :symbol
|
7
|
+
|
8
|
+
def initialize(symbol)
|
9
|
+
@symbol = symbol
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(options)
|
13
|
+
check_for_blank_symbol
|
14
|
+
left_result = @left_operand.evaluate(options)
|
15
|
+
right_result = @right_operand.evaluate(options)
|
16
|
+
return left_result, right_result
|
17
|
+
end
|
18
|
+
|
19
|
+
def to_s
|
20
|
+
"#{@left_operand.to_s} #{symbol} #{@right_operand.to_s}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def insert_element_to_left(element)
|
24
|
+
@left_operand = element
|
25
|
+
element.parent = self
|
26
|
+
element
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert_element_to_right(element)
|
30
|
+
@right_operand = element
|
31
|
+
element.parent = self
|
32
|
+
element
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'romanesco/elements/operand'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class ConstantOperand < Operand
|
5
|
+
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
def initialize(value)
|
9
|
+
@value = value.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(options)
|
13
|
+
@value
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
@value.to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'romanesco/elements/binary_operator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class DivisionOperator < BinaryOperator
|
5
|
+
|
6
|
+
def initialize(symbol)
|
7
|
+
@symbol = '/'
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(options)
|
11
|
+
left_result, right_result = super(options)
|
12
|
+
left_result / right_result
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_precedence
|
16
|
+
50
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'romanesco/elements/binary_operator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class MultiplicationOperator < BinaryOperator
|
5
|
+
|
6
|
+
def initialize(symbol)
|
7
|
+
@symbol = '*'
|
8
|
+
end
|
9
|
+
|
10
|
+
def evaluate(options)
|
11
|
+
left_result, right_result = super(options)
|
12
|
+
left_result * right_result
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_precedence
|
16
|
+
50
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'romanesco/elements/expression'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class Operand < Expression
|
5
|
+
|
6
|
+
def connect(last_operator, last_operand)
|
7
|
+
if last_operator && last_operator.is_a?(BinaryOperator) && last_operator.left_operand
|
8
|
+
last_operator.insert_element_to_right(self)
|
9
|
+
elsif last_operator
|
10
|
+
last_operator.insert_element_to_right(self)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
require 'romanesco/elements/expression'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class Operator < Expression
|
5
|
+
|
6
|
+
attr_accessor :symbol
|
7
|
+
|
8
|
+
def evaluate(options)
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
11
|
+
|
12
|
+
def precedence
|
13
|
+
@precedence || default_precedence
|
14
|
+
end
|
15
|
+
|
16
|
+
def check_for_blank_symbol
|
17
|
+
raise NoSymbolError if @symbol.nil? || @symbol.gsub(/\s+/, '').empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
def precedence=(value)
|
21
|
+
@precedence = value
|
22
|
+
end
|
23
|
+
|
24
|
+
def default_precedence
|
25
|
+
raise NotImplementedError
|
26
|
+
end
|
27
|
+
|
28
|
+
def connect(last_operator, last_operand)
|
29
|
+
if last_operator && last_operator.is_a?(ParenthesesOperator) && last_operator.precedence > self.precedence
|
30
|
+
connect_in_place(last_operator, last_operand)
|
31
|
+
elsif last_operator && last_operator.is_a?(ParenthesesOperator) && self.is_a?(ParenthesesOperator)
|
32
|
+
last_operator.connect_to_left(self)
|
33
|
+
elsif last_operator && last_operator.parent && last_operator.precedence >= self.precedence
|
34
|
+
connect_in_place_with_parent(last_operator, last_operand)
|
35
|
+
elsif last_operator && last_operator.parent && last_operator.precedence < self.precedence
|
36
|
+
connect_in_place_with_parent(last_operator, last_operand)
|
37
|
+
elsif last_operator && last_operator.is_a?(ParenthesesOperator) && last_operator.precedence < self.precedence
|
38
|
+
connect_up_tree(last_operator)
|
39
|
+
elsif last_operator && last_operator.precedence >= self.precedence
|
40
|
+
connect_up_tree(last_operator)
|
41
|
+
elsif last_operator && self.is_a?(ParenthesesOperator)
|
42
|
+
connect_to_right(last_operator)
|
43
|
+
elsif last_operator
|
44
|
+
connect_in_place(last_operator, last_operand)
|
45
|
+
else
|
46
|
+
connect_to_left(last_operand)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def connect_in_place_with_parent(last_operator, last_operand)
|
51
|
+
last_operator.parent.insert_element_to_right(self)
|
52
|
+
self.insert_element_to_left(last_operand)
|
53
|
+
end
|
54
|
+
|
55
|
+
def connect_in_place(last_operator, last_operand)
|
56
|
+
self.insert_element_to_left(last_operand)
|
57
|
+
last_operator.insert_element_to_right(self)
|
58
|
+
end
|
59
|
+
|
60
|
+
def connect_up_tree(last_operator)
|
61
|
+
self.insert_element_to_left(last_operator)
|
62
|
+
end
|
63
|
+
|
64
|
+
def connect_to_left(element)
|
65
|
+
self.insert_element_to_left(element)
|
66
|
+
end
|
67
|
+
|
68
|
+
def connect_to_right(element)
|
69
|
+
element.insert_element_to_right(self)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Romanesco
|
2
|
+
class SubtractionOperator < BinaryOperator
|
3
|
+
|
4
|
+
def initialize(symbol)
|
5
|
+
@symbol = '-'
|
6
|
+
end
|
7
|
+
|
8
|
+
def evaluate(options)
|
9
|
+
left_result, right_result = super(options)
|
10
|
+
left_result - right_result
|
11
|
+
end
|
12
|
+
|
13
|
+
def default_precedence
|
14
|
+
10
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'romanesco/elements/operator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class UnaryOperator < Operator
|
5
|
+
|
6
|
+
attr_accessor :operand, :symbol
|
7
|
+
|
8
|
+
def initialize(symbol)
|
9
|
+
@symbol = symbol
|
10
|
+
end
|
11
|
+
|
12
|
+
def evaluate(options)
|
13
|
+
check_for_blank_symbol
|
14
|
+
@result = @operand.evaluate(options)
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
"(#{@operand.to_s})"
|
19
|
+
end
|
20
|
+
|
21
|
+
def insert_element_to_left(element)
|
22
|
+
insert_element(element)
|
23
|
+
end
|
24
|
+
|
25
|
+
def insert_element_to_right(element)
|
26
|
+
insert_element(element)
|
27
|
+
end
|
28
|
+
|
29
|
+
def insert_element(element)
|
30
|
+
@operand = element
|
31
|
+
element.parent = self if element
|
32
|
+
element
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'romanesco/elements/operand'
|
2
|
+
require 'romanesco/errors'
|
3
|
+
|
4
|
+
module Romanesco
|
5
|
+
|
6
|
+
class VariableOperand < Operand
|
7
|
+
|
8
|
+
attr_accessor :name
|
9
|
+
|
10
|
+
def initialize(name)
|
11
|
+
@name = name
|
12
|
+
end
|
13
|
+
|
14
|
+
def evaluate(options)
|
15
|
+
element = options[@name.to_sym]
|
16
|
+
return element.evaluate(options) if element.respond_to?(:evaluate)
|
17
|
+
options[@name.to_sym].to_f if element.is_a? Numeric
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
@name.to_s
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Romanesco
|
2
|
+
|
3
|
+
class NoSymbolError < StandardError; end
|
4
|
+
class InvalidExpressionError < StandardError; end
|
5
|
+
|
6
|
+
class MissingVariables < StandardError
|
7
|
+
attr_accessor :missing_variables
|
8
|
+
|
9
|
+
def initialize(message = nil, missing_variables = nil)
|
10
|
+
super(message)
|
11
|
+
self.missing_variables = missing_variables
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
class HasInfiniteLoopError < StandardError; end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'romanesco/errors'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class ExpressionTree
|
5
|
+
attr_accessor :last_operand, :last_operator, :original_expression
|
6
|
+
attr_reader :required_variables
|
7
|
+
|
8
|
+
def initialize(expression)
|
9
|
+
@original_expression = expression
|
10
|
+
@required_variables = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(element)
|
14
|
+
element.connect(@last_operator, @last_operand)
|
15
|
+
@last_operand = element if element.is_a? Operand
|
16
|
+
@last_operator = element if element.is_a? Operator
|
17
|
+
@required_variables << element.name.to_sym if element.is_a? VariableOperand
|
18
|
+
end
|
19
|
+
|
20
|
+
def close_parenthesis
|
21
|
+
if last_operator.is_a? ParenthesesOperator
|
22
|
+
current_node = @last_operator
|
23
|
+
else
|
24
|
+
current_node = @last_operand
|
25
|
+
until current_node.is_a? ParenthesesOperator || current_node.parent.nil?
|
26
|
+
current_node = current_node.parent
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
current_node.precedence = 0
|
31
|
+
@last_operand = @last_operator = current_node
|
32
|
+
end
|
33
|
+
|
34
|
+
def evaluate(options={}, default_value=nil)
|
35
|
+
start = starting_point
|
36
|
+
check_for_loops(start, options)
|
37
|
+
missing_variables, new_options = check_for_missing_variables(start, options, [], default_value)
|
38
|
+
raise MissingVariables.new("Missing variables: #{missing_variables.join', '}", missing_variables) unless missing_variables.empty?
|
39
|
+
start.evaluate(new_options)
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_s
|
43
|
+
starting_point.to_s
|
44
|
+
end
|
45
|
+
|
46
|
+
def starting_point
|
47
|
+
return @starting_point if @starting_point
|
48
|
+
current_node = @last_operand
|
49
|
+
until current_node.parent.nil?
|
50
|
+
current_node = current_node.parent
|
51
|
+
end
|
52
|
+
@starting_point = current_node
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def check_for_loops(start, options)
|
58
|
+
iterate_to_variables(self, start, options) do |node, element, opts, block|
|
59
|
+
variable_value = opts[element.name.to_sym]
|
60
|
+
if variable_value.respond_to? :starting_point
|
61
|
+
raise HasInfiniteLoopError.new('Cannot evaluate - infinite loop detected') if node == variable_value
|
62
|
+
iterate_to_variables node, variable_value.starting_point, opts, &block if variable_value.respond_to? :evaluate
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def check_for_missing_variables(start, options, missing_variables, default_value)
|
68
|
+
iterate_to_variables(missing_variables, start, options) do |missing_variables, element, opts, block|
|
69
|
+
variable_value = opts[element.name.to_sym]
|
70
|
+
if variable_value.respond_to? :starting_point
|
71
|
+
iterate_to_variables missing_variables, variable_value.starting_point, opts, &block if variable_value.respond_to? :evaluate
|
72
|
+
elsif variable_value.nil?
|
73
|
+
options[element.name.to_sym] = default_value
|
74
|
+
missing_variables << element.name.to_sym unless default_value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
return missing_variables, options
|
78
|
+
end
|
79
|
+
|
80
|
+
def iterate_to_variables(node, element, options, &block)
|
81
|
+
if element.is_a? BinaryOperator
|
82
|
+
iterate_to_variables node, element.left_operand, options, &block
|
83
|
+
iterate_to_variables node, element.right_operand, options, &block
|
84
|
+
elsif element.is_a? UnaryOperator
|
85
|
+
iterate_to_variables node, element.operand, options, &block
|
86
|
+
elsif element.is_a? VariableOperand
|
87
|
+
block.call(node, element, options, block)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'romanesco/state_machine'
|
2
|
+
require 'romanesco/expression_tree'
|
3
|
+
|
4
|
+
module Romanesco
|
5
|
+
class ExpressionTreeBuilder
|
6
|
+
|
7
|
+
def build_tree(expression, elements)
|
8
|
+
first_state = StateZero
|
9
|
+
new_tree = ExpressionTree.new(expression)
|
10
|
+
first_state.transition(new_tree, elements)
|
11
|
+
new_tree
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'romanesco/expression_tree_builder'
|
2
|
+
require 'romanesco/validators/parenthesis_count_validator'
|
3
|
+
require 'romanesco/validators/character_validator'
|
4
|
+
require 'romanesco/tokeniser'
|
5
|
+
|
6
|
+
module Romanesco
|
7
|
+
class Parser
|
8
|
+
|
9
|
+
def parse(raw_expression)
|
10
|
+
validate_expression(raw_expression)
|
11
|
+
|
12
|
+
tokens = Tokeniser.new.tokenise(raw_expression)
|
13
|
+
|
14
|
+
tree_builder = ExpressionTreeBuilder.new
|
15
|
+
tree_builder.build_tree(raw_expression, tokens)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def validate_expression(raw_expression)
|
21
|
+
chain = build_chain([Validators::CharacterValidator, Validators::ParenthesisCountValidator])
|
22
|
+
chain.execute(raw_expression)
|
23
|
+
end
|
24
|
+
|
25
|
+
def build_chain(classes)
|
26
|
+
last = nil
|
27
|
+
classes.reverse.each do |klass|
|
28
|
+
last = klass.new(last)
|
29
|
+
end
|
30
|
+
last
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'romanesco/token'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
|
5
|
+
class ExpressionState
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def next_state(token)
|
9
|
+
state = @states[token.state_machine_class]
|
10
|
+
return state if state
|
11
|
+
raise InvalidExpressionError
|
12
|
+
end
|
13
|
+
|
14
|
+
def finish_here
|
15
|
+
@finishing_state = true
|
16
|
+
end
|
17
|
+
|
18
|
+
def transitions(value)
|
19
|
+
@states = value
|
20
|
+
end
|
21
|
+
|
22
|
+
def transition(tree, tokens)
|
23
|
+
if tokens.empty?
|
24
|
+
return tree if @finishing_state
|
25
|
+
raise InvalidExpressionError
|
26
|
+
end
|
27
|
+
current_token = tokens.shift
|
28
|
+
tree.add(current_token.element.new(current_token.expression_part)) unless current_token.is_a? CloseParenthesisToken
|
29
|
+
tree.close_parenthesis if current_token.is_a? CloseParenthesisToken
|
30
|
+
next_state = next_state(current_token)
|
31
|
+
classify(next_state).transition(tree, tokens)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def classify(string)
|
37
|
+
Object.const_get("Romanesco::#{string}")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class StateZero < ExpressionState
|
43
|
+
transitions( OperandToken => 'StateOne',
|
44
|
+
OpenParenthesisToken => 'StateZero' )
|
45
|
+
end
|
46
|
+
|
47
|
+
class StateOne < ExpressionState
|
48
|
+
transitions( OperatorToken => 'StateTwo',
|
49
|
+
CloseParenthesisToken => 'StateOne' )
|
50
|
+
|
51
|
+
finish_here
|
52
|
+
end
|
53
|
+
|
54
|
+
class StateTwo < ExpressionState
|
55
|
+
transitions( OperandToken => 'StateThree',
|
56
|
+
OpenParenthesisToken => 'StateZero' )
|
57
|
+
end
|
58
|
+
|
59
|
+
class StateThree < ExpressionState
|
60
|
+
transitions( OperatorToken => 'StateTwo',
|
61
|
+
CloseParenthesisToken => 'StateOne' )
|
62
|
+
|
63
|
+
finish_here
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'romanesco/elements/addition_operator'
|
2
|
+
require 'romanesco/elements/subtraction_operator'
|
3
|
+
require 'romanesco/elements/multiplication_operator'
|
4
|
+
require 'romanesco/elements/division_operator'
|
5
|
+
require 'romanesco/elements/parentheses_operator'
|
6
|
+
require 'romanesco/elements/variable_operand'
|
7
|
+
require 'romanesco/elements/constant_operand'
|
8
|
+
|
9
|
+
module Romanesco
|
10
|
+
|
11
|
+
class Token
|
12
|
+
|
13
|
+
attr_accessor :expression_part
|
14
|
+
|
15
|
+
def initialize(string)
|
16
|
+
@expression_part = string
|
17
|
+
end
|
18
|
+
|
19
|
+
def element
|
20
|
+
raise NotImplementedError
|
21
|
+
end
|
22
|
+
|
23
|
+
def state_machine_class
|
24
|
+
self.class
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class OperatorToken < Token
|
29
|
+
|
30
|
+
def state_machine_class
|
31
|
+
OperatorToken
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class MultiplicationToken < OperatorToken
|
36
|
+
def element
|
37
|
+
MultiplicationOperator
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class DivisionToken < OperatorToken
|
42
|
+
def element
|
43
|
+
DivisionOperator
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class AdditionToken < OperatorToken
|
48
|
+
def element
|
49
|
+
AdditionOperator
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class SubtractionToken < OperatorToken
|
54
|
+
def element
|
55
|
+
SubtractionOperator
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
class OperandToken < Token
|
61
|
+
|
62
|
+
def state_machine_class
|
63
|
+
OperandToken
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
class VariableToken < OperandToken
|
69
|
+
def element
|
70
|
+
VariableOperand
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class ConstantToken < OperandToken
|
75
|
+
def element
|
76
|
+
ConstantOperand
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
class OpenParenthesisToken < Token
|
82
|
+
def element
|
83
|
+
ParenthesesOperator
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class CloseParenthesisToken < Token
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative './token'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
class Tokeniser
|
5
|
+
|
6
|
+
TOKEN_TYPES = [
|
7
|
+
{name: 'addition', regex: /^\+/, token_class: AdditionToken},
|
8
|
+
{name: 'multiplication', regex: /^\*/, token_class: MultiplicationToken},
|
9
|
+
{name: 'subtraction', regex: /^\-/, token_class: SubtractionToken},
|
10
|
+
{name: 'division', regex: /^\//, token_class: DivisionToken},
|
11
|
+
{name: 'open parenthesis', regex: /^\(/, token_class: OpenParenthesisToken},
|
12
|
+
{name: 'close parenthesis', regex: /^\)/, token_class: CloseParenthesisToken},
|
13
|
+
{name: 'constant', regex: /^[0-9]*\.?[0-9]+/, token_class: ConstantToken},
|
14
|
+
{name: 'string', regex: /^[a-zA-Z_]+/, token_class: VariableToken}
|
15
|
+
]
|
16
|
+
|
17
|
+
def tokenise(raw_expression)
|
18
|
+
expression = remove_commas_and_whitespace(raw_expression)
|
19
|
+
|
20
|
+
tokens = []
|
21
|
+
|
22
|
+
while expression.length > 0
|
23
|
+
token_count = tokens.count
|
24
|
+
TOKEN_TYPES.each do |type|
|
25
|
+
if expression =~ type[:regex]
|
26
|
+
token_string = expression.slice!(type[:regex])
|
27
|
+
tokens << type[:token_class].send(:new, token_string)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
raise InvalidExpressionError.new("Expression error starting at #{expression}") if tokens.size == token_count
|
32
|
+
end
|
33
|
+
|
34
|
+
tokens
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def remove_commas_and_whitespace(raw_expression)
|
40
|
+
raw_expression.gsub(/\s+/, '').gsub(',', '')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'romanesco/validators/validator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
module Validators
|
5
|
+
class CharacterValidator < Validator
|
6
|
+
|
7
|
+
def validate(raw_expression)
|
8
|
+
regex = /^[\w\s\-\*\+\/\(\)\.,]*$/
|
9
|
+
|
10
|
+
no_whitespace = raw_expression.gsub(/\s+/, '')
|
11
|
+
raise InvalidExpressionError.new('Empty expression') if raw_expression.nil? || no_whitespace.empty?
|
12
|
+
raise InvalidExpressionError.new('Illegal characters found') unless regex =~ raw_expression
|
13
|
+
|
14
|
+
raw_expression
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'romanesco/validators/validator'
|
2
|
+
|
3
|
+
module Romanesco
|
4
|
+
module Validators
|
5
|
+
class ParenthesisCountValidator < Validator
|
6
|
+
|
7
|
+
def validate(raw_expression)
|
8
|
+
open_parenthesis = /(\()/
|
9
|
+
close_parenthesis = /(\))/
|
10
|
+
|
11
|
+
open_parenthesis_count = raw_expression.scan(open_parenthesis).size
|
12
|
+
close_parenthesis_count = raw_expression.scan(close_parenthesis).size
|
13
|
+
|
14
|
+
raise InvalidExpressionError.new('Uneven number of parentheses') unless open_parenthesis_count == close_parenthesis_count
|
15
|
+
|
16
|
+
raw_expression
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Romanesco
|
2
|
+
module Validators
|
3
|
+
class Validator
|
4
|
+
|
5
|
+
attr_accessor :next_step
|
6
|
+
|
7
|
+
def initialize(next_step=nil)
|
8
|
+
@next_step = next_step
|
9
|
+
end
|
10
|
+
|
11
|
+
def execute(object)
|
12
|
+
modified_object = validate(object)
|
13
|
+
return modified_object if next_step.nil?
|
14
|
+
next_step.execute(modified_object)
|
15
|
+
end
|
16
|
+
|
17
|
+
def validate(object)
|
18
|
+
raise NotImplementedError
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/romanesco.rb
ADDED
metadata
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: romanesco
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.8
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Charlie Ablett
|
8
|
+
- Craig Taube-Schock
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2014-06-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ~>
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1.3'
|
21
|
+
type: :development
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ~>
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1.3'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rspec
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - '>='
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '0'
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - '>='
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: '0'
|
42
|
+
- !ruby/object:Gem::Dependency
|
43
|
+
name: rake
|
44
|
+
requirement: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - '>='
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: '0'
|
49
|
+
type: :development
|
50
|
+
prerelease: false
|
51
|
+
version_requirements: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
description: Parses simple mathematical expressions, creates a fully object-oriented
|
57
|
+
expression tree. Evaluation can have injected variables.
|
58
|
+
email:
|
59
|
+
- charlie@enspiral.com
|
60
|
+
- craig.schock@enspiral.com
|
61
|
+
executables: []
|
62
|
+
extensions: []
|
63
|
+
extra_rdoc_files: []
|
64
|
+
files:
|
65
|
+
- lib/romanesco.rb
|
66
|
+
- lib/romanesco/expression_tree.rb
|
67
|
+
- lib/romanesco/token.rb
|
68
|
+
- lib/romanesco/elements/unary_operator.rb
|
69
|
+
- lib/romanesco/elements/addition_operator.rb
|
70
|
+
- lib/romanesco/elements/parentheses_operator.rb
|
71
|
+
- lib/romanesco/elements/variable_operand.rb
|
72
|
+
- lib/romanesco/elements/multiplication_operator.rb
|
73
|
+
- lib/romanesco/elements/division_operator.rb
|
74
|
+
- lib/romanesco/elements/expression.rb
|
75
|
+
- lib/romanesco/elements/subtraction_operator.rb
|
76
|
+
- lib/romanesco/elements/operator.rb
|
77
|
+
- lib/romanesco/elements/constant_operand.rb
|
78
|
+
- lib/romanesco/elements/operand.rb
|
79
|
+
- lib/romanesco/elements/binary_operator.rb
|
80
|
+
- lib/romanesco/expression_tree_builder.rb
|
81
|
+
- lib/romanesco/parser.rb
|
82
|
+
- lib/romanesco/tokeniser.rb
|
83
|
+
- lib/romanesco/state_machine.rb
|
84
|
+
- lib/romanesco/errors.rb
|
85
|
+
- lib/romanesco/validators/parenthesis_count_validator.rb
|
86
|
+
- lib/romanesco/validators/validator.rb
|
87
|
+
- lib/romanesco/validators/character_validator.rb
|
88
|
+
- lib/romanesco/version.rb
|
89
|
+
- ./LICENSE.txt
|
90
|
+
- ./README.md
|
91
|
+
homepage: ''
|
92
|
+
licenses:
|
93
|
+
- MIT
|
94
|
+
metadata: {}
|
95
|
+
post_install_message:
|
96
|
+
rdoc_options: []
|
97
|
+
require_paths:
|
98
|
+
- lib
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ~>
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '2.0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
requirements:
|
106
|
+
- - '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
requirements: []
|
110
|
+
rubyforge_project:
|
111
|
+
rubygems_version: 2.0.3
|
112
|
+
signing_key:
|
113
|
+
specification_version: 4
|
114
|
+
summary: Parse math expressions and evaluate with injected variables
|
115
|
+
test_files: []
|