dentaku 1.2.6 → 2.0.0

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.
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