dentaku 3.5.7 → 4.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +2 -6
  3. data/.github/workflows/rubocop.yml +1 -1
  4. data/.rubocop.yml +1 -1
  5. data/CHANGELOG.md +91 -0
  6. data/Gemfile +14 -1
  7. data/README.md +87 -0
  8. data/dentaku.gemspec +14 -14
  9. data/lib/dentaku/ast/access.rb +18 -1
  10. data/lib/dentaku/ast/arithmetic.rb +5 -17
  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 +49 -4
  20. data/lib/dentaku/ast/comparators.rb +3 -7
  21. data/lib/dentaku/ast/function.rb +12 -13
  22. data/lib/dentaku/ast/function_registry.rb +11 -2
  23. data/lib/dentaku/ast/functions/abs.rb +1 -1
  24. data/lib/dentaku/ast/functions/and.rb +4 -4
  25. data/lib/dentaku/ast/functions/avg.rb +3 -3
  26. data/lib/dentaku/ast/functions/duration.rb +1 -1
  27. data/lib/dentaku/ast/functions/if.rb +9 -1
  28. data/lib/dentaku/ast/functions/intercept.rb +4 -4
  29. data/lib/dentaku/ast/functions/max.rb +1 -1
  30. data/lib/dentaku/ast/functions/min.rb +1 -1
  31. data/lib/dentaku/ast/functions/mul.rb +3 -3
  32. data/lib/dentaku/ast/functions/or.rb +4 -4
  33. data/lib/dentaku/ast/functions/pluck.rb +1 -1
  34. data/lib/dentaku/ast/functions/round.rb +1 -1
  35. data/lib/dentaku/ast/functions/rounddown.rb +2 -2
  36. data/lib/dentaku/ast/functions/roundup.rb +2 -2
  37. data/lib/dentaku/ast/functions/ruby_math.rb +5 -2
  38. data/lib/dentaku/ast/functions/string_functions.rb +5 -5
  39. data/lib/dentaku/ast/functions/sum.rb +3 -3
  40. data/lib/dentaku/ast/functions/xor.rb +4 -4
  41. data/lib/dentaku/ast/grouping.rb +6 -0
  42. data/lib/dentaku/ast/negation.rb +5 -0
  43. data/lib/dentaku/ast/node.rb +25 -0
  44. data/lib/dentaku/ast/operation.rb +7 -0
  45. data/lib/dentaku/bulk_expression_solver.rb +22 -16
  46. data/lib/dentaku/calculator.rb +45 -17
  47. data/lib/dentaku/date_arithmetic.rb +2 -2
  48. data/lib/dentaku/dependency_resolver.rb +12 -5
  49. data/lib/dentaku/exceptions.rb +105 -13
  50. data/lib/dentaku/numeric_parser.rb +68 -0
  51. data/lib/dentaku/parser.rb +24 -48
  52. data/lib/dentaku/print_visitor.rb +1 -0
  53. data/lib/dentaku/token_matcher.rb +31 -37
  54. data/lib/dentaku/token_matchers.rb +3 -3
  55. data/lib/dentaku/token_scanner.rb +3 -4
  56. data/lib/dentaku/tokenizer.rb +2 -16
  57. data/lib/dentaku/version.rb +1 -1
  58. data/spec/ast/case_spec.rb +7 -0
  59. data/spec/ast/function_spec.rb +11 -25
  60. data/spec/bulk_expression_solver_spec.rb +48 -3
  61. data/spec/calculator_spec.rb +77 -0
  62. data/spec/dependency_resolver_spec.rb +14 -0
  63. data/spec/exceptions_spec.rb +87 -0
  64. data/spec/gemspec_spec.rb +10 -0
  65. data/spec/identifiers_spec.rb +66 -0
  66. data/spec/numeric_parser_spec.rb +66 -0
  67. data/spec/parser_spec.rb +35 -0
  68. data/spec/print_visitor_spec.rb +5 -0
  69. data/spec/tokenizer_spec.rb +6 -0
  70. data/spec/volatile_functions_spec.rb +121 -0
  71. metadata +27 -150
  72. 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
 
@@ -106,6 +114,20 @@ module Dentaku
106
114
  end
107
115
  end
108
116
 
117
+ # every identifier the expression could reference, regardless of
118
+ # branching: purely syntactic, ignores stored memory, and never
119
+ # evaluates guards or functions
120
+ def identifiers(expression)
121
+ case expression
122
+ when Dentaku::AST::Node
123
+ expression.dependencies(AST::Node::STATIC_CONTEXT).uniq
124
+ when Array
125
+ expression.flat_map { |e| identifiers(e) }.uniq
126
+ else
127
+ ast(expression).dependencies(AST::Node::STATIC_CONTEXT).uniq
128
+ end
129
+ end
130
+
109
131
  def ast(expression)
110
132
  return expression if expression.is_a?(AST::Node)
111
133
  return expression.map { |e| ast(e) } if expression.is_a? Array
@@ -186,7 +208,13 @@ module Dentaku
186
208
  end
187
209
 
188
210
  def cache_ast?
189
- Dentaku.cache_ast? && !@disable_ast_cache
211
+ return false if @disable_ast_cache
212
+
213
+ @cache_ast.nil? ? Dentaku.cache_ast? : @cache_ast
214
+ end
215
+
216
+ def cache_dependency_order?
217
+ @cache_dependency_order.nil? ? Dentaku.cache_dependency_order? : @cache_dependency_order
190
218
  end
191
219
  end
192
220
  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
@@ -5,13 +5,14 @@ module Dentaku
5
5
  include TSort
6
6
 
7
7
  def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
8
- self.new(vars_to_dependencies_hash).sort
8
+ self.new(vars_to_dependencies_hash, case_sensitive).sort
9
9
  end
10
10
 
11
- def initialize(vars_to_dependencies_hash)
12
- @key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
13
- # ensure variables are normalized strings
14
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
11
+ def initialize(vars_to_dependencies_hash, case_sensitive = false)
12
+ @case_sensitive = case_sensitive
13
+ @key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [normalized_name(k), k] }]
14
+ # normalize variables and their dependencies alike so child lookups match
15
+ @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [normalized_name(k), v.map { |d| normalized_name(d) }] }]
15
16
  end
16
17
 
17
18
  def sort
@@ -25,5 +26,11 @@ module Dentaku
25
26
  def tsort_each_child(node, &block)
26
27
  @vars_to_deps.fetch(node.to_s, []).each(&block)
27
28
  end
29
+
30
+ private
31
+
32
+ def normalized_name(variable_name)
33
+ @case_sensitive ? variable_name.to_s : variable_name.to_s.downcase
34
+ end
28
35
  end
29
36
  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
@@ -0,0 +1,68 @@
1
+ module Dentaku
2
+ module NumericParser
3
+ NUMERIC_PATTERN = '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b'.freeze
4
+ # e.g: [2, 1.2, .5, 1e10, 1.5E-10]
5
+ HEXADECIMAL_PATTERN = '(0x[0-9a-f]+)\b'.freeze
6
+ # e.g: [0x1A3F]
7
+
8
+ class << self
9
+ def match(string)
10
+ regex = %r{\A(-?(#{ NUMERIC_PATTERN }|#{ HEXADECIMAL_PATTERN }))\z}i
11
+ string.match(regex)
12
+ end
13
+
14
+ def match_numeric(string)
15
+ regex = %r{\A(#{ NUMERIC_PATTERN })\z}i
16
+ string.match(regex)
17
+ end
18
+
19
+ def match_hexadecimal(string)
20
+ regex = %r{\A(#{ HEXADECIMAL_PATTERN })\z}i
21
+ string.match(regex)
22
+ end
23
+
24
+ def parse_numeric_string(raw)
25
+ raw =~ /(\.|e|E)/ ? BigDecimal(raw, Float::DIG + 1) : raw.to_i
26
+ end
27
+
28
+ def parse_hexadecimal_string(raw)
29
+ raw[2..-1].to_i(16)
30
+ end
31
+
32
+ def parse_string(raw)
33
+ return nil unless !!match(raw)
34
+
35
+ is_negative = raw.start_with?('-')
36
+ number_part = is_negative ? raw[1..-1] : raw
37
+
38
+ value = if number_part =~ /\A0x/i
39
+ parse_hexadecimal_string(number_part)
40
+ else
41
+ parse_numeric_string(number_part)
42
+ end
43
+
44
+ is_negative ? -value : value
45
+ end
46
+
47
+ # @return [Numeric] where possible it returns an Integer otherwise a BigDecimal.
48
+ # An Exception will be raised if a value is passed that cannot be cast to a Number.
49
+ def ensure_numeric!(value)
50
+ return value if value.is_a?(::Numeric)
51
+
52
+ if value.is_a?(::String)
53
+ number = parse_string(value)
54
+ return number if number
55
+ end
56
+
57
+ raise Dentaku::ArgumentError.for(:incompatible_type, actual: value, expected: Numeric),
58
+ "'#{value || value.class}' is not coercible to numeric"
59
+ end
60
+
61
+ def ensure_numeric(value)
62
+ ensure_numeric!(value)
63
+ rescue Dentaku::ArgumentError
64
+ nil
65
+ end
66
+ end
67
+ end
68
+ end
@@ -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
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: false
1
2
  module Dentaku
2
3
  class PrintVisitor
3
4
  def initialize(node)
@@ -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
@@ -2,6 +2,7 @@ require 'bigdecimal'
2
2
  require 'time'
3
3
  require 'dentaku/string_casing'
4
4
  require 'dentaku/token'
5
+ require 'dentaku/numeric_parser'
5
6
 
6
7
  module Dentaku
7
8
  class TokenScanner
@@ -95,13 +96,11 @@ module Dentaku
95
96
  end
96
97
 
97
98
  def numeric
98
- new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
99
- raw =~ /(\.|e|E)/ ? BigDecimal(raw) : raw.to_i
100
- })
99
+ new(:numeric, NumericParser::NUMERIC_PATTERN, lambda { |raw| NumericParser.parse_numeric_string(raw) })
101
100
  end
102
101
 
103
102
  def hexadecimal
104
- new(:numeric, '(0x[0-9a-f]+)\b', lambda { |raw| raw[2..-1].to_i(16) })
103
+ new(:numeric, NumericParser::HEXADECIMAL_PATTERN, lambda { |raw| NumericParser.parse_hexadecimal_string(raw) })
105
104
  end
106
105
 
107
106
  def double_quoted_string