math_engine 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,11 +1,13 @@
1
1
  # MathEngine
2
2
 
3
- MathEngine is a lightweight mathematical expression parser and evaluator. It currently handles addition, subtraction, multiplication, division and the use of variables.
3
+ MathEngine is a lightweight mathematical expression parser and evaluator. It currently handles addition, subtraction, multiplication, division, exponent, modulus and the use of variables and pre-defined functions.
4
4
 
5
5
  Install with
6
6
 
7
7
  gem install math_engine
8
8
 
9
+ The only dependency is on my other gem, lexr[http://github.com/michaelbaldry/lexr], which is really lightweight and has no external dependencies.
10
+
9
11
  ## An example: Expressions
10
12
 
11
13
  require 'rubygems'
@@ -25,4 +27,9 @@ if you missed a closing parenthesis, had an operator where it wasn't meant to be
25
27
 
26
28
  Unexpected multiplication(*), expected: number, variable name or open_parenthesis
27
29
 
28
- and that is pretty much every feature so far. Please let me know of any bugs or additions that you'd like to see!
30
+ and that is pretty much every feature so far. Please let me know of any bugs or additions that you'd like to see!
31
+
32
+ ## License
33
+
34
+ See the LICENSE file included with the distribution for licensing and
35
+ copyright details.
data/lib/errors.rb ADDED
@@ -0,0 +1,41 @@
1
+ class MathEngine
2
+ class ParseError < StandardError
3
+ def initialize(message)
4
+ @message = message
5
+ end
6
+
7
+ def to_s
8
+ @message
9
+ end
10
+ end
11
+
12
+ class UnknownVariableError < StandardError
13
+ def initialize(variable_name)
14
+ @variable_name = variable_name
15
+ end
16
+
17
+ def to_s
18
+ "Variable '#{@variable_name}' was referenced but does not exist"
19
+ end
20
+ end
21
+
22
+ class UnknownFunctionError < StandardError
23
+ def initialize(function_name)
24
+ @function_name = function_name
25
+ end
26
+
27
+ def to_s
28
+ "Function '#{@function_name}' was referenced but does not exist"
29
+ end
30
+ end
31
+
32
+ class UnableToModifyConstant < StandardError
33
+ def initialize(constant_name)
34
+ @constant_name = constant_name
35
+ end
36
+
37
+ def to_s
38
+ "Unable to modify value of constant '#{@constant_name}'"
39
+ end
40
+ end
41
+ end
data/lib/lexer.rb ADDED
@@ -0,0 +1,20 @@
1
+ require 'rubygems'
2
+ require 'lexr'
3
+
4
+ class MathEngine
5
+ Lexer = Lexr.that {
6
+ ignores /\s/ => :whitespace
7
+ matches /[a-z][a-z0-9_]*/ => :identifier, :convert_with => lambda { |v| v.to_sym }
8
+ matches /[-+]?[0-9]*\.?[0-9]+/ => :number, :convert_with => lambda { |v| Float(v) }
9
+ matches ',' => :comma
10
+ matches '=' => :assignment
11
+ matches '+' => :addition
12
+ matches '-' => :subtraction
13
+ matches '*' => :multiplication
14
+ matches '/' => :division
15
+ matches '^' => :exponent
16
+ matches '%' => :modulus
17
+ matches '(' => :open_parenthesis
18
+ matches ')' => :close_parenthesis
19
+ }
20
+ end
data/lib/math_engine.rb CHANGED
@@ -1,19 +1,20 @@
1
- require 'rubygems'
2
- require 'lexr'
3
-
4
- require File.expand_path(File.join(File.dirname(__FILE__), 'math_lexer'))
5
- require File.expand_path(File.join(File.dirname(__FILE__), 'math_parser'))
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'errors'))
2
+ require File.expand_path(File.join(File.dirname(__FILE__), 'lexer'))
3
+ require File.expand_path(File.join(File.dirname(__FILE__), 'parser'))
6
4
 
7
5
  class MathEngine
8
- def initialize
6
+ def initialize()
9
7
  @variables = {}
8
+ @dyn_library = Class.new.new
9
+ @libraries = [@dyn_library, Math]
10
10
  end
11
11
 
12
12
  def evaluate(expression)
13
- MathParser.new(MathLexer.new(expression)).parse.evaluate(self)
13
+ Parser.new(Lexer.new(expression)).parse.evaluate(self)
14
14
  end
15
15
 
16
16
  def set(variable_name, value)
17
+ raise UnableToModifyConstant.new(variable_name) if @variables.keys.include? variable_name and variable_name.to_s == variable_name.to_s.upcase
17
18
  @variables[variable_name] = value
18
19
  end
19
20
 
@@ -23,16 +24,32 @@ class MathEngine
23
24
  end
24
25
 
25
26
  def variables
26
- @variables.keys
27
+ @variables.keys.collect { |k| k.to_s }.reject { |v| v.downcase != v }.sort.collect { |k| k.to_sym }
27
28
  end
28
29
 
29
- class UnknownVariableError < StandardError
30
- def initialize(variable_name)
31
- @variable_name = variable_name
32
- end
33
-
34
- def to_s
35
- "Variable '#{@variable_name}' was referenced but does not exist"
30
+ def constants
31
+ @variables.keys.collect { |k| k.to_s }.reject { |v| v.upcase != v }.sort.collect { |k| k.to_sym }
32
+ end
33
+
34
+ def call(name, *parameters)
35
+ cls = class_for_function(name)
36
+ raise UnknownFunctionError.new(name) unless cls
37
+ cls.send name, *parameters
38
+ end
39
+
40
+ def define(name, func = nil, &block)
41
+ @dyn_library.class.send :define_method, name.to_sym do |*args|
42
+ func ? func.call(*args) : block.call(*args)
36
43
  end
37
44
  end
45
+
46
+ def include_library(library)
47
+ @libraries << library
48
+ end
49
+
50
+ private
51
+
52
+ def class_for_function(name)
53
+ @libraries.detect { |l| l.methods.include? name.to_s }
54
+ end
38
55
  end
data/lib/nodes.rb ADDED
@@ -0,0 +1,85 @@
1
+ class MathEngine
2
+ class Node
3
+ attr_reader :left, :right
4
+ alias :value :left
5
+
6
+ def initialize(left, right = nil)
7
+ @left, @right = left, right
8
+ end
9
+ end
10
+
11
+ class LiteralNumberNode < Node
12
+ def evaluate(engine)
13
+ value
14
+ end
15
+ end
16
+
17
+ class ExpressionNode < Node
18
+ def evaluate(engine)
19
+ left.evaluate(engine)
20
+ end
21
+ end
22
+
23
+ class IdentifierNode < Node
24
+ def evaluate(engine)
25
+ engine.get value
26
+ end
27
+ end
28
+
29
+ class AssignmentNode < Node
30
+ def evaluate(engine)
31
+ result = right.evaluate(engine)
32
+ engine.set(left.value, result)
33
+ result
34
+ end
35
+ end
36
+
37
+ class AdditionNode < Node
38
+ def evaluate(engine)
39
+ left.evaluate(engine) + right.evaluate(engine)
40
+ end
41
+ end
42
+
43
+ class SubtractionNode < Node
44
+ def evaluate(engine)
45
+ left.evaluate(engine) - right.evaluate(engine)
46
+ end
47
+ end
48
+ class MultiplicationNode < Node
49
+ def evaluate(engine)
50
+ left.evaluate(engine) * right.evaluate(engine)
51
+ end
52
+ end
53
+ class DivisionNode < Node
54
+ def evaluate(engine)
55
+ left.evaluate(engine) / right.evaluate(engine)
56
+ end
57
+ end
58
+ class ExponentNode < Node
59
+ def evaluate(engine)
60
+ left.evaluate(engine) ** right.evaluate(engine)
61
+ end
62
+ end
63
+ class ModulusNode < Node
64
+ def evaluate(engine)
65
+ left.evaluate(engine) % right.evaluate(engine)
66
+ end
67
+ end
68
+
69
+ class FunctionCallNode < Node
70
+ def evaluate(engine)
71
+ parameters = right ? right.to_a.collect { |p| p.evaluate(engine) } : []
72
+ engine.call(left, *parameters)
73
+ end
74
+ end
75
+
76
+ class ParametersNode < Node
77
+ def evaluate(engine)
78
+ left.evaluate(engine)
79
+ end
80
+
81
+ def to_a
82
+ [left] + (right ? right.to_a : [])
83
+ end
84
+ end
85
+ end
data/lib/parser.rb ADDED
@@ -0,0 +1,128 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'nodes'))
2
+
3
+ class MathEngine
4
+ class Parser
5
+ def initialize(lexer)
6
+ @lexer = lexer
7
+ end
8
+
9
+ def parse
10
+ #statement = { <identifier> <assignment> } expression <end>
11
+ #expression = term { ( <addition> | <subtraction> ) term }
12
+ #term = exp { ( <multiplication> | <division> ) exp }
13
+ #exp = factor { ( <exponent> | <modulus> ) factor }
14
+ #factor = <call> | <identifier> | <number> | ( <open_parenthesis> expression <close_parenthesis> )
15
+ #call = <identifier> <open_parenthesis> { call_parameter } <close_parenthesis>
16
+ #call_parameter = <expression> { <comma> call_parameter }
17
+ statement
18
+ end
19
+
20
+ private
21
+
22
+ def statement
23
+ next!
24
+ if current.type == :identifier && peek.type == :assignment
25
+ variable_name = current.value
26
+ next!
27
+ expect_current :assignment
28
+ next!
29
+ result = MathEngine::AssignmentNode.new(MathEngine::IdentifierNode.new(variable_name), expression)
30
+ else
31
+ result = expression
32
+ end
33
+ next!
34
+ expect_current :end
35
+ result
36
+ end
37
+
38
+ def expression
39
+ left = term
40
+ result = nil
41
+ while [:addition, :subtraction].include? current.type
42
+ node_type = current.type == :addition ? MathEngine::AdditionNode : MathEngine::SubtractionNode
43
+ next!
44
+ left = node_type.new(left, term)
45
+ end
46
+ result = MathEngine::ExpressionNode.new(result || left)
47
+ result
48
+ end
49
+
50
+ def term
51
+ left = exp
52
+ result = nil
53
+ while [:multiplication, :division].include? current.type
54
+ node_type = current.type == :multiplication ? MathEngine::MultiplicationNode : MathEngine::DivisionNode
55
+ next!
56
+ left = node_type.new(left, exp)
57
+ end
58
+ result || left
59
+ end
60
+
61
+ def exp
62
+ left = factor
63
+ result = nil
64
+ while [:exponent, :modulus].include? current.type
65
+ node_type = current.type == :exponent ? MathEngine::ExponentNode : MathEngine::ModulusNode
66
+ next!
67
+ left = node_type.new(left, factor)
68
+ end
69
+ result || left
70
+ end
71
+
72
+ def factor
73
+ if current.type == :number
74
+ result = MathEngine::LiteralNumberNode.new(current.value)
75
+ next!
76
+ return result
77
+ elsif current.type == :identifier
78
+ result = peek.type == :open_parenthesis ? call : MathEngine::IdentifierNode.new(current.value)
79
+ next!
80
+ return result
81
+ end
82
+
83
+ expect_current :open_parenthesis, "number, variable or open_parenthesis"
84
+ next!
85
+ result = expression
86
+ expect_current :close_parenthesis
87
+ next!
88
+ result
89
+ end
90
+
91
+ def call
92
+ expect_current :identifier
93
+ function_name = current.value
94
+ next!
95
+ expect_current :open_parenthesis
96
+ next!
97
+ result = MathEngine::FunctionCallNode.new(function_name, current.type == :close_parenthesis ? nil : call_parameter)
98
+ expect_current :close_parenthesis
99
+ result
100
+ end
101
+
102
+ def call_parameter
103
+ left = expression
104
+ right = nil
105
+ if current.type == :comma
106
+ next!
107
+ right = call_parameter
108
+ end
109
+ MathEngine::ParametersNode.new(left, right)
110
+ end
111
+
112
+ def current
113
+ @lexer.current
114
+ end
115
+
116
+ def peek
117
+ @lexer.peek
118
+ end
119
+
120
+ def next!
121
+ @lexer.next
122
+ end
123
+
124
+ def expect_current(type, friendly = nil)
125
+ raise MathEngine::ParseError.new("Unexpected #{current}, expected: #{friendly ? friendly : type}") unless current.type == type
126
+ end
127
+ end
128
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: math_engine
3
3
  version: !ruby/object:Gem::Version
4
- hash: 29
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
9
- - 3
10
- version: 0.1.3
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Michael Baldry
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2010-11-24 00:00:00 +00:00
18
+ date: 2010-12-02 00:00:00 +00:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -58,9 +58,11 @@ extra_rdoc_files:
58
58
  - README.md
59
59
  files:
60
60
  - README.md
61
+ - lib/errors.rb
62
+ - lib/lexer.rb
61
63
  - lib/math_engine.rb
62
- - lib/math_lexer.rb
63
- - lib/math_parser.rb
64
+ - lib/nodes.rb
65
+ - lib/parser.rb
64
66
  has_rdoc: true
65
67
  homepage: http://www.forwardtechnology.co.uk
66
68
  licenses: []
@@ -95,6 +97,6 @@ rubyforge_project:
95
97
  rubygems_version: 1.3.7
96
98
  signing_key:
97
99
  specification_version: 3
98
- summary: Evaluates simple mathematical expressions
100
+ summary: Evaluates mathematical expressions
99
101
  test_files: []
100
102
 
data/lib/math_lexer.rb DELETED
@@ -1,12 +0,0 @@
1
- MathLexer = Lexr.that {
2
- ignores /\s/ => :whitespace
3
- matches /[a-z][a-z0-9]*/ => :identifier, :convert_with => lambda { |v| v.to_sym }
4
- matches /[-+]?[0-9]*\.?[0-9]+/ => :number, :convert_with => lambda { |v| Float(v) }
5
- matches '=' => :assignment
6
- matches '+' => :addition
7
- matches '-' => :subtraction
8
- matches '*' => :multiplication
9
- matches '/' => :division
10
- matches '(' => :open_parenthesis
11
- matches ')' => :close_parenthesis
12
- }
data/lib/math_parser.rb DELETED
@@ -1,158 +0,0 @@
1
- class MathParser
2
- def initialize(lexer)
3
- @lexer = lexer
4
- end
5
-
6
- def parse
7
- #statement = { <identifier> <assignment> } expression <end>
8
- #expression = term { ( <addition> | <subtraction> ) term }
9
- #term = factor { ( <multiplication> | <division> ) factor }
10
- #factor = <identifier> | <number> | <open_parenthesis> expression <close_parenthesis>
11
- statement
12
- end
13
-
14
- private
15
-
16
- def statement
17
- next!
18
- if current.type == :identifier && peek.type == :assignment
19
- variable_name = current.value
20
- next!
21
- expect_current :assignment
22
- next!
23
- result = AssignmentNode.new(IdentifierNode.new(variable_name), expression)
24
- else
25
- result = expression
26
- end
27
- next!
28
- expect_current :end
29
- result
30
- end
31
-
32
- def expression
33
- left = term
34
- result = nil
35
- while [:addition, :subtraction].include? current.type
36
- node_type = current.type == :addition ? AdditionNode : SubtractionNode
37
- next!
38
- left = node_type.new(left, term)
39
- end
40
- ExpressionNode.new(result || left)
41
- end
42
-
43
- def term
44
- left = factor
45
- result = nil
46
- while [:multiplication, :division].include? current.type
47
- node_type = current.type == :multiplication ? MultiplicationNode : DivisionNode
48
- next!
49
- left = node_type.new(left, factor)
50
- end
51
- result || left
52
- end
53
-
54
- def factor
55
- if [:number, :identifier].include? current.type
56
- node_type = current.type == :number ? LiteralNumberNode : IdentifierNode
57
- result = node_type.new(current.value)
58
- next!
59
- return result
60
- end
61
-
62
- expect_current :open_parenthesis, "number, variable or open_parenthesis"
63
- next!
64
- result = expression
65
- expect_current :close_parenthesis
66
- next!
67
- result
68
- end
69
-
70
- private
71
-
72
- def current
73
- @lexer.current
74
- end
75
-
76
- def peek
77
- @lexer.peek
78
- end
79
-
80
- def next!
81
- @lexer.next
82
- end
83
-
84
- def expect_current(type, friendly = nil)
85
- raise ParseError.new("Unexpected #{current}, expected: #{friendly ? friendly : type}") unless current.type == type
86
- end
87
-
88
- class Node
89
- attr_reader :left, :right
90
- alias :value :left
91
-
92
- def initialize(left, right = nil)
93
- @left, @right = left, right
94
- end
95
-
96
- def evaluate(engine)
97
- raise "Evaluate not overridden in #{self.class.name}"
98
- end
99
- end
100
-
101
- class LiteralNumberNode < Node
102
- def evaluate(engine)
103
- value
104
- end
105
- end
106
-
107
- class ExpressionNode < Node
108
- def evaluate(engine)
109
- left.evaluate(engine)
110
- end
111
- end
112
-
113
- class IdentifierNode < Node
114
- def evaluate(engine)
115
- engine.get value
116
- end
117
- end
118
-
119
- class AssignmentNode < Node
120
- def evaluate(engine)
121
- result = right.evaluate(engine)
122
- engine.set(left.value, result)
123
- result
124
- end
125
- end
126
-
127
- class AdditionNode < Node
128
- def evaluate(engine)
129
- left.evaluate(engine) + right.evaluate(engine)
130
- end
131
- end
132
-
133
- class SubtractionNode < Node
134
- def evaluate(engine)
135
- left.evaluate(engine) - right.evaluate(engine)
136
- end
137
- end
138
- class MultiplicationNode < Node
139
- def evaluate(engine)
140
- left.evaluate(engine) * right.evaluate(engine)
141
- end
142
- end
143
- class DivisionNode < Node
144
- def evaluate(engine)
145
- left.evaluate(engine) / right.evaluate(engine)
146
- end
147
- end
148
-
149
- class ParseError < StandardError
150
- def initialize(message)
151
- @message = message
152
- end
153
-
154
- def to_s
155
- @message
156
- end
157
- end
158
- end