minad-evaluator 0.1 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/evaluator.rb +101 -123
  2. data/test/test_evaluator.rb +20 -2
  3. metadata +1 -1
data/lib/evaluator.rb CHANGED
@@ -1,23 +1,67 @@
1
1
  require 'complex'
2
2
 
3
3
  module Evaluator
4
+ def self.infix(priority, unary = nil, &block) [false, priority, lambda(&block), unary] end
5
+ def self.prefix(&block) [true, 1e5, lambda(&block)] end
4
6
 
5
- VERSION = "0.1"
6
- INTEGER = /\d+/
7
- REAL = /(?:\d*\.\d+(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+)/
8
- STRING = /'(?:[^']|\\')+'|"(?:[^"]|\\")+"/
9
- SYMBOL = /[\w_]+/
10
- FUNCTIONS = {
11
- 'sin' => 1, 'cos' => 1, 'tan' => 1, 'sinh' => 1, 'cosh' => 1, 'tanh' => 1, 'asin' => 1, 'acos' => 1, 'atan' => 1,
12
- 'asinh' => 1, 'atanh' => 1, 'sqrt' => 1, 'log' => 1, 'ln' => 1, 'log10' => 1, 'log2' => 1, 'exp' => 1,
13
- 'floor' => 1, 'ceil' => 1, 'string' => 1, 'int' => 1, 'float' => 1, 'rand' => 0, 'conj' => 1, 'im' => 1, 're' => 1, 'round' => 1,
14
- 'abs' => 1, 'minus' => 1, 'plus' => 1, 'not' => 1 }
15
- OPERATOR = [ %w(|| or), %w(&& and), %w(== != <= >= < >), %w(+ -),
16
- %w(<< >>), %w(& | ^), %w(* / % div mod), %w(**), %w(!) ]
17
- UNARY = {
18
- '+' => 'plus',
19
- '-' => 'minus',
20
- '!' => 'not'
7
+ VERSION = "0.1.1"
8
+ OPERATOR = {
9
+ '||' => infix(0) {|a,b| a || b },
10
+ 'or' => infix(0) {|a,b| a || b },
11
+ '&&' => infix(1) {|a,b| a && b },
12
+ 'and' => infix(1) {|a,b| a && b },
13
+ '==' => infix(2) {|a,b| a == b },
14
+ '!=' => infix(2) {|a,b| a != b },
15
+ '<=' => infix(2) {|a,b| a <= b },
16
+ '>=' => infix(2) {|a,b| a >= b },
17
+ '<' => infix(2) {|a,b| a < b },
18
+ '>' => infix(2) {|a,b| a > b },
19
+ '+' => infix(3, 'plus') {|a,b| a + b },
20
+ '-' => infix(3, 'minus') {|a,b| a - b },
21
+ '>>' => infix(4) {|a,b| a >> b },
22
+ '<<' => infix(4) {|a,b| a << b },
23
+ '&' => infix(5) {|a,b| a & b },
24
+ '|' => infix(5) {|a,b| a | b },
25
+ '^' => infix(5) {|a,b| a ^ b },
26
+ '*' => infix(6) {|a,b| a * b },
27
+ '/' => infix(6) {|a,b| a / b },
28
+ '%' => infix(6) {|a,b| a % b },
29
+ 'div' => infix(6) {|a,b| a.div b },
30
+ 'mod' => infix(6) {|a,b| a % b },
31
+ '**' => infix(7) {|a,b| a ** b },
32
+ 'sin' => prefix {|x| Math.sin(x) },
33
+ 'cos' => prefix {|x| Math.cos(x) },
34
+ 'tan' => prefix {|x| Math.tan(x) },
35
+ 'sinh' => prefix {|x| Math.sinh(x) },
36
+ 'cosh' => prefix {|x| Math.cosh(x) },
37
+ 'tanh' => prefix {|x| Math.tanh(x) },
38
+ 'asin' => prefix {|x| Math.asin(x) },
39
+ 'acos' => prefix {|x| Math.acos(x) },
40
+ 'atan' => prefix {|x| Math.atan(x) },
41
+ 'asinh' => prefix {|x| Math.asinh(x) },
42
+ 'atanh' => prefix {|x| Math.atanh(x) },
43
+ 'sqrt' => prefix {|x| Math.sqrt(x) },
44
+ 'log' => prefix {|x| Math.log(x) },
45
+ 'ln' => prefix {|x| Math.log(x) },
46
+ 'log10' => prefix {|x| Math.log10(x) },
47
+ 'log2' => prefix {|x| Math.log(x)/Math.log(2) },
48
+ 'exp' => prefix {|x| Math.exp(x) },
49
+ 'floor' => prefix {|x| x.floor },
50
+ 'ceil' => prefix {|x| x.ceil },
51
+ 'string' => prefix {|x| x.to_s },
52
+ 'int' => prefix {|x| x.to_i },
53
+ 'float' => prefix {|x| x.to_f },
54
+ 'rand' => prefix {|| rand },
55
+ 'conj' => prefix {|x| x.conj },
56
+ 'im' => prefix {|x| x.imag },
57
+ 're' => prefix {|x| x.real },
58
+ 'round' => prefix {|x| x.round },
59
+ 'abs' => prefix {|x| x.abs },
60
+ 'minus' => prefix {|x| -x },
61
+ 'plus' => prefix {|x| x },
62
+ '!' => prefix {|x| !x },
63
+ 'substr' => prefix {|x,a,b| x.slice(a,b) },
64
+ 'len' => prefix {|x| x.length }
21
65
  }
22
66
  CONSTANTS = {
23
67
  'true' => true,
@@ -27,131 +71,65 @@ module Evaluator
27
71
  'pi' => Math::PI,
28
72
  'i' => Complex::I
29
73
  }
30
-
31
- OP = {}
32
- (OPERATOR + [FUNCTIONS.keys]).each_with_index do |ops,i|
33
- ops.each { |op| OP[op] = i }
34
- end
35
-
36
- TOKENIZER = Regexp.new("#{REAL.source}|#{INTEGER.source}|#{STRING.source}|#{SYMBOL.source}|\\(|\\)|,|" +
37
- (OPERATOR + FUNCTIONS.keys).flatten.sort { |a,b| b.length <=> a.length}.map { |op| Regexp.quote(op) }.join('|'))
74
+ TOKENS = [
75
+ [ /^'(?:[^']|\\')+'|"(?:[^"]|\\")+"$/, lambda {|tok, vars| tok[1..-2] } ],
76
+ [ /^(?:\d*\.\d+(?:[eE][-+]?\d+)?|\d+[eE][-+]?\d+)$/, lambda {|tok, vars| tok.to_f } ],
77
+ [ /^0[xX][\dA-Fa-f]+$/, lambda {|tok, vars| tok.to_i(16) } ],
78
+ [ /^0[0-7]+$/, lambda {|tok, vars| tok.to_i(8) } ],
79
+ [ /^\d+$/, lambda {|tok, vars| tok.to_i(10) } ],
80
+ [ /^[a-zA-Z_][\w_]*$/, lambda {|tok, vars|
81
+ tok.downcase!
82
+ raise(NameError, "Symbol #{tok} is undefined") if !vars.include?(tok)
83
+ vars[tok]
84
+ }
85
+ ]
86
+ ]
87
+ TOKENIZER = Regexp.new((TOKENS.map {|x| x[0].source[1..-2] } +
88
+ OPERATOR.keys.flatten.sort { |a,b| b.length <=> a.length}.map { |op| Regexp.quote(op) }).
89
+ join('|') + '|\\(|\\)|,')
38
90
 
39
91
  def self.eval(expr, vars = {})
40
- table = CONSTANTS.dup
41
- vars.each_pair {|k,v| table[k.to_s.downcase] = v }
42
- tokens = expr.scan(TOKENIZER)
43
- stack, post = [], []
44
- prev = nil
45
- tokens.each do |tok|
92
+ vars = Hash[*vars.map {|k,v| [k.to_s.downcase, v] }.flatten].merge(CONSTANTS)
93
+ stack, result, unary = [], [], true
94
+ expr.scan(TOKENIZER).each do |tok|
46
95
  if tok == '('
47
96
  stack << '('
48
97
  elsif tok == ')'
49
- op(post, stack.pop) while !stack.empty? && stack.last != '('
98
+ exec(result, stack.pop) while !stack.empty? && stack.last != '('
50
99
  raise(SyntaxError, "Unexpected token )") if stack.empty?
51
100
  stack.pop
52
101
  elsif tok == ','
53
- op(post, stack.pop) while !stack.empty? && stack.last != '('
54
- elsif FUNCTIONS.include?(tok.downcase)
55
- stack << tok.downcase
56
- elsif OP.include?(tok)
57
- if (prev == nil || OP.include?(prev)) && UNARY.include?(tok)
58
- stack << UNARY[tok]
102
+ exec(result, stack.pop) while !stack.empty? && stack.last != '('
103
+ elsif OPERATOR.include?(tok.downcase)
104
+ tok.downcase!
105
+ if OPERATOR[tok][0]
106
+ stack << tok
107
+ elsif unary && OPERATOR[tok][3]
108
+ stack << OPERATOR[tok][3]
59
109
  else
60
- op(post, stack.pop) while !stack.empty? && stack.last != '(' && OP[stack.last] >= OP[tok]
110
+ exec(result, stack.pop) while !stack.empty? && stack.last != '(' && OPERATOR[stack.last][1] >= OPERATOR[tok][1]
61
111
  stack << tok
62
112
  end
63
- elsif tok =~ STRING
64
- post << tok[1..-2]
65
- elsif tok =~ REAL
66
- post << tok.to_f
67
- elsif tok =~ INTEGER
68
- post << tok.to_i
69
113
  else
70
- tok.downcase!
71
- raise(NameError, "Symbol #{tok} is undefined") if !table.include?(tok)
72
- post << table[tok]
114
+ result << TOKENS.find {|x| tok =~ x[0] }[1][tok, vars]
115
+ unary = false
116
+ next
73
117
  end
74
- prev = tok
118
+ unary = true
75
119
  end
76
- op(post, stack.pop) while !stack.empty?
77
- post[0]
120
+ exec(result, stack.pop) while !stack.empty?
121
+ raise(SyntaxError, "Unexpected operands") if result.size != 1
122
+ result[0]
78
123
  end
79
124
 
80
- def self.op(stack, op)
81
- stack << \
82
- if FUNCTIONS.include?(op)
83
- args = FUNCTIONS[op]
84
- raise(SyntaxError, "Not enough operands on the stack") if stack.size < args
85
- a = stack.slice!(-args, args)
86
- case op
87
- when 'sin' then Math.sin(a[0])
88
- when 'cos' then Math.cos(a[0])
89
- when 'tan' then Math.tan(a[0])
90
- when 'sinh' then Math.sinh(a[0])
91
- when 'cosh' then Math.cosh(a[0])
92
- when 'tanh' then Math.tanh(a[0])
93
- when 'asin' then Math.asin(a[0])
94
- when 'acos' then Math.acos(a[0])
95
- when 'atan' then Math.atan(a[0])
96
- when 'asinh' then Math.asinh(a[0])
97
- when 'atanh' then Math.atanh(a[0])
98
- when 'sqrt' then Math.sqrt(a[0])
99
- when 'log' then Math.log(a[0])
100
- when 'ln' then Math.log(a[0])
101
- when 'log10' then Math.log10(a[0])
102
- when 'log2' then Math.log(a[0])/Math.log(2)
103
- when 'exp' then Math.exp(a[0])
104
- when 'floor' then a[0].floor
105
- when 'ceil' then a[0].ceil
106
- when 'string' then a[0].to_s
107
- when 'float' then a[0].to_f
108
- when 'int' then a[0].to_i
109
- when 'rand' then rand
110
- when 'conj' then a[0].conj
111
- when 'im' then a[0].imag
112
- when 're' then a[0].real
113
- when 'round' then a[0].round
114
- when 'abs' then a[0].abs
115
- when 'plus' then a[0]
116
- when 'minus' then -a[0]
117
- when 'not' then !a[0]
118
- end
119
- else
120
- raise(SyntaxError, "Not enough operands on the stack") if stack.size < 2
121
- b = stack.pop
122
- a = stack.pop
123
- case op
124
- when '||' then a || b
125
- when 'or' then a || b
126
- when '&&' then a && b
127
- when 'and' then a && b
128
- when '==' then a == b
129
- when '!=' then a != b
130
- when '<=' then a <= b
131
- when '>=' then a >= b
132
- when '<' then a < b
133
- when '>' then a > b
134
- when '+' then a + b
135
- when '-' then a - b
136
- when '*' then a * b
137
- when '/' then a / b
138
- when 'div' then a.div(b)
139
- when '%' then a % b
140
- when 'mod' then a % b
141
- when '**' then a ** b
142
- when '<<' then a << b
143
- when '>>' then a >> b
144
- when '&' then a & b
145
- when '|' then a | b
146
- when '^' then a ^ b
147
- else
148
- raise(SyntaxError, "Unexpected token #{op}")
149
- end
150
- end
125
+ def self.exec(result, op)
126
+ raise(SyntaxError, "Unexpected token #{op}") if !OPERATOR.include?(op)
127
+ op = OPERATOR[op][2]
128
+ raise(SyntaxError, "Not enough operands for #{op}") if result.size < op.arity
129
+ result << op[*result.slice!(-op.arity, op.arity)]
151
130
  end
152
131
 
153
- private_class_method :op
154
-
132
+ private_class_method :infix, :prefix, :exec
155
133
  end
156
134
 
157
135
  def Evaluator(expr, vars = {})
@@ -111,8 +111,10 @@ class TestEvaluator < Test::Unit::TestCase
111
111
  assert_equal 4, Evaluator('round(3.5)')
112
112
  assert_equal 6, Evaluator('abs -6')
113
113
  assert_equal 3, Evaluator('plus 3')
114
- assert_equal -3, Evaluator('minus 3')
115
- assert_equal false, Evaluator('not 3')
114
+ assert_equal(-3, Evaluator('minus 3'))
115
+ assert_equal false, Evaluator('!3')
116
+ assert_equal 'bcd', Evaluator('substr("abcde", 1, 3)')
117
+ assert_equal 4, Evaluator('len("abcd")')
116
118
  end
117
119
 
118
120
  def test_variables
@@ -120,4 +122,20 @@ class TestEvaluator < Test::Unit::TestCase
120
122
  assert_equal 14, Evaluator('a+b*C', 'A' => 2, 'b' => 3, :c => 4)
121
123
  assert_equal 14, Evaluator('alpha+beta*GAMMA', 'ALPHA' => 2, 'bEtA' => 3, 'gamma' => 4)
122
124
  end
125
+
126
+ def test_errors
127
+ assert_raise(SyntaxError) { Evaluator('(((((((((((((3+3))') }
128
+ assert_raise(SyntaxError) { Evaluator('1+2)') }
129
+ assert_raise(SyntaxError) { Evaluator('1+2+3+') }
130
+ assert_raise(SyntaxError) { Evaluator('1 + floor') }
131
+ assert_raise(NameError) { Evaluator('42*a+3') }
132
+ assert_raise(NameError) { Evaluator('abc10') }
133
+ assert_raise(NameError) { Evaluator('abc10d') }
134
+ end
135
+
136
+ def test_numbers
137
+ assert_equal 0xABCDEF123, Evaluator('0xABCDEF123')
138
+ assert_equal 01234, Evaluator('01234')
139
+ assert_equal 234, Evaluator('234 ')
140
+ end
123
141
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: minad-evaluator
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.1"
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Mendler