dentaku 3.2.0 → 3.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (100) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +4 -6
  4. data/CHANGELOG.md +86 -2
  5. data/README.md +7 -6
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +21 -1
  8. data/lib/dentaku/ast/arithmetic.rb +51 -15
  9. data/lib/dentaku/ast/array.rb +41 -0
  10. data/lib/dentaku/ast/bitwise.rb +30 -5
  11. data/lib/dentaku/ast/case/case_conditional.rb +17 -2
  12. data/lib/dentaku/ast/case/case_else.rb +17 -3
  13. data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
  14. data/lib/dentaku/ast/case/case_then.rb +17 -3
  15. data/lib/dentaku/ast/case/case_when.rb +21 -3
  16. data/lib/dentaku/ast/case.rb +19 -3
  17. data/lib/dentaku/ast/comparators.rb +38 -28
  18. data/lib/dentaku/ast/function.rb +11 -3
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +23 -0
  21. data/lib/dentaku/ast/functions/and.rb +2 -2
  22. data/lib/dentaku/ast/functions/any.rb +23 -0
  23. data/lib/dentaku/ast/functions/avg.rb +2 -2
  24. data/lib/dentaku/ast/functions/count.rb +8 -0
  25. data/lib/dentaku/ast/functions/duration.rb +51 -0
  26. data/lib/dentaku/ast/functions/enum.rb +37 -0
  27. data/lib/dentaku/ast/functions/filter.rb +23 -0
  28. data/lib/dentaku/ast/functions/if.rb +19 -2
  29. data/lib/dentaku/ast/functions/map.rb +23 -0
  30. data/lib/dentaku/ast/functions/or.rb +4 -4
  31. data/lib/dentaku/ast/functions/pluck.rb +30 -0
  32. data/lib/dentaku/ast/functions/round.rb +1 -1
  33. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  34. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  35. data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
  36. data/lib/dentaku/ast/functions/string_functions.rb +105 -12
  37. data/lib/dentaku/ast/functions/xor.rb +44 -0
  38. data/lib/dentaku/ast/grouping.rb +3 -1
  39. data/lib/dentaku/ast/identifier.rb +16 -4
  40. data/lib/dentaku/ast/literal.rb +10 -0
  41. data/lib/dentaku/ast/negation.rb +7 -1
  42. data/lib/dentaku/ast/nil.rb +4 -0
  43. data/lib/dentaku/ast/node.rb +8 -0
  44. data/lib/dentaku/ast/operation.rb +17 -0
  45. data/lib/dentaku/ast/string.rb +7 -0
  46. data/lib/dentaku/ast.rb +8 -0
  47. data/lib/dentaku/bulk_expression_solver.rb +38 -27
  48. data/lib/dentaku/calculator.rb +21 -8
  49. data/lib/dentaku/date_arithmetic.rb +45 -0
  50. data/lib/dentaku/exceptions.rb +11 -8
  51. data/lib/dentaku/flat_hash.rb +9 -2
  52. data/lib/dentaku/parser.rb +57 -16
  53. data/lib/dentaku/print_visitor.rb +101 -0
  54. data/lib/dentaku/token_matcher.rb +1 -1
  55. data/lib/dentaku/token_scanner.rb +9 -3
  56. data/lib/dentaku/tokenizer.rb +7 -2
  57. data/lib/dentaku/version.rb +1 -1
  58. data/lib/dentaku/visitor/infix.rb +82 -0
  59. data/lib/dentaku.rb +20 -7
  60. data/spec/ast/addition_spec.rb +7 -1
  61. data/spec/ast/all_spec.rb +25 -0
  62. data/spec/ast/and_function_spec.rb +6 -6
  63. data/spec/ast/and_spec.rb +1 -1
  64. data/spec/ast/any_spec.rb +23 -0
  65. data/spec/ast/arithmetic_spec.rb +64 -29
  66. data/spec/ast/avg_spec.rb +9 -5
  67. data/spec/ast/comparator_spec.rb +31 -1
  68. data/spec/ast/count_spec.rb +7 -7
  69. data/spec/ast/division_spec.rb +7 -1
  70. data/spec/ast/filter_spec.rb +25 -0
  71. data/spec/ast/function_spec.rb +20 -15
  72. data/spec/ast/map_spec.rb +27 -0
  73. data/spec/ast/max_spec.rb +16 -3
  74. data/spec/ast/min_spec.rb +16 -3
  75. data/spec/ast/mul_spec.rb +11 -6
  76. data/spec/ast/negation_spec.rb +48 -0
  77. data/spec/ast/node_spec.rb +11 -8
  78. data/spec/ast/numeric_spec.rb +1 -1
  79. data/spec/ast/or_spec.rb +7 -7
  80. data/spec/ast/pluck_spec.rb +32 -0
  81. data/spec/ast/round_spec.rb +14 -4
  82. data/spec/ast/rounddown_spec.rb +14 -4
  83. data/spec/ast/roundup_spec.rb +14 -4
  84. data/spec/ast/string_functions_spec.rb +73 -0
  85. data/spec/ast/sum_spec.rb +11 -6
  86. data/spec/ast/switch_spec.rb +5 -5
  87. data/spec/ast/xor_spec.rb +35 -0
  88. data/spec/bulk_expression_solver_spec.rb +37 -1
  89. data/spec/calculator_spec.rb +341 -32
  90. data/spec/dentaku_spec.rb +19 -6
  91. data/spec/external_function_spec.rb +32 -6
  92. data/spec/parser_spec.rb +100 -123
  93. data/spec/print_visitor_spec.rb +66 -0
  94. data/spec/spec_helper.rb +6 -4
  95. data/spec/token_matcher_spec.rb +8 -8
  96. data/spec/token_scanner_spec.rb +4 -4
  97. data/spec/tokenizer_spec.rb +56 -13
  98. data/spec/visitor/infix_spec.rb +31 -0
  99. data/spec/visitor_spec.rb +138 -0
  100. metadata +52 -7
@@ -1,7 +1,9 @@
1
1
  module Dentaku
2
- class UnboundVariableError < StandardError
2
+ class Error < StandardError
3
3
  attr_accessor :recipient_variable
4
+ end
4
5
 
6
+ class UnboundVariableError < Error
5
7
  attr_reader :unbound_variables
6
8
 
7
9
  def initialize(unbound_variables)
@@ -9,7 +11,7 @@ module Dentaku
9
11
  end
10
12
  end
11
13
 
12
- class NodeError < StandardError
14
+ class NodeError < Error
13
15
  attr_reader :child, :expect, :actual
14
16
 
15
17
  def initialize(expect, actual, child)
@@ -19,7 +21,7 @@ module Dentaku
19
21
  end
20
22
  end
21
23
 
22
- class ParseError < StandardError
24
+ class ParseError < Error
23
25
  attr_reader :reason, :meta
24
26
 
25
27
  def initialize(reason, **meta)
@@ -30,7 +32,7 @@ module Dentaku
30
32
  private_class_method :new
31
33
 
32
34
  VALID_REASONS = %i[
33
- node_invalid too_few_operands undefined_function
35
+ node_invalid too_few_operands too_many_operands undefined_function
34
36
  unprocessed_token unknown_case_token unbalanced_bracket
35
37
  unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
36
38
  invalid_statement
@@ -41,11 +43,11 @@ module Dentaku
41
43
  raise ::ArgumentError, "Unhandled #{reason}"
42
44
  end
43
45
 
44
- new reason, meta
46
+ new(reason, **meta)
45
47
  end
46
48
  end
47
49
 
48
- class TokenizerError < StandardError
50
+ class TokenizerError < Error
49
51
  attr_reader :reason, :meta
50
52
 
51
53
  def initialize(reason, **meta)
@@ -65,12 +67,13 @@ module Dentaku
65
67
  raise ::ArgumentError, "Unhandled #{reason}"
66
68
  end
67
69
 
68
- new reason, meta
70
+ new(reason, **meta)
69
71
  end
70
72
  end
71
73
 
72
74
  class ArgumentError < ::ArgumentError
73
75
  attr_reader :reason, :meta
76
+ attr_accessor :recipient_variable
74
77
 
75
78
  def initialize(reason, **meta)
76
79
  @reason = reason
@@ -89,7 +92,7 @@ module Dentaku
89
92
  raise ::ArgumentError, "Unhandled #{reason}"
90
93
  end
91
94
 
92
- new reason, meta
95
+ new(reason, **meta)
93
96
  end
94
97
  end
95
98
 
@@ -6,6 +6,13 @@ module Dentaku
6
6
  flatten_keys(acc)
7
7
  end
8
8
 
9
+ def self.from_hash_with_intermediates(h, key = [], acc = {})
10
+ acc.update(key => h) unless key.empty?
11
+ return unless h.is_a? Hash
12
+ h.each { |k, v| from_hash_with_intermediates(v, key + [k], acc) }
13
+ flatten_keys(acc)
14
+ end
15
+
9
16
  def self.flatten_keys(hash)
10
17
  hash.each_with_object({}) do |(k, v), h|
11
18
  h[flatten_key(k)] = v
@@ -19,8 +26,8 @@ module Dentaku
19
26
  key
20
27
  end
21
28
 
22
- def self.expand(h)
23
- h.each_with_object({}) do |(k, v), r|
29
+ def self.expand(hash)
30
+ hash.each_with_object({}) do |(k, v), r|
24
31
  hash_levels = k.to_s.split('.')
25
32
  hash_levels = hash_levels.map(&:to_sym) if k.is_a?(Symbol)
26
33
  child_hash = hash_levels[0...-1].reduce(r) { |h, n| h[n] ||= {} }
@@ -10,8 +10,11 @@ module Dentaku
10
10
  pow: AST::Exponentiation,
11
11
  negate: AST::Negation,
12
12
  mod: AST::Modulo,
13
+
13
14
  bitor: AST::BitwiseOr,
14
15
  bitand: AST::BitwiseAnd,
16
+ bitshiftleft: AST::BitwiseShiftLeft,
17
+ bitshiftright: AST::BitwiseShiftRight,
15
18
 
16
19
  lt: AST::LessThan,
17
20
  gt: AST::GreaterThan,
@@ -22,6 +25,7 @@ module Dentaku
22
25
 
23
26
  and: AST::And,
24
27
  or: AST::Or,
28
+ xor: AST::Xor,
25
29
  }.freeze
26
30
 
27
31
  attr_reader :input, :output, :operations, :arities, :case_sensitive
@@ -37,15 +41,28 @@ module Dentaku
37
41
 
38
42
  def consume(count = 2)
39
43
  operator = operations.pop
44
+ fail! :invalid_statement if operator.nil?
45
+
40
46
  operator.peek(output)
41
47
 
42
48
  args_size = operator.arity || count
43
- if args_size > output.length
44
- fail! :too_few_operands, operator: operator, expect: args_size, actual: output.length
49
+ min_size = operator.arity || operator.min_param_count || count
50
+ max_size = operator.arity || operator.max_param_count || count
51
+
52
+ if output.length < min_size || args_size < min_size
53
+ fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
54
+ end
55
+
56
+ if output.length > max_size && operations.empty? || args_size > max_size
57
+ fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
45
58
  end
59
+
60
+ fail! :invalid_statement if output.size < args_size
46
61
  args = Array.new(args_size) { output.pop }.reverse
47
62
 
48
63
  output.push operator.new(*args)
64
+ rescue ::ArgumentError => e
65
+ raise Dentaku::ArgumentError, e.message
49
66
  rescue NodeError => e
50
67
  fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
51
68
  end
@@ -111,17 +128,19 @@ module Dentaku
111
128
  open_cases = 0
112
129
  case_end_index = nil
113
130
 
114
- input.each_with_index do |token, index|
115
- if token.category == :case && token.value == :open
116
- open_cases += 1
117
- end
131
+ input.each_with_index do |input_token, index|
132
+ if input_token.category == :case
133
+ if input_token.value == :open
134
+ open_cases += 1
135
+ end
118
136
 
119
- if token.category == :case && token.value == :close
120
- if open_cases > 0
121
- open_cases -= 1
122
- else
123
- case_end_index = index
124
- break
137
+ if input_token.value == :close
138
+ if open_cases > 0
139
+ open_cases -= 1
140
+ else
141
+ case_end_index = index
142
+ break
143
+ end
125
144
  end
126
145
  end
127
146
  end
@@ -129,7 +148,9 @@ module Dentaku
129
148
  subparser = Parser.new(
130
149
  inner_case_inputs,
131
150
  operations: [AST::Case],
132
- arities: [0]
151
+ arities: [0],
152
+ function_registry: @function_registry,
153
+ case_sensitive: case_sensitive
133
154
  )
134
155
  subparser.parse
135
156
  output.concat(subparser.output)
@@ -205,11 +226,28 @@ module Dentaku
205
226
  end
206
227
 
207
228
  unless operations.last == AST::Access
208
- fail! :unbalanced_bracket, token
229
+ fail! :unbalanced_bracket, token: token
209
230
  end
210
231
  consume
211
232
  end
212
233
 
234
+ when :array
235
+ case token.value
236
+ when :array_start
237
+ operations.push AST::Array
238
+ arities.push 0
239
+ when :array_end
240
+ while operations.any? && operations.last != AST::Array
241
+ consume
242
+ end
243
+
244
+ unless operations.last == AST::Array
245
+ fail! :unbalanced_bracket, token: token
246
+ end
247
+
248
+ consume(arities.pop.succ)
249
+ end
250
+
213
251
  when :grouping
214
252
  case token.value
215
253
  when :open
@@ -236,8 +274,9 @@ module Dentaku
236
274
  end
237
275
 
238
276
  when :comma
277
+ fail! :invalid_statement if arities.empty?
239
278
  arities[-1] += 1
240
- while operations.any? && operations.last != AST::Grouping
279
+ while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
241
280
  consume
242
281
  end
243
282
 
@@ -282,6 +321,8 @@ module Dentaku
282
321
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
283
322
  when :too_few_operands
284
323
  "#{meta.fetch(:operator)} has too few operands"
324
+ when :too_many_operands
325
+ "#{meta.fetch(:operator)} has too many operands"
285
326
  when :undefined_function
286
327
  "Undefined function #{meta.fetch(:function_name)}"
287
328
  when :unprocessed_token
@@ -302,7 +343,7 @@ module Dentaku
302
343
  raise ::ArgumentError, "Unhandled #{reason}"
303
344
  end
304
345
 
305
- raise ParseError.for(reason, meta), message
346
+ raise ParseError.for(reason, **meta), message
306
347
  end
307
348
  end
308
349
  end
@@ -0,0 +1,101 @@
1
+ module Dentaku
2
+ class PrintVisitor
3
+ def initialize(node)
4
+ @output = ''
5
+ node.accept(self)
6
+ end
7
+
8
+ def visit_operation(node)
9
+ if node.left
10
+ visit_operand(node.left, node.class.precedence, suffix: " ")
11
+ end
12
+
13
+ @output << node.display_operator
14
+
15
+ if node.right
16
+ visit_operand(node.right, node.class.precedence, prefix: " ")
17
+ end
18
+ end
19
+
20
+ def visit_operand(node, precedence, prefix: "", suffix: "")
21
+ @output << prefix
22
+ @output << "(" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
23
+ node.accept(self)
24
+ @output << ")" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
25
+ @output << suffix
26
+ end
27
+
28
+ def visit_function(node)
29
+ @output << node.name
30
+ @output << "("
31
+ arg_count = node.args.length
32
+ node.args.each_with_index do |a, index|
33
+ a.accept(self)
34
+ @output << ", " unless index >= arg_count - 1
35
+ end
36
+ @output << ")"
37
+ end
38
+
39
+ def visit_case(node)
40
+ @output << "CASE "
41
+ node.switch.accept(self)
42
+ node.conditions.each { |c| c.accept(self) }
43
+ node.else && node.else.accept(self)
44
+ @output << " END"
45
+ end
46
+
47
+ def visit_switch(node)
48
+ node.node.accept(self)
49
+ end
50
+
51
+ def visit_case_conditional(node)
52
+ node.when.accept(self)
53
+ node.then.accept(self)
54
+ end
55
+
56
+ def visit_when(node)
57
+ @output << " WHEN "
58
+ node.node.accept(self)
59
+ end
60
+
61
+ def visit_then(node)
62
+ @output << " THEN "
63
+ node.node.accept(self)
64
+ end
65
+
66
+ def visit_else(node)
67
+ @output << " ELSE "
68
+ node.node.accept(self)
69
+ end
70
+
71
+ def visit_negation(node)
72
+ @output << "-"
73
+ @output << "(" unless node.node.is_a? Dentaku::AST::Literal
74
+ node.node.accept(self)
75
+ @output << ")" unless node.node.is_a? Dentaku::AST::Literal
76
+ end
77
+
78
+ def visit_access(node)
79
+ node.structure.accept(self)
80
+ @output << "["
81
+ node.index.accept(self)
82
+ @output << "]"
83
+ end
84
+
85
+ def visit_literal(node)
86
+ @output << node.quoted
87
+ end
88
+
89
+ def visit_identifier(node)
90
+ @output << node.identifier
91
+ end
92
+
93
+ def visit_nil(node)
94
+ @output << "NULL"
95
+ end
96
+
97
+ def to_s
98
+ @output
99
+ end
100
+ end
101
+ end
@@ -16,7 +16,7 @@ module Dentaku
16
16
  @range = (@min..@max)
17
17
  end
18
18
 
19
- def | (other_matcher)
19
+ def |(other_matcher)
20
20
  self.class.new(:nomatch, :nomatch, leaf_matchers + other_matcher.leaf_matchers)
21
21
  end
22
22
 
@@ -43,6 +43,7 @@ module Dentaku
43
43
  :combinator,
44
44
  :operator,
45
45
  :grouping,
46
+ :array,
46
47
  :access,
47
48
  :case_statement,
48
49
  :comparator,
@@ -90,7 +91,7 @@ module Dentaku
90
91
 
91
92
  def numeric
92
93
  new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
93
- raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i
94
+ raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
94
95
  })
95
96
  end
96
97
 
@@ -119,9 +120,9 @@ module Dentaku
119
120
 
120
121
  def operator
121
122
  names = {
122
- pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
123
+ pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
123
124
  }.invert
124
- new(:operator, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
125
+ new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
125
126
  end
126
127
 
127
128
  def grouping
@@ -129,6 +130,11 @@ module Dentaku
129
130
  new(:grouping, '\(|\)|,', lambda { |raw| names[raw] })
130
131
  end
131
132
 
133
+ def array
134
+ names = { array_start: '{', array_end: '}', }.invert
135
+ new(:array, '\{|\}|,', lambda { |raw| names[raw] })
136
+ end
137
+
132
138
  def access
133
139
  names = { lbracket: '[', rbracket: ']' }.invert
134
140
  new(:access, '\[|\]', lambda { |raw| names[raw] })
@@ -12,7 +12,7 @@ module Dentaku
12
12
  def tokenize(string, options = {})
13
13
  @nesting = 0
14
14
  @tokens = []
15
- @aliases = options.fetch(:aliases, Dentaku.aliases)
15
+ @aliases = options.fetch(:aliases, global_aliases)
16
16
  input = strip_comments(string.to_s.dup)
17
17
  input = replace_aliases(input)
18
18
  @case_sensitive = options.fetch(:case_sensitive, false)
@@ -84,6 +84,11 @@ module Dentaku
84
84
 
85
85
  private
86
86
 
87
+ def global_aliases
88
+ return {} unless Dentaku.respond_to?(:aliases)
89
+ Dentaku.aliases
90
+ end
91
+
87
92
  def fail!(reason, **meta)
88
93
  message =
89
94
  case reason
@@ -99,7 +104,7 @@ module Dentaku
99
104
  raise ::ArgumentError, "Unhandled #{reason}"
100
105
  end
101
106
 
102
- raise TokenizerError.for(reason, meta), message
107
+ raise TokenizerError.for(reason, **meta), message
103
108
  end
104
109
  end
105
110
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.2.0"
2
+ VERSION = "3.5.1"
3
3
  end
@@ -0,0 +1,82 @@
1
+ # infix visitor
2
+ #
3
+ # use this visitor in a processor to get infix visiting order
4
+ #
5
+ # visitor node deps
6
+ # accept -> visit left ->
7
+ # process
8
+ # visit right ->
9
+ module Dentaku
10
+ module Visitor
11
+ module Infix
12
+ def visit(ast)
13
+ ast.accept(self)
14
+ end
15
+
16
+ def process(_ast)
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def visit_function(node)
21
+ node.args.each do |arg|
22
+ visit(arg)
23
+ end
24
+ process(node)
25
+ end
26
+
27
+ def visit_identifier(node)
28
+ process(node)
29
+ end
30
+
31
+ def visit_operation(node)
32
+ visit(node.left) if node.left
33
+ process(node)
34
+ visit(node.right) if node.right
35
+ end
36
+
37
+ def visit_operand(node)
38
+ process(node)
39
+ end
40
+
41
+ def visit_case(node)
42
+ process(node)
43
+ end
44
+
45
+ def visit_switch(node)
46
+ process(node)
47
+ end
48
+
49
+ def visit_case_conditional(node)
50
+ process(node)
51
+ end
52
+
53
+ def visit_when(node)
54
+ process(node)
55
+ end
56
+
57
+ def visit_then(node)
58
+ process(node)
59
+ end
60
+
61
+ def visit_else(node)
62
+ process(node)
63
+ end
64
+
65
+ def visit_negation(node)
66
+ process(node)
67
+ end
68
+
69
+ def visit_access(node)
70
+ process(node)
71
+ end
72
+
73
+ def visit_literal(node)
74
+ process(node)
75
+ end
76
+
77
+ def visit_nil(node)
78
+ process(node)
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/dentaku.rb CHANGED
@@ -1,23 +1,26 @@
1
1
  require "bigdecimal"
2
+ require "concurrent"
2
3
  require "dentaku/calculator"
3
4
  require "dentaku/version"
4
5
 
5
6
  module Dentaku
6
7
  @enable_ast_caching = false
7
8
  @enable_dependency_order_caching = false
9
+ @enable_identifier_caching = false
8
10
  @aliases = {}
9
11
 
10
- def self.evaluate(expression, data = {})
11
- calculator.evaluate(expression, data)
12
+ def self.evaluate(expression, data = {}, &block)
13
+ calculator.value.evaluate(expression, data, &block)
12
14
  end
13
15
 
14
- def self.evaluate!(expression, data = {})
15
- calculator.evaluate!(expression, data)
16
+ def self.evaluate!(expression, data = {}, &block)
17
+ calculator.value.evaluate!(expression, data, &block)
16
18
  end
17
19
 
18
20
  def self.enable_caching!
19
21
  enable_ast_cache!
20
22
  enable_dependency_order_cache!
23
+ enable_identifier_cache!
21
24
  end
22
25
 
23
26
  def self.enable_ast_cache!
@@ -36,6 +39,14 @@ module Dentaku
36
39
  @enable_dependency_order_caching
37
40
  end
38
41
 
42
+ def self.enable_identifier_cache!
43
+ @enable_identifier_caching = true
44
+ end
45
+
46
+ def self.cache_identifier?
47
+ @enable_identifier_caching
48
+ end
49
+
39
50
  def self.aliases
40
51
  @aliases
41
52
  end
@@ -44,13 +55,15 @@ module Dentaku
44
55
  @aliases = hash
45
56
  end
46
57
 
47
- private
48
-
49
58
  def self.calculator
50
- @calculator ||= Dentaku::Calculator.new
59
+ @calculator ||= Concurrent::ThreadLocalVar.new { Dentaku::Calculator.new }
51
60
  end
52
61
  end
53
62
 
54
63
  def Dentaku(expression, data = {})
55
64
  Dentaku.evaluate(expression, data)
56
65
  end
66
+
67
+ def Dentaku!(expression, data = {})
68
+ Dentaku.evaluate!(expression, data)
69
+ end
@@ -9,9 +9,15 @@ describe Dentaku::AST::Addition do
9
9
 
10
10
  let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
11
 
12
+ it 'allows access to its sub-trees' do
13
+ node = described_class.new(five, six)
14
+ expect(node.left).to eq(five)
15
+ expect(node.right).to eq(six)
16
+ end
17
+
12
18
  it 'performs addition' do
13
19
  node = described_class.new(five, six)
14
- expect(node.value).to eq 11
20
+ expect(node.value).to eq(11)
15
21
  end
16
22
 
17
23
  it 'requires numeric operands' do
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/ast/functions/all'
3
+ require 'dentaku'
4
+
5
+ describe Dentaku::AST::All do
6
+ let(:calculator) { Dentaku::Calculator.new }
7
+ it 'performs ALL operation' do
8
+ result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
9
+ expect(result).to eq(false)
10
+ end
11
+
12
+ it 'works with a single value if needed for some reason' do
13
+ result = Dentaku('ALL(vals, val, val > 1)', vals: 1)
14
+ expect(result).to eq(false)
15
+
16
+ result = Dentaku('ALL(vals, val, val > 1)', vals: 2)
17
+ expect(result).to eq(true)
18
+ end
19
+
20
+ it 'raises argument error if a string is passed as identifier' do
21
+ expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
22
+ Dentaku::ArgumentError, 'ALL() requires second argument to be an identifier'
23
+ )
24
+ end
25
+ end
@@ -7,29 +7,29 @@ describe 'Dentaku::AST::And' do
7
7
 
8
8
  it 'returns false if any of the arguments is false' do
9
9
  result = Dentaku('AND(1 = 1, 0 = 1)')
10
- expect(result).to eq false
10
+ expect(result).to eq(false)
11
11
  end
12
12
 
13
13
  it 'supports nested expressions' do
14
14
  result = Dentaku('AND(y = 1, x = 1)', x: 1, y: 2)
15
- expect(result).to eq false
15
+ expect(result).to eq(false)
16
16
  end
17
17
 
18
18
  it 'returns true if all of the arguments are true' do
19
19
  result = Dentaku('AND(1 = 1, "2" = "2", true = true, true)')
20
- expect(result).to eq true
20
+ expect(result).to eq(true)
21
21
  end
22
22
 
23
23
  it 'returns true if all nested AND functions return true' do
24
24
  result = Dentaku('AND(AND(1 = 1), AND(true != false, AND(true)))')
25
- expect(result).to eq true
25
+ expect(result).to eq(true)
26
26
  end
27
27
 
28
28
  it 'raises an error if no arguments are passed' do
29
- expect { calculator.evaluate!('AND()') }.to raise_error(ArgumentError)
29
+ expect { calculator.evaluate!('AND()') }.to raise_error(Dentaku::ArgumentError)
30
30
  end
31
31
 
32
32
  it 'raises an error if a non logical argument is passed' do
33
- expect { calculator.evaluate!('AND("r")') }.to raise_error(ArgumentError)
33
+ expect { calculator.evaluate!('AND("r")') }.to raise_error(Dentaku::ArgumentError)
34
34
  end
35
35
  end
data/spec/ast/and_spec.rb CHANGED
@@ -11,7 +11,7 @@ describe Dentaku::AST::And do
11
11
 
12
12
  it 'performs logical AND' do
13
13
  node = described_class.new(t, f)
14
- expect(node.value).to eq false
14
+ expect(node.value).to eq(false)
15
15
  end
16
16
 
17
17
  it 'requires logical operands' do