romanesco 0.1.8
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 +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
|
+
[](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: []
|