dentaku 3.5.8 → 4.0.1

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 (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +2 -4
  3. data/.github/workflows/rubocop.yml +1 -1
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +108 -1
  6. data/Gemfile +1 -1
  7. data/README.md +123 -0
  8. data/dentaku.gemspec +15 -8
  9. data/lib/dentaku/ast/access.rb +18 -1
  10. data/lib/dentaku/ast/arithmetic.rb +22 -9
  11. data/lib/dentaku/ast/array.rb +6 -0
  12. data/lib/dentaku/ast/bitwise.rb +6 -4
  13. data/lib/dentaku/ast/case/case_conditional.rb +6 -0
  14. data/lib/dentaku/ast/case/case_else.rb +6 -0
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +6 -0
  16. data/lib/dentaku/ast/case/case_then.rb +6 -0
  17. data/lib/dentaku/ast/case/case_when.rb +7 -0
  18. data/lib/dentaku/ast/case.rb +30 -3
  19. data/lib/dentaku/ast/combinators.rb +52 -5
  20. data/lib/dentaku/ast/comparators.rb +2 -3
  21. data/lib/dentaku/ast/function.rb +15 -2
  22. data/lib/dentaku/ast/function_registry.rb +28 -2
  23. data/lib/dentaku/ast/functions/and.rb +4 -4
  24. data/lib/dentaku/ast/functions/avg.rb +2 -2
  25. data/lib/dentaku/ast/functions/duration.rb +1 -1
  26. data/lib/dentaku/ast/functions/if.rb +9 -1
  27. data/lib/dentaku/ast/functions/intercept.rb +2 -2
  28. data/lib/dentaku/ast/functions/mul.rb +2 -2
  29. data/lib/dentaku/ast/functions/or.rb +4 -4
  30. data/lib/dentaku/ast/functions/pluck.rb +1 -1
  31. data/lib/dentaku/ast/functions/string_functions.rb +1 -1
  32. data/lib/dentaku/ast/functions/sum.rb +2 -2
  33. data/lib/dentaku/ast/functions/xor.rb +4 -4
  34. data/lib/dentaku/ast/grouping.rb +6 -0
  35. data/lib/dentaku/ast/negation.rb +8 -1
  36. data/lib/dentaku/ast/node.rb +25 -0
  37. data/lib/dentaku/ast/operation.rb +7 -0
  38. data/lib/dentaku/bulk_expression_solver.rb +9 -10
  39. data/lib/dentaku/calculator.rb +83 -34
  40. data/lib/dentaku/date_arithmetic.rb +2 -2
  41. data/lib/dentaku/exceptions.rb +105 -13
  42. data/lib/dentaku/numeric_parser.rb +1 -1
  43. data/lib/dentaku/parser.rb +24 -48
  44. data/lib/dentaku/token_matcher.rb +31 -37
  45. data/lib/dentaku/token_matchers.rb +3 -3
  46. data/lib/dentaku/tokenizer.rb +1 -15
  47. data/lib/dentaku/version.rb +1 -1
  48. data/spec/ast/arithmetic_spec.rb +93 -0
  49. data/spec/ast/case_spec.rb +7 -0
  50. data/spec/bulk_expression_solver_spec.rb +13 -3
  51. data/spec/calculator_spec.rb +146 -0
  52. data/spec/exceptions_spec.rb +87 -0
  53. data/spec/external_function_spec.rb +33 -0
  54. data/spec/gemspec_spec.rb +10 -0
  55. data/spec/identifiers_spec.rb +66 -0
  56. data/spec/parser_spec.rb +35 -0
  57. data/spec/print_visitor_spec.rb +5 -0
  58. data/spec/spec_helper.rb +9 -0
  59. data/spec/volatile_functions_spec.rb +121 -0
  60. metadata +21 -63
  61. data/.travis.yml +0 -10
@@ -9,38 +9,46 @@ require 'dentaku/token'
9
9
  module Dentaku
10
10
  class Calculator
11
11
  include StringCasing
12
- attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
12
+ attr_reader :result, :memory, :tokenizer, :case_sensitive,
13
13
  :nested_data_support, :ast_cache, :raw_date_literals
14
14
 
15
- def initialize(options = {})
15
+ def initialize(case_sensitive: false, aliases: nil, nested_data_support: true,
16
+ raw_date_literals: true, ast_cache: {},
17
+ cache_ast: nil, cache_dependency_order: nil)
16
18
  clear
17
19
  @tokenizer = Tokenizer.new
18
- @case_sensitive = options.delete(:case_sensitive)
19
- @aliases = options.delete(:aliases) || Dentaku.aliases
20
- @nested_data_support = options.fetch(:nested_data_support, true)
21
- options.delete(:nested_data_support)
22
- @raw_date_literals = options.fetch(:raw_date_literals, true)
23
- options.delete(:raw_date_literals)
24
- @ast_cache = options
20
+ @case_sensitive = case_sensitive
21
+ @aliases = aliases
22
+ @nested_data_support = nested_data_support
23
+ @raw_date_literals = raw_date_literals
24
+ @ast_cache = ast_cache
25
+ @cache_ast = cache_ast
26
+ @cache_dependency_order = cache_dependency_order
25
27
  @disable_ast_cache = false
26
28
  @function_registry = Dentaku::AST::FunctionRegistry.new
27
29
  end
28
30
 
29
- def self.add_function(name, type, body, callback = nil)
30
- Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
31
+ # explicitly configured aliases win; otherwise the module-level default is
32
+ # resolved lazily so it can be set after this calculator was created
33
+ def aliases
34
+ @aliases || Dentaku.aliases
35
+ end
36
+
37
+ def self.add_function(name, type, body, callback = nil, volatile: false)
38
+ Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback, volatile: volatile)
31
39
  end
32
40
 
33
41
  def self.add_functions(functions)
34
- functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
42
+ functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
35
43
  end
36
44
 
37
- def add_function(name, type, body, callback = nil)
38
- @function_registry.register(name, type, body, callback)
45
+ def add_function(name, type, body, callback = nil, volatile: false)
46
+ @function_registry.register(name, type, body, callback, volatile: volatile)
39
47
  self
40
48
  end
41
49
 
42
50
  def add_functions(functions)
43
- functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
51
+ functions.each { |(name, type, body, callback, volatile)| add_function(name, type, body, callback, volatile: !!volatile) }
44
52
  self
45
53
  end
46
54
 
@@ -56,7 +64,7 @@ module Dentaku
56
64
  return evaluate_array(expression, context, &block) if expression.is_a?(Array)
57
65
 
58
66
  evaluate!(expression, context)
59
- rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError => ex
67
+ rescue Dentaku::Error => ex
60
68
  block.call(expression, ex) if block_given?
61
69
  end
62
70
 
@@ -94,7 +102,9 @@ module Dentaku
94
102
  end
95
103
 
96
104
  def dependencies(expression, context = {})
97
- test_context = context.nil? ? {} : store(context) { memory }
105
+ # dup inside the block: `store` now restores in place, so the caller
106
+ # needs its own copy of the merged context rather than the live memory
107
+ test_context = context.nil? ? {} : store(context) { memory.dup }
98
108
 
99
109
  case expression
100
110
  when Dentaku::AST::Node
@@ -106,6 +116,20 @@ module Dentaku
106
116
  end
107
117
  end
108
118
 
119
+ # every identifier the expression could reference, regardless of
120
+ # branching: purely syntactic, ignores stored memory, and never
121
+ # evaluates guards or functions
122
+ def identifiers(expression)
123
+ case expression
124
+ when Dentaku::AST::Node
125
+ expression.dependencies(AST::Node::STATIC_CONTEXT).uniq
126
+ when Array
127
+ expression.flat_map { |e| identifiers(e) }.uniq
128
+ else
129
+ ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq
130
+ end
131
+ end
132
+
109
133
  def ast(expression)
110
134
  return expression if expression.is_a?(AST::Node)
111
135
  return expression.map { |e| ast(e) } if expression.is_a? Array
@@ -147,32 +171,51 @@ module Dentaku
147
171
  end
148
172
 
149
173
  def store(key_or_hash, value = nil)
150
- restore = Hash[memory]
174
+ pairs = pairs_to_store(key_or_hash, value)
151
175
 
152
- if value.nil?
153
- key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
154
- key_or_hash.each do |key, val|
155
- memory[standardize_case(key.to_s)] = val
156
- end
157
- else
158
- memory[standardize_case(key_or_hash.to_s)] = value
176
+ unless block_given?
177
+ pairs.each { |key, val| memory[key] = val }
178
+ return self
159
179
  end
160
180
 
161
- if block_given?
181
+ # `evaluate!` routes every call through here, so snapshotting the whole
182
+ # memory hash makes each evaluation scale with the number of stored
183
+ # variables (#336). Undoing just the keys this call touched is O(pairs)
184
+ # instead of O(memory) -- but it is only equivalent while nothing else
185
+ # writes into memory during the block. The identifier cache does exactly
186
+ # that, so when it is enabled we still need the wholesale snapshot to
187
+ # keep cached values scoped to a single evaluation.
188
+ if Dentaku.cache_identifier?
189
+ restore = Hash[memory]
190
+ pairs.each { |key, val| memory[key] = val }
191
+
162
192
  begin
163
- result = yield
164
- @memory = restore
165
- return result
166
- rescue => e
193
+ yield
194
+ ensure
167
195
  @memory = restore
168
- raise e
169
196
  end
170
- end
197
+ else
198
+ undo = pairs.map { |key, _| [key, memory.key?(key), memory[key]] }
199
+ pairs.each { |key, val| memory[key] = val }
171
200
 
172
- self
201
+ begin
202
+ yield
203
+ ensure
204
+ undo.each { |key, present, val| present ? memory[key] = val : memory.delete(key) }
205
+ end
206
+ end
173
207
  end
174
208
  alias_method :bind, :store
175
209
 
210
+ private def pairs_to_store(key_or_hash, value)
211
+ if value.nil?
212
+ key_or_hash = FlatHash.from_hash_with_intermediates(key_or_hash) if nested_data_support
213
+ key_or_hash.map { |key, val| [standardize_case(key.to_s), val] }
214
+ else
215
+ [[standardize_case(key_or_hash.to_s), value]]
216
+ end
217
+ end
218
+
176
219
  def store_formula(key, formula)
177
220
  store(key, ast(formula))
178
221
  end
@@ -186,7 +229,13 @@ module Dentaku
186
229
  end
187
230
 
188
231
  def cache_ast?
189
- Dentaku.cache_ast? && !@disable_ast_cache
232
+ return false if @disable_ast_cache
233
+
234
+ @cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast
235
+ end
236
+
237
+ def cache_dependency_order?
238
+ @cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order
190
239
  end
191
240
  end
192
241
  end
@@ -20,7 +20,7 @@ module Dentaku
20
20
  change_datetime(@base, duration.unit, duration.value)
21
21
  end
22
22
  else
23
- raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
23
+ raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
24
24
  "'#{duration || duration.class}' is not coercible for date arithmetic"
25
25
  end
26
26
  end
@@ -39,7 +39,7 @@ module Dentaku
39
39
  when Dentaku::TokenScanner::DATE_TIME_REGEXP
40
40
  @base - Time.parse(duration).to_datetime
41
41
  else
42
- raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
42
+ raise Dentaku::ArgumentError.for(:incompatible_type, actual: duration, expected: Numeric),
43
43
  "'#{duration || duration.class}' is not coercible for date arithmetic"
44
44
  end
45
45
  end
@@ -1,9 +1,16 @@
1
1
  module Dentaku
2
- class Error < StandardError
3
- attr_accessor :recipient_variable
2
+ # Shared by all Dentaku exceptions so that `rescue Dentaku::Error` catches
3
+ # everything the gem raises, including exceptions that subclass Ruby
4
+ # built-ins (Dentaku::ArgumentError, Dentaku::ZeroDivisionError).
5
+ module Error
6
+ attr_accessor :assigned_to
4
7
  end
5
8
 
6
- class UnboundVariableError < Error
9
+ class BaseError < StandardError
10
+ include Error
11
+ end
12
+
13
+ class UnboundVariableError < BaseError
7
14
  attr_reader :unbound_variables
8
15
 
9
16
  def initialize(unbound_variables)
@@ -11,7 +18,7 @@ module Dentaku
11
18
  end
12
19
  end
13
20
 
14
- class MathDomainError < Error
21
+ class MathDomainError < BaseError
15
22
  attr_reader :function_name, :args
16
23
 
17
24
  def initialize(function_name, args)
@@ -20,22 +27,23 @@ module Dentaku
20
27
  end
21
28
  end
22
29
 
23
- class NodeError < Error
24
- attr_reader :child, :expect, :actual
30
+ class NodeError < BaseError
31
+ attr_reader :operand, :expected, :actual
25
32
 
26
- def initialize(expect, actual, child)
27
- @expect = Array(expect)
33
+ def initialize(expected, actual, operand)
34
+ @expected = Array(expected)
28
35
  @actual = actual
29
- @child = child
36
+ @operand = operand
30
37
  end
31
38
  end
32
39
 
33
- class ParseError < Error
40
+ class ParseError < BaseError
34
41
  attr_reader :reason, :meta
35
42
 
36
43
  def initialize(reason, **meta)
37
44
  @reason = reason
38
45
  @meta = meta
46
+ super(default_message)
39
47
  end
40
48
 
41
49
  private_class_method :new
@@ -54,14 +62,49 @@ module Dentaku
54
62
 
55
63
  new(reason, **meta)
56
64
  end
65
+
66
+ private
67
+
68
+ def default_message
69
+ case reason
70
+ when :node_invalid
71
+ if meta.key?(:operation)
72
+ expected = Array(meta[:expected]).map { |e| e == :incompatible ? :compatible : e }
73
+ "#{meta[:operation]} requires #{expected.join(', ')} operands, but got #{meta[:actual]}"
74
+ else
75
+ "Node is invalid"
76
+ end
77
+ when :too_few_operands
78
+ "#{meta[:operation]} has too few operands (given #{meta[:actual]}, expected #{meta[:expected]})"
79
+ when :too_many_operands
80
+ "#{meta[:operation]} has too many operands (given #{meta[:actual]}, expected #{meta[:expected]})"
81
+ when :undefined_function
82
+ "Undefined function #{meta[:function_name]}"
83
+ when :unprocessed_token
84
+ "Unprocessed token #{meta[:token_name]}"
85
+ when :unknown_case_token
86
+ "Unknown case token #{meta[:token_name]}"
87
+ when :unbalanced_bracket
88
+ "Unbalanced bracket"
89
+ when :unbalanced_parenthesis
90
+ "Unbalanced parenthesis"
91
+ when :unknown_grouping_token
92
+ "Unknown grouping token #{meta[:token_name]}"
93
+ when :not_implemented_token_category
94
+ "Not implemented for tokens of category #{meta[:token_category]}"
95
+ when :invalid_statement
96
+ "Invalid statement"
97
+ end
98
+ end
57
99
  end
58
100
 
59
- class TokenizerError < Error
101
+ class TokenizerError < BaseError
60
102
  attr_reader :reason, :meta
61
103
 
62
104
  def initialize(reason, **meta)
63
105
  @reason = reason
64
106
  @meta = meta
107
+ super(default_message)
65
108
  end
66
109
 
67
110
  private_class_method :new
@@ -80,15 +123,32 @@ module Dentaku
80
123
 
81
124
  new(reason, **meta)
82
125
  end
126
+
127
+ private
128
+
129
+ def default_message
130
+ case reason
131
+ when :parse_error
132
+ "parse error at: '#{meta[:at]}'"
133
+ when :too_many_opening_parentheses
134
+ "too many opening parentheses"
135
+ when :too_many_closing_parentheses
136
+ "too many closing parentheses"
137
+ when :unexpected_zero_width_match
138
+ "unexpected zero-width match (:#{meta[:token_category]}) at '#{meta[:at]}'"
139
+ end
140
+ end
83
141
  end
84
142
 
85
143
  class ArgumentError < ::ArgumentError
144
+ include Error
145
+
86
146
  attr_reader :reason, :meta
87
- attr_accessor :recipient_variable
88
147
 
89
148
  def initialize(reason, **meta)
90
149
  @reason = reason
91
150
  @meta = meta
151
+ super(default_message)
92
152
  end
93
153
 
94
154
  private_class_method :new
@@ -108,9 +168,41 @@ module Dentaku
108
168
 
109
169
  new(reason, **meta)
110
170
  end
171
+
172
+ private
173
+
174
+ def default_message
175
+ case reason
176
+ when :incompatible_type
177
+ if meta.key?(:function_name)
178
+ "#{meta[:function_name]}() requires #{meta[:expected]} arguments, but got #{meta[:actual].class}"
179
+ else
180
+ "#{meta[:actual].class} is not compatible with #{meta[:expected]}"
181
+ end
182
+ when :invalid_operator
183
+ "#{meta[:operation]} requires operands that respond to #{meta[:operator]}"
184
+ when :invalid_value
185
+ if meta.key?(:expected)
186
+ "'#{meta[:actual]}' is not a valid #{meta[:expected]}"
187
+ elsif meta.key?(:actual)
188
+ "'#{meta[:actual]}' is not a valid value"
189
+ else
190
+ "Invalid value"
191
+ end
192
+ when :too_few_arguments
193
+ "#{meta[:function_name]}() has too few arguments (given #{meta[:actual]}, expected #{expected_count})"
194
+ when :wrong_number_of_arguments
195
+ "#{meta[:function_name]}() has the wrong number of arguments (given #{meta[:actual]}, expected #{expected_count})"
196
+ end
197
+ end
198
+
199
+ def expected_count
200
+ expected = meta[:expected]
201
+ expected.is_a?(Range) && expected.end.nil? ? "at least #{expected.begin}" : expected
202
+ end
111
203
  end
112
204
 
113
205
  class ZeroDivisionError < ::ZeroDivisionError
114
- attr_accessor :recipient_variable
206
+ include Error
115
207
  end
116
208
  end
@@ -54,7 +54,7 @@ module Dentaku
54
54
  return number if number
55
55
  end
56
56
 
57
- raise Dentaku::ArgumentError.for(:incompatible_type, value: value, for: Numeric),
57
+ raise Dentaku::ArgumentError.for(:incompatible_type, actual: value, expected: Numeric),
58
58
  "'#{value || value.class}' is not coercible to numeric"
59
59
  end
60
60
 
@@ -41,41 +41,42 @@ module Dentaku
41
41
  end
42
42
 
43
43
  def consume(count = 2)
44
- operator = operations.pop
45
- fail! :invalid_statement if operator.nil?
44
+ operation = operations.pop
45
+ fail! :invalid_statement if operation.nil?
46
46
 
47
47
  output_size = output.length
48
- args_size = operator.arity || count
49
- min_size = operator.arity || operator.min_param_count || count
50
- max_size = operator.arity || operator.max_param_count || count
48
+ args_size = operation.arity || count
49
+ min_size = operation.arity || operation.min_param_count || count
50
+ max_size = operation.arity || operation.max_param_count || count
51
51
 
52
52
  if output_size < min_size || args_size < min_size
53
- expect = min_size == max_size ? min_size : min_size..max_size
54
- fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
53
+ expected = min_size == max_size ? min_size : min_size..max_size
54
+ fail! :too_few_operands, operation: operation, expected: expected, actual: output_size
55
55
  end
56
56
 
57
57
  if (output_size > max_size && operations.empty?) || args_size > max_size
58
- expect = min_size == max_size ? min_size : min_size..max_size
59
- fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
58
+ expected = min_size == max_size ? min_size : min_size..max_size
59
+ fail! :too_many_operands, operation: operation, expected: expected, actual: output_size
60
60
  end
61
61
 
62
62
  args = []
63
- if operator == AST::Array && output.empty?
63
+ if operation == AST::Array && output.empty?
64
64
  # special case: empty array literal '{}'
65
- output.push(operator.new)
65
+ output.push(operation.new)
66
66
  else
67
67
  fail! :invalid_statement if output_size < args_size
68
68
  args = Array.new(args_size) { output.pop }.reverse
69
- output.push operator.new(*args)
69
+ output.push operation.new(*args)
70
70
  end
71
71
 
72
- if operator.respond_to?(:callback) && !operator.callback.nil?
73
- operator.callback.call(args)
72
+ if operation.respond_to?(:callback) && !operation.callback.nil?
73
+ operation.callback.call(args)
74
74
  end
75
75
  rescue ::ArgumentError => e
76
- raise Dentaku::ArgumentError, e.message
76
+ raise if e.is_a?(Dentaku::ArgumentError)
77
+ raise Dentaku::ArgumentError.for(:invalid_value), e.message
77
78
  rescue NodeError => e
78
- fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
79
+ raise ParseError.for(:node_invalid, operation: operation, operand: e.operand, expected: e.expected, actual: e.actual), e.message
79
80
  end
80
81
 
81
82
  def parse
@@ -190,8 +191,11 @@ module Dentaku
190
191
  operations.push(AST::CaseConditional)
191
192
  consume(2)
192
193
  arities[-1] += 1
193
- elsif operations.last == AST::Case
194
- # First WHEN: finalize switch variable expression.
194
+ elsif operations[token_index].nil? ||
195
+ (operations[token_index] != AST::CaseWhen && operations[token_index] < AST::Operation)
196
+ # First WHEN: fold any operators pending from an unparenthesized
197
+ # switch expression, then finalize the switch variable.
198
+ consume_until(AST::Case)
195
199
  operations.push(AST::CaseSwitchVariable)
196
200
  consume
197
201
  end
@@ -268,7 +272,7 @@ module Dentaku
268
272
  when :close
269
273
  consume while operations.any? && operations.last != AST::Grouping
270
274
  lparen = operations.pop
271
- fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
275
+ fail! :unbalanced_parenthesis, token: token unless lparen == AST::Grouping
272
276
  if operations.last && operations.last < AST::Function
273
277
  consume(arities.pop.succ)
274
278
  end
@@ -284,35 +288,7 @@ module Dentaku
284
288
  end
285
289
 
286
290
  def fail!(reason, **meta)
287
- message =
288
- case reason
289
- when :node_invalid
290
- "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
291
- when :too_few_operands
292
- "#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
293
- when :too_many_operands
294
- "#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
295
- when :undefined_function
296
- "Undefined function #{meta.fetch(:function_name)}"
297
- when :unprocessed_token
298
- "Unprocessed token #{meta.fetch(:token_name)}"
299
- when :unknown_case_token
300
- "Unknown case token #{meta.fetch(:token_name)}"
301
- when :unbalanced_bracket
302
- "Unbalanced bracket"
303
- when :unbalanced_parenthesis
304
- "Unbalanced parenthesis"
305
- when :unknown_grouping_token
306
- "Unknown grouping token #{meta.fetch(:token_name)}"
307
- when :not_implemented_token_category
308
- "Not implemented for tokens of category #{meta.fetch(:token_category)}"
309
- when :invalid_statement
310
- "Invalid statement"
311
- else
312
- raise ::ArgumentError, "Unhandled #{reason}"
313
- end
314
-
315
- raise ParseError.for(reason, **meta), message
291
+ raise ParseError.for(reason, **meta)
316
292
  end
317
293
  end
318
294
  end
@@ -93,46 +93,40 @@ module Dentaku
93
93
  @values.empty? || @values.key?(value)
94
94
  end
95
95
 
96
- def self.datetime; new(:datetime); end
97
- def self.numeric; new(:numeric); end
98
- def self.string; new(:string); end
99
- def self.logical; new(:logical); end
96
+ def self.datetime = new(:datetime)
97
+ def self.numeric = new(:numeric)
98
+ def self.string = new(:string)
99
+ def self.logical = new(:logical)
100
100
  def self.value
101
101
  new(:datetime) | new(:numeric) | new(:string) | new(:logical)
102
102
  end
103
103
 
104
- def self.addsub; new(:operator, [:add, :subtract]); end
105
- def self.subtract; new(:operator, :subtract); end
106
- def self.anchored_minus; new(:operator, :subtract).caret; end
107
- def self.muldiv; new(:operator, [:multiply, :divide]); end
108
- def self.pow; new(:operator, :pow); end
109
- def self.mod; new(:operator, :mod); end
110
- def self.combinator; new(:combinator); end
111
-
112
- def self.comparator; new(:comparator); end
113
- def self.comp_gt; new(:comparator, [:gt, :ge]); end
114
- def self.comp_lt; new(:comparator, [:lt, :le]); end
115
-
116
- def self.open; new(:grouping, :open); end
117
- def self.close; new(:grouping, :close); end
118
- def self.comma; new(:grouping, :comma); end
119
- def self.non_group; new(:grouping).invert; end
120
- def self.non_group_star; new(:grouping).invert.star; end
121
- def self.non_close_plus; new(:grouping, :close).invert.plus; end
122
- def self.arguments; (value | comma).plus; end
123
-
124
- def self.if; new(:function, :if); end
125
- def self.round; new(:function, :round); end
126
- def self.roundup; new(:function, :roundup); end
127
- def self.rounddown; new(:function, :rounddown); end
128
- def self.not; new(:function, :not); end
129
-
130
- def self.method_missing(name, *args, &block)
131
- new(:function, name)
132
- end
133
-
134
- def self.respond_to_missing?(name, include_priv)
135
- true
136
- end
104
+ def self.addsub = new(:operator, [:add, :subtract])
105
+ def self.subtract = new(:operator, :subtract)
106
+ def self.anchored_minus = new(:operator, :subtract).caret
107
+ def self.muldiv = new(:operator, [:multiply, :divide])
108
+ def self.pow = new(:operator, :pow)
109
+ def self.mod = new(:operator, :mod)
110
+ def self.combinator = new(:combinator)
111
+
112
+ def self.comparator = new(:comparator)
113
+ def self.comp_gt = new(:comparator, [:gt, :ge])
114
+ def self.comp_lt = new(:comparator, [:lt, :le])
115
+
116
+ def self.open = new(:grouping, :open)
117
+ def self.close = new(:grouping, :close)
118
+ def self.comma = new(:grouping, :comma)
119
+ def self.non_group = new(:grouping).invert
120
+ def self.non_group_star = new(:grouping).invert.star
121
+ def self.non_close_plus = new(:grouping, :close).invert.plus
122
+ def self.arguments = (value | comma).plus
123
+
124
+ def self.function(name) = new(:function, name)
125
+
126
+ def self.if = function(:if)
127
+ def self.round = function(:round)
128
+ def self.roundup = function(:roundup)
129
+ def self.rounddown = function(:rounddown)
130
+ def self.not = function(:not)
137
131
  end
138
132
  end
@@ -6,7 +6,7 @@ module Dentaku
6
6
 
7
7
  def self.function_token_matchers(function_name, *symbols)
8
8
  token_matchers(:open, *symbols, :close).unshift(
9
- TokenMatcher.send(function_name)
9
+ TokenMatcher.function(function_name)
10
10
  )
11
11
  end
12
12
 
@@ -16,9 +16,9 @@ module Dentaku
16
16
  :comparator, :comp_gt, :comp_lt, :open, :close, :comma,
17
17
  :non_close_plus, :non_group, :non_group_star, :arguments,
18
18
  :logical, :combinator, :if, :round, :roundup, :rounddown, :not,
19
- :anchored_minus, :math_neg_pow, :math_neg_mul
19
+ :anchored_minus
20
20
  ].each_with_object({}) do |name, matchers|
21
- matchers[name] = TokenMatcher.send(name)
21
+ matchers[name] = TokenMatcher.public_send(name)
22
22
  end
23
23
 
24
24
  @matchers.fetch(symbol) do
@@ -94,21 +94,7 @@ module Dentaku
94
94
  end
95
95
 
96
96
  def fail!(reason, **meta)
97
- message =
98
- case reason
99
- when :parse_error
100
- "parse error at: '#{meta.fetch(:at)}'"
101
- when :too_many_opening_parentheses
102
- "too many opening parentheses"
103
- when :too_many_closing_parentheses
104
- "too many closing parentheses"
105
- when :unexpected_zero_width_match
106
- "unexpected zero-width match (:#{meta.fetch(:category)}) at '#{meta.fetch(:at)}'"
107
- else
108
- raise ::ArgumentError, "Unhandled #{reason}"
109
- end
110
-
111
- raise TokenizerError.for(reason, **meta), message
97
+ raise TokenizerError.for(reason, **meta)
112
98
  end
113
99
  end
114
100
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.8"
2
+ VERSION = "4.0.1"
3
3
  end