dentaku 1.2.6 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +52 -57
  3. data/Rakefile +1 -1
  4. data/lib/dentaku.rb +8 -0
  5. data/lib/dentaku/ast.rb +22 -0
  6. data/lib/dentaku/ast/addition.rb +15 -0
  7. data/lib/dentaku/ast/combinators.rb +15 -0
  8. data/lib/dentaku/ast/comparators.rb +47 -0
  9. data/lib/dentaku/ast/division.rb +15 -0
  10. data/lib/dentaku/ast/exponentiation.rb +15 -0
  11. data/lib/dentaku/ast/function.rb +54 -0
  12. data/lib/dentaku/ast/functions/if.rb +26 -0
  13. data/lib/dentaku/ast/functions/max.rb +5 -0
  14. data/lib/dentaku/ast/functions/min.rb +5 -0
  15. data/lib/dentaku/ast/functions/not.rb +5 -0
  16. data/lib/dentaku/ast/functions/round.rb +5 -0
  17. data/lib/dentaku/ast/functions/rounddown.rb +5 -0
  18. data/lib/dentaku/ast/functions/roundup.rb +5 -0
  19. data/lib/dentaku/ast/functions/ruby_math.rb +8 -0
  20. data/lib/dentaku/ast/grouping.rb +13 -0
  21. data/lib/dentaku/ast/identifier.rb +29 -0
  22. data/lib/dentaku/ast/multiplication.rb +15 -0
  23. data/lib/dentaku/ast/negation.rb +25 -0
  24. data/lib/dentaku/ast/nil.rb +9 -0
  25. data/lib/dentaku/ast/node.rb +13 -0
  26. data/lib/dentaku/ast/numeric.rb +17 -0
  27. data/lib/dentaku/ast/operation.rb +20 -0
  28. data/lib/dentaku/ast/string.rb +17 -0
  29. data/lib/dentaku/ast/subtraction.rb +15 -0
  30. data/lib/dentaku/bulk_expression_solver.rb +6 -11
  31. data/lib/dentaku/calculator.rb +26 -20
  32. data/lib/dentaku/parser.rb +131 -0
  33. data/lib/dentaku/token.rb +4 -0
  34. data/lib/dentaku/token_matchers.rb +29 -0
  35. data/lib/dentaku/token_scanner.rb +18 -3
  36. data/lib/dentaku/tokenizer.rb +10 -2
  37. data/lib/dentaku/version.rb +1 -1
  38. data/spec/ast/function_spec.rb +19 -0
  39. data/spec/ast/node_spec.rb +37 -0
  40. data/spec/bulk_expression_solver_spec.rb +12 -5
  41. data/spec/calculator_spec.rb +14 -1
  42. data/spec/external_function_spec.rb +12 -28
  43. data/spec/parser_spec.rb +88 -0
  44. data/spec/spec_helper.rb +2 -1
  45. data/spec/token_scanner_spec.rb +4 -3
  46. data/spec/tokenizer_spec.rb +32 -6
  47. metadata +36 -16
  48. data/lib/dentaku/binary_operation.rb +0 -35
  49. data/lib/dentaku/evaluator.rb +0 -166
  50. data/lib/dentaku/expression.rb +0 -56
  51. data/lib/dentaku/external_function.rb +0 -10
  52. data/lib/dentaku/rule_set.rb +0 -153
  53. data/spec/binary_operation_spec.rb +0 -45
  54. data/spec/evaluator_spec.rb +0 -145
  55. data/spec/expression_spec.rb +0 -25
  56. data/spec/rule_set_spec.rb +0 -43
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:max, ->(*args) {
4
+ args.max
5
+ })
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:min, ->(*args) {
4
+ args.min
5
+ })
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:not, ->(logical) {
4
+ ! logical
5
+ })
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:round, ->(numeric, places=nil) {
4
+ numeric.round(places || 0)
5
+ })
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:rounddown, ->(numeric) {
4
+ numeric.floor
5
+ })
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:roundup, ->(numeric) {
4
+ numeric.ceil
5
+ })
@@ -0,0 +1,8 @@
1
+ # import all functions from Ruby's Math module
2
+ require_relative "../function"
3
+
4
+ Math.methods(false).each do |method|
5
+ Dentaku::AST::Function.register(method, ->(*args) {
6
+ Math.send(method, *args)
7
+ })
8
+ end
@@ -0,0 +1,13 @@
1
+ module Dentaku
2
+ module AST
3
+ class Grouping
4
+ def initialize(node)
5
+ @node = node
6
+ end
7
+
8
+ def value(context={})
9
+ @node.value(context)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ require_relative '../exceptions'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Identifier < Node
6
+ attr_reader :identifier
7
+
8
+ def initialize(token)
9
+ @identifier = token.value.downcase
10
+ end
11
+
12
+ def value(context={})
13
+ v = context[identifier]
14
+ case v
15
+ when Node
16
+ v.value
17
+ when NilClass
18
+ raise UnboundVariableError.new([identifier])
19
+ else
20
+ v
21
+ end
22
+ end
23
+
24
+ def dependencies(context={})
25
+ context.has_key?(identifier) ? [] : [identifier]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,15 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Multiplication < Operation
6
+ def value(context={})
7
+ left.value(context) * right.value(context)
8
+ end
9
+
10
+ def self.precedence
11
+ 20
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ module Dentaku
2
+ module AST
3
+ class Negation < Operation
4
+ def initialize(node)
5
+ @node = node
6
+ end
7
+
8
+ def value(context={})
9
+ @node.value(context) * -1
10
+ end
11
+
12
+ def self.arity
13
+ 1
14
+ end
15
+
16
+ def self.right_associative?
17
+ true
18
+ end
19
+
20
+ def self.precedence
21
+ 40
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module Dentaku
2
+ module AST
3
+ class Nil < Node
4
+ def value(*)
5
+ nil
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,13 @@
1
+ module Dentaku
2
+ module AST
3
+ class Node
4
+ def self.precedence
5
+ 0
6
+ end
7
+
8
+ def self.arity
9
+ nil
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ module Dentaku
2
+ module AST
3
+ class Numeric < Node
4
+ def initialize(token)
5
+ @value = token.value
6
+ end
7
+
8
+ def value(*)
9
+ @value
10
+ end
11
+
12
+ def dependencies(*)
13
+ []
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,20 @@
1
+ module Dentaku
2
+ module AST
3
+ class Operation < Node
4
+ attr_reader :left, :right
5
+
6
+ def initialize(left, right)
7
+ @left = left
8
+ @right = right
9
+ end
10
+
11
+ def dependencies(context={})
12
+ (left.dependencies(context) + right.dependencies(context)).uniq
13
+ end
14
+
15
+ def self.right_associative?
16
+ false
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,17 @@
1
+ module Dentaku
2
+ module AST
3
+ class String < Node
4
+ def initialize(token)
5
+ @value = token.value
6
+ end
7
+
8
+ def value(*)
9
+ @value
10
+ end
11
+
12
+ def dependencies(*)
13
+ []
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ require_relative './operation'
2
+
3
+ module Dentaku
4
+ module AST
5
+ class Subtraction < Operation
6
+ def value(context={})
7
+ left.value(context) - right.value(context)
8
+ end
9
+
10
+ def self.precedence
11
+ 10
12
+ end
13
+ end
14
+ end
15
+ end
@@ -1,13 +1,14 @@
1
1
  require 'dentaku/calculator'
2
2
  require 'dentaku/dependency_resolver'
3
3
  require 'dentaku/exceptions'
4
- require 'dentaku/expression'
4
+ require 'dentaku/parser'
5
+ require 'dentaku/tokenizer'
5
6
 
6
7
  module Dentaku
7
8
  class BulkExpressionSolver
8
9
  def initialize(expression_hash, memory)
9
10
  self.expression_hash = expression_hash
10
- self.memory = memory
11
+ self.calculator = Calculator.new.store(memory)
11
12
  end
12
13
 
13
14
  def solve!
@@ -25,7 +26,7 @@ module Dentaku
25
26
 
26
27
  private
27
28
 
28
- attr_accessor :expression_hash, :memory
29
+ attr_accessor :expression_hash, :calculator
29
30
 
30
31
  def return_undefined_handler
31
32
  ->(*) { :undefined }
@@ -38,7 +39,7 @@ module Dentaku
38
39
  def load_results(&block)
39
40
  variables_in_resolve_order.each_with_object({}) do |var_name, r|
40
41
  begin
41
- r[var_name] = evaluate!(expressions[var_name], r)
42
+ r[var_name] = calculator.memory[var_name] || evaluate!(expressions[var_name], r)
42
43
  rescue Dentaku::UnboundVariableError, ZeroDivisionError => ex
43
44
  r[var_name] = block.call(ex)
44
45
  end
@@ -46,7 +47,7 @@ module Dentaku
46
47
  end
47
48
 
48
49
  def dependencies(expression)
49
- Expression.new(expression, memory).identifiers
50
+ Parser.new(Tokenizer.new.tokenize(expression)).parse.dependencies
50
51
  end
51
52
 
52
53
  def expressions
@@ -63,13 +64,7 @@ module Dentaku
63
64
  end
64
65
 
65
66
  def evaluate!(expression, results)
66
- expr = Expression.new(expression, memory.merge(expressions))
67
- raise UnboundVariableError.new(expr.identifiers) if expr.unbound?
68
67
  calculator.evaluate!(expression, results)
69
68
  end
70
-
71
- def calculator
72
- @calculator ||= Calculator.new.store(memory)
73
- end
74
69
  end
75
70
  end
@@ -1,27 +1,26 @@
1
1
  require 'dentaku/bulk_expression_solver'
2
- require 'dentaku/evaluator'
3
2
  require 'dentaku/exceptions'
4
- require 'dentaku/expression'
5
- require 'dentaku/rule_set'
6
3
  require 'dentaku/token'
7
4
  require 'dentaku/dependency_resolver'
5
+ require 'dentaku/parser'
8
6
 
9
7
  module Dentaku
10
8
  class Calculator
11
- attr_reader :result, :rule_set
9
+ attr_reader :result, :memory, :tokenizer
12
10
 
13
11
  def initialize
14
12
  clear
15
- @rule_set = RuleSet.new
13
+ @tokenizer = Tokenizer.new
14
+ @ast_cache = {}
16
15
  end
17
16
 
18
- def add_function(fn)
19
- rule_set.add_function(fn)
17
+ def add_function(name, body)
18
+ Dentaku::AST::Function.register(name, body)
20
19
  self
21
20
  end
22
21
 
23
22
  def add_functions(fns)
24
- fns.each { |fn| add_function(fn) }
23
+ fns.each { |(name, body)| add_function(name, body) }
25
24
  self
26
25
  end
27
26
 
@@ -32,35 +31,42 @@ module Dentaku
32
31
  end
33
32
 
34
33
  def evaluate!(expression, data={})
35
- store(data) do
36
- expr = Expression.new(expression, @memory)
37
- raise UnboundVariableError.new(expr.identifiers) if expr.unbound?
38
- @evaluator ||= Evaluator.new(rule_set)
39
- @result = @evaluator.evaluate(expr.tokens)
34
+ memory[expression] || store(data) do
35
+ node = expression
36
+ node = ast(node) unless node.is_a?(AST::Node)
37
+ node.value(memory)
40
38
  end
41
39
  end
42
40
 
43
41
  def solve!(expression_hash)
44
- BulkExpressionSolver.new(expression_hash, @memory).solve!
42
+ BulkExpressionSolver.new(expression_hash, memory).solve!
45
43
  end
46
44
 
47
45
  def solve(expression_hash, &block)
48
- BulkExpressionSolver.new(expression_hash, @memory).solve(&block)
46
+ BulkExpressionSolver.new(expression_hash, memory).solve(&block)
49
47
  end
50
48
 
51
49
  def dependencies(expression)
52
- Expression.new(expression, @memory).identifiers
50
+ ast(expression).dependencies(memory)
51
+ end
52
+
53
+ def ast(expression)
54
+ @ast_cache.fetch(expression) {
55
+ Parser.new(tokenizer.tokenize(expression)).parse.tap do |node|
56
+ @ast_cache[expression] = node if Dentaku.cache_ast?
57
+ end
58
+ }
53
59
  end
54
60
 
55
61
  def store(key_or_hash, value=nil)
56
- restore = @memory.dup
62
+ restore = memory.dup
57
63
 
58
64
  if value.nil?
59
65
  key_or_hash.each do |key, val|
60
- @memory[key.downcase.to_s] = val
66
+ memory[key.downcase.to_s] = val
61
67
  end
62
68
  else
63
- @memory[key_or_hash.to_s] = value
69
+ memory[key_or_hash.to_s] = value
64
70
  end
65
71
 
66
72
  if block_given?
@@ -78,7 +84,7 @@ module Dentaku
78
84
  end
79
85
 
80
86
  def empty?
81
- @memory.empty?
87
+ memory.empty?
82
88
  end
83
89
  end
84
90
  end
@@ -0,0 +1,131 @@
1
+ require_relative './ast'
2
+
3
+ module Dentaku
4
+ class Parser
5
+ attr_reader :input, :output, :operations, :arities
6
+
7
+ def initialize(tokens)
8
+ @input = tokens.dup
9
+ @output = []
10
+ @operations = []
11
+ @arities = []
12
+ end
13
+
14
+ def get_args(count)
15
+ Array.new(count) { output.pop }.reverse
16
+ end
17
+
18
+ def consume(count=2)
19
+ operator = operations.pop
20
+ output.push operator.new(*get_args(operator.arity || count))
21
+ end
22
+
23
+ def parse
24
+ return AST::Nil.new if input.empty?
25
+
26
+ while token = input.shift
27
+ case token.category
28
+ when :numeric
29
+ output.push AST::Numeric.new(token)
30
+
31
+ when :string
32
+ output.push AST::String.new(token)
33
+
34
+ when :identifier
35
+ output.push AST::Identifier.new(token)
36
+
37
+ when :operator, :comparator, :combinator
38
+ op_class = operation(token)
39
+
40
+ if op_class.right_associative?
41
+ while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
42
+ consume
43
+ end
44
+
45
+ operations.push op_class
46
+ else
47
+ while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
48
+ consume
49
+ end
50
+
51
+ operations.push op_class
52
+ end
53
+
54
+ when :function
55
+ arities.push 0
56
+ operations.push function(token)
57
+
58
+ when :grouping
59
+ case token.value
60
+ when :open, :fopen
61
+ if input.first && input.first.value == :close
62
+ input.shift
63
+ consume(0)
64
+ else
65
+ operations.push AST::Grouping
66
+ end
67
+
68
+ when :close
69
+ while operations.any? && operations.last != AST::Grouping
70
+ consume
71
+ end
72
+
73
+ lparen = operations.pop
74
+ fail "Unbalanced parenthesis" unless lparen == AST::Grouping
75
+
76
+ if operations.last && operations.last < AST::Function
77
+ consume(arities.pop.succ)
78
+ end
79
+
80
+ when :comma
81
+ arities[-1] += 1
82
+ while operations.any? && operations.last != AST::Grouping
83
+ consume
84
+ end
85
+
86
+ else
87
+ fail "Unknown grouping token #{ token.value }"
88
+ end
89
+
90
+ else
91
+ fail "Not implemented for tokens of category #{ token.category }"
92
+ end
93
+ end
94
+
95
+ while operations.any?
96
+ consume
97
+ end
98
+
99
+ unless output.count == 1
100
+ fail "Parse error"
101
+ end
102
+
103
+ output.first
104
+ end
105
+
106
+ def operation(token)
107
+ {
108
+ add: AST::Addition,
109
+ subtract: AST::Subtraction,
110
+ multiply: AST::Multiplication,
111
+ divide: AST::Division,
112
+ pow: AST::Exponentiation,
113
+ negate: AST::Negation,
114
+
115
+ lt: AST::LessThan,
116
+ gt: AST::GreaterThan,
117
+ le: AST::LessThanOrEqual,
118
+ ge: AST::GreaterThanOrEqual,
119
+ ne: AST::NotEqual,
120
+ eq: AST::Equal,
121
+
122
+ and: AST::And,
123
+ or: AST::Or,
124
+ }.fetch(token.value)
125
+ end
126
+
127
+ def function(token)
128
+ Dentaku::AST::Function.get(token.value)
129
+ end
130
+ end
131
+ end