romanesco 0.1.8

Sign up to get free protection for your applications and to get access to all the features.
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,15 @@
1
+ module Romanesco
2
+ class Expression
3
+
4
+ attr_accessor :parent
5
+
6
+ def initialize(expression_part)
7
+ @expression_part = expression_part
8
+ end
9
+
10
+ def evaluate(options)
11
+ raise NotImplementedError
12
+ end
13
+
14
+ end
15
+ 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,15 @@
1
+ require 'romanesco/elements/unary_operator'
2
+
3
+ module Romanesco
4
+ class ParenthesesOperator < UnaryOperator
5
+
6
+ def initialize(symbol)
7
+ @symbol = '()'
8
+ end
9
+
10
+ def default_precedence
11
+ 100
12
+ end
13
+
14
+ end
15
+ 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
@@ -0,0 +1,3 @@
1
+ module Romanesco
2
+ VERSION = '0.1.8'
3
+ end
data/lib/romanesco.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'romanesco/version'
2
+ require 'romanesco/parser'
3
+
4
+ module Romanesco
5
+ extend self
6
+
7
+ def self.parse(raw_expression)
8
+ parser = Parser.new
9
+ parser.parse(raw_expression)
10
+ end
11
+
12
+ end
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: []