dentaku 3.5.1 → 3.5.3

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +1 -1
  4. data/lib/dentaku/ast/arithmetic.rb +34 -12
  5. data/lib/dentaku/ast/comparators.rb +1 -2
  6. data/lib/dentaku/ast/function_registry.rb +10 -1
  7. data/lib/dentaku/ast/functions/abs.rb +5 -0
  8. data/lib/dentaku/ast/functions/avg.rb +1 -1
  9. data/lib/dentaku/ast/functions/enum.rb +5 -4
  10. data/lib/dentaku/ast/functions/if.rb +3 -7
  11. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  12. data/lib/dentaku/ast/functions/reduce.rb +61 -0
  13. data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
  14. data/lib/dentaku/ast.rb +4 -1
  15. data/lib/dentaku/calculator.rb +17 -10
  16. data/lib/dentaku/date_arithmetic.rb +8 -2
  17. data/lib/dentaku/exceptions.rb +17 -3
  18. data/lib/dentaku/parser.rb +20 -9
  19. data/lib/dentaku/print_visitor.rb +16 -5
  20. data/lib/dentaku/token_scanner.rb +12 -3
  21. data/lib/dentaku/tokenizer.rb +7 -3
  22. data/lib/dentaku/version.rb +1 -1
  23. data/lib/dentaku/visitor/infix.rb +4 -0
  24. data/spec/ast/abs_spec.rb +26 -0
  25. data/spec/ast/addition_spec.rb +4 -4
  26. data/spec/ast/all_spec.rb +1 -1
  27. data/spec/ast/any_spec.rb +1 -1
  28. data/spec/ast/arithmetic_spec.rb +61 -12
  29. data/spec/ast/avg_spec.rb +5 -0
  30. data/spec/ast/division_spec.rb +25 -0
  31. data/spec/ast/filter_spec.rb +1 -1
  32. data/spec/ast/intercept_spec.rb +30 -0
  33. data/spec/ast/map_spec.rb +1 -1
  34. data/spec/ast/pluck_spec.rb +1 -1
  35. data/spec/ast/reduce_spec.rb +22 -0
  36. data/spec/bulk_expression_solver_spec.rb +17 -0
  37. data/spec/calculator_spec.rb +99 -17
  38. data/spec/external_function_spec.rb +89 -18
  39. data/spec/parser_spec.rb +3 -0
  40. data/spec/print_visitor_spec.rb +6 -0
  41. data/spec/tokenizer_spec.rb +6 -4
  42. data/spec/visitor/infix_spec.rb +22 -1
  43. data/spec/visitor_spec.rb +2 -1
  44. metadata +12 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5592654ee45adeb24167b584374fb2d69bc67d1db4d5f4ac99050130f187f8f
4
- data.tar.gz: 31d3952b08887ae934661f4c0ecc5c298b2f36f0f8365cc1503495eb044eb558
3
+ metadata.gz: a51418767c413ccded1f235a56d34866005edf21cbc0d4cf5848d4cff4bc801f
4
+ data.tar.gz: 2009c09a76a5cc0b85cf04769c93f0d372cc8613f410a18401de53ca268b134d
5
5
  SHA512:
6
- metadata.gz: d8e0d003f897e06173c91b200e62d9fed12ec3bacfe0f2ecc3f3705a1cbf914d1705948b79962f5ac6a5397138ff89c2123aa5c3753963f0046a88280b29825b
7
- data.tar.gz: 5f48d3b8fef4e56ed308e717da88937bef508e47dfca4c3e16610aff7523f11405e53e2b55d00c53b5eca8efbe7be64df0d0d12e7ddafbc61099cde08bf92a53
6
+ metadata.gz: 6cfeabc676fa63016096d2f3c291d60a68ce247fe7c75ba195cdc2968c28af85de9ab9eeaaa9e88749cbf7920a120b1e6fe4d78464b98f9c825180d42aafc4f1
7
+ data.tar.gz: 42863ad11afff2dc72dfe906d1fdc1c07821f4391dcb7fc67921d862c736350be15a6671beac9c16de9e1345f17869e0de40bb131bc1fcfea70190538274bd53
data/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Change Log
2
2
 
3
+ ## [Unreleased]
4
+ - add support for empty array literals
5
+ - add support for quoted identifiers
6
+ - add REDUCE function
7
+ - add INTERCEPT function
8
+ - improve date/time parsing an arithmetic
9
+ - improve custom class arithmetic
10
+ - fix IF dependency
11
+
12
+ ## [v3.5.2]
13
+ - add ABS function
14
+ - add array support for AST visitors
15
+ - add support for function callbacks
16
+ - improve support for date / time values
17
+ - improve error messaging for invalid arity
18
+ - improve AVG function accuracy
19
+ - validate enum arguments at parse time
20
+ - support adding multiple functions at once to global registry
21
+ - fix bug in print visitor precedence checking
22
+ - fix handling of Math::DomainError
23
+ - fix invalid cast
24
+
3
25
  ## [v3.5.1]
4
26
  - add bitwise shift left and shift right operators
5
27
  - improve numeric conversions
@@ -231,7 +253,8 @@
231
253
  ## [v0.1.0] 2012-01-20
232
254
  - initial release
233
255
 
234
- [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.1...HEAD
256
+ [Unreleased]: https://github.com/rubysolo/dentaku/compare/v3.5.2...HEAD
257
+ [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
235
258
  [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
236
259
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
237
260
  [v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
data/README.md CHANGED
@@ -145,7 +145,7 @@ Comparison: `<`, `>`, `<=`, `>=`, `<>`, `!=`, `=`,
145
145
 
146
146
  Logic: `IF`, `AND`, `OR`, `XOR`, `NOT`, `SWITCH`
147
147
 
148
- Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`
148
+ Numeric: `MIN`, `MAX`, `SUM`, `AVG`, `COUNT`, `ROUND`, `ROUNDDOWN`, `ROUNDUP`, `ABS`, `INTERCEPT`
149
149
 
150
150
  Selections: `CASE` (syntax see [spec](https://github.com/rubysolo/dentaku/blob/master/spec/calculator_spec.rb#L593))
151
151
 
@@ -6,6 +6,9 @@ require 'bigdecimal/util'
6
6
  module Dentaku
7
7
  module AST
8
8
  class Arithmetic < Operation
9
+ DECIMAL = /\A-?\d*\.\d+\z/.freeze
10
+ INTEGER = /\A-?\d+\z/.freeze
11
+
9
12
  def initialize(*)
10
13
  super
11
14
 
@@ -29,8 +32,14 @@ module Dentaku
29
32
  end
30
33
 
31
34
  def value(context = {})
32
- l = cast(left.value(context))
33
- r = cast(right.value(context))
35
+ calculate(left.value(context), right.value(context))
36
+ end
37
+
38
+ private
39
+
40
+ def calculate(left_value, right_value)
41
+ l = cast(left_value)
42
+ r = cast(right_value)
34
43
 
35
44
  l.public_send(operator, r)
36
45
  rescue ::TypeError => e
@@ -38,8 +47,6 @@ module Dentaku
38
47
  raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
39
48
  end
40
49
 
41
- private
42
-
43
50
  def cast(val)
44
51
  validate_value(val)
45
52
  numeric(val)
@@ -47,14 +54,23 @@ module Dentaku
47
54
 
48
55
  def numeric(val)
49
56
  case val.to_s
50
- when /\A\d*\.\d+\z/ then decimal(val)
51
- when /\A-?\d+\z/ then val.to_i
57
+ when DECIMAL then decimal(val)
58
+ when INTEGER then val.to_i
52
59
  else val
53
60
  end
54
61
  end
55
62
 
56
63
  def decimal(val)
57
64
  BigDecimal(val.to_s, Float::DIG + 1)
65
+ rescue # return as is, in case value can't be coerced to big decimal
66
+ val
67
+ end
68
+
69
+ def datetime?(val)
70
+ # val is a Date, Time, or DateTime
71
+ return true if val.respond_to?(:strftime)
72
+
73
+ val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
58
74
  end
59
75
 
60
76
  def valid_node?(node)
@@ -102,10 +118,13 @@ module Dentaku
102
118
  end
103
119
 
104
120
  def value(context = {})
105
- if left.type == :datetime
106
- Dentaku::DateArithmetic.new(left.value(context)).add(right.value(context))
121
+ left_value = left.value(context)
122
+ right_value = right.value(context)
123
+
124
+ if left.type == :datetime || datetime?(left_value)
125
+ Dentaku::DateArithmetic.new(left_value).add(right_value)
107
126
  else
108
- super
127
+ calculate(left_value, right_value)
109
128
  end
110
129
  end
111
130
  end
@@ -120,10 +139,13 @@ module Dentaku
120
139
  end
121
140
 
122
141
  def value(context = {})
123
- if left.type == :datetime
124
- Dentaku::DateArithmetic.new(left.value(context)).sub(right.value(context))
142
+ left_value = left.value(context)
143
+ right_value = right.value(context)
144
+
145
+ if left.type == :datetime || datetime?(left_value)
146
+ Dentaku::DateArithmetic.new(left_value).sub(right_value)
125
147
  else
126
- super
148
+ calculate(left_value, right_value)
127
149
  end
128
150
  end
129
151
  end
@@ -28,8 +28,7 @@ module Dentaku
28
28
 
29
29
  def cast(val)
30
30
  return val unless val.is_a?(::String)
31
- return val if val.empty?
32
- return val unless val.match?(/\A-?\d*(\.\d+)?\z/)
31
+ return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
33
32
 
34
33
  v = BigDecimal(val, Float::DIG + 1)
35
34
  v = v.to_i if v.frac.zero?
@@ -8,7 +8,7 @@ module Dentaku
8
8
  nil
9
9
  end
10
10
 
11
- def register(name, type, implementation)
11
+ def register(name, type, implementation, callback = nil)
12
12
  function = Class.new(Function) do
13
13
  def self.name=(name)
14
14
  @name = name
@@ -34,6 +34,14 @@ module Dentaku
34
34
  @type
35
35
  end
36
36
 
37
+ def self.callback=(callback)
38
+ @callback = callback
39
+ end
40
+
41
+ def self.callback
42
+ @callback
43
+ end
44
+
37
45
  def self.arity
38
46
  @implementation.arity < 0 ? nil : @implementation.arity
39
47
  end
@@ -61,6 +69,7 @@ module Dentaku
61
69
  function.name = name
62
70
  function.type = type
63
71
  function.implementation = implementation
72
+ function.callback = callback
64
73
 
65
74
  self[function_name(name)] = function
66
75
  end
@@ -0,0 +1,5 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:abs, :numeric, lambda { |numeric|
4
+ Dentaku::AST::Function.numeric(numeric).abs
5
+ })
@@ -9,5 +9,5 @@ Dentaku::AST::Function.register(:avg, :numeric, ->(*args) {
9
9
  ), 'AVG() requires at least one argument'
10
10
  end
11
11
 
12
- flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / flatten_args.length
12
+ flatten_args.map { |arg| Dentaku::AST::Function.numeric(arg) }.reduce(0, :+) / BigDecimal(flatten_args.length)
13
13
  })
@@ -12,9 +12,12 @@ module Dentaku
12
12
  3
13
13
  end
14
14
 
15
- def dependencies(context = {})
15
+ def initialize(*args)
16
+ super
16
17
  validate_identifier(@args[1])
18
+ end
17
19
 
20
+ def dependencies(context = {})
18
21
  collection = @args[0]
19
22
  item_identifier = @args[1].identifier
20
23
  expression = @args[2]
@@ -28,9 +31,7 @@ module Dentaku
28
31
  end
29
32
 
30
33
  def validate_identifier(arg, message = "#{name}() requires second argument to be an identifier")
31
- unless arg.is_a?(Identifier)
32
- raise ArgumentError.for(:incompatible_type, value: arg, for: Identifier), message
33
- end
34
+ raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
34
35
  end
35
36
  end
36
37
  end
@@ -36,13 +36,9 @@ module Dentaku
36
36
  end
37
37
 
38
38
  def dependencies(context = {})
39
- deps = predicate.dependencies(context)
40
-
41
- if deps.empty?
42
- predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
43
- else
44
- (deps + left.dependencies(context) + right.dependencies(context)).uniq
45
- end
39
+ predicate.value(context) ? left.dependencies(context) : right.dependencies(context)
40
+ rescue Dentaku::Error, Dentaku::ArgumentError, Dentaku::ZeroDivisionError
41
+ args.flat_map { |arg| arg.dependencies(context) }.uniq
46
42
  end
47
43
  end
48
44
  end
@@ -0,0 +1,33 @@
1
+ require_relative '../function'
2
+
3
+ Dentaku::AST::Function.register(:intercept, :list, ->(*args) {
4
+ if args.length != 2
5
+ raise Dentaku::ArgumentError.for(
6
+ :wrong_number_of_arguments,
7
+ function_name: 'INTERCEPT()', exact: 2, given: args.length
8
+ ), 'INTERCEPT() requires exactly two arrays of numbers'
9
+ end
10
+
11
+ x_values, y_values = args
12
+ if !x_values.is_a?(Array) || !y_values.is_a?(Array) || x_values.length != y_values.length
13
+ raise Dentaku::ArgumentError.for(
14
+ :invalid_value,
15
+ function_name: 'INTERCEPT()'
16
+ ), 'INTERCEPT() requires arrays of equal length'
17
+ end
18
+
19
+ n = x_values.length.to_f
20
+ x_values = x_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
21
+ y_values = y_values.map { |arg| Dentaku::AST::Function.numeric(arg) }
22
+
23
+ x_avg = x_values.sum / n
24
+ y_avg = y_values.sum / n
25
+
26
+ xy_sum = x_values.zip(y_values).map { |x, y| (x_avg - x) * (y_avg - y) }.sum
27
+ x_square_sum = x_values.map { |x| (x_avg - x)**2 }.sum
28
+
29
+ slope = xy_sum / x_square_sum
30
+ intercept = x_values.zip(y_values).map { |x, y| y - slope * x }.sum / n
31
+
32
+ BigDecimal(intercept, Float::DIG + 1)
33
+ })
@@ -0,0 +1,61 @@
1
+ require_relative '../function'
2
+ require_relative '../../exceptions'
3
+
4
+ module Dentaku
5
+ module AST
6
+ class Reduce < Function
7
+ def self.min_param_count
8
+ 4
9
+ end
10
+
11
+ def self.max_param_count
12
+ 5
13
+ end
14
+
15
+ def initialize(*args)
16
+ super
17
+
18
+ validate_identifier(@args[1], 'second')
19
+ validate_identifier(@args[2], 'third')
20
+ end
21
+
22
+ def dependencies(context = {})
23
+ collection = @args[0]
24
+ memo_identifier = @args[1].identifier
25
+ item_identifier = @args[2].identifier
26
+ expression = @args[3]
27
+
28
+ collection_deps = collection.dependencies(context)
29
+ expression_deps = expression.dependencies(context).reject do |i|
30
+ i == memo_identifier || i.start_with?("#{memo_identifier}.") ||
31
+ i == item_identifier || i.start_with?("#{item_identifier}.")
32
+ end
33
+ inital_value_deps = @args[4] ? @args[4].dependencies(context) : []
34
+
35
+ collection_deps + expression_deps + inital_value_deps
36
+ end
37
+
38
+ def value(context = {})
39
+ collection = Array(@args[0].value(context))
40
+ memo_identifier = @args[1].identifier
41
+ item_identifier = @args[2].identifier
42
+ expression = @args[3]
43
+ initial_value = @args[4] && @args[4].value(context)
44
+
45
+ collection.reduce(initial_value) do |memo, item|
46
+ expression.value(
47
+ context.merge(
48
+ FlatHash.from_hash_with_intermediates(memo_identifier => memo, item_identifier => item)
49
+ )
50
+ )
51
+ end
52
+ end
53
+
54
+ def validate_identifier(arg, position, message = "#{name}() requires #{position} argument to be an identifier")
55
+ raise ParseError.for(:node_invalid), message unless arg.is_a?(Identifier)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ Dentaku::AST::Function.register_class(:reduce, Dentaku::AST::Reduce)
@@ -34,6 +34,8 @@ module Dentaku
34
34
 
35
35
  def self.call(*args)
36
36
  @implementation.call(*args)
37
+ rescue Math::DomainError => _e
38
+ raise Dentaku::MathDomainError.new(name, args)
37
39
  end
38
40
 
39
41
  def value(context = {})
data/lib/dentaku/ast.rb CHANGED
@@ -15,6 +15,7 @@ require_relative './ast/array'
15
15
  require_relative './ast/grouping'
16
16
  require_relative './ast/case'
17
17
  require_relative './ast/function_registry'
18
+ require_relative './ast/functions/abs'
18
19
  require_relative './ast/functions/all'
19
20
  require_relative './ast/functions/and'
20
21
  require_relative './ast/functions/any'
@@ -23,12 +24,14 @@ require_relative './ast/functions/count'
23
24
  require_relative './ast/functions/duration'
24
25
  require_relative './ast/functions/filter'
25
26
  require_relative './ast/functions/if'
27
+ require_relative './ast/functions/intercept'
26
28
  require_relative './ast/functions/map'
27
29
  require_relative './ast/functions/max'
28
30
  require_relative './ast/functions/min'
29
31
  require_relative './ast/functions/not'
30
32
  require_relative './ast/functions/or'
31
33
  require_relative './ast/functions/pluck'
34
+ require_relative './ast/functions/reduce'
32
35
  require_relative './ast/functions/round'
33
36
  require_relative './ast/functions/rounddown'
34
37
  require_relative './ast/functions/roundup'
@@ -36,4 +39,4 @@ require_relative './ast/functions/ruby_math'
36
39
  require_relative './ast/functions/string_functions'
37
40
  require_relative './ast/functions/sum'
38
41
  require_relative './ast/functions/switch'
39
- require_relative './ast/functions/xor'
42
+ require_relative './ast/functions/xor'
@@ -10,7 +10,7 @@ module Dentaku
10
10
  class Calculator
11
11
  include StringCasing
12
12
  attr_reader :result, :memory, :tokenizer, :case_sensitive, :aliases,
13
- :nested_data_support, :ast_cache
13
+ :nested_data_support, :ast_cache, :raw_date_literals
14
14
 
15
15
  def initialize(options = {})
16
16
  clear
@@ -19,22 +19,28 @@ module Dentaku
19
19
  @aliases = options.delete(:aliases) || Dentaku.aliases
20
20
  @nested_data_support = options.fetch(:nested_data_support, true)
21
21
  options.delete(:nested_data_support)
22
+ @raw_date_literals = options.fetch(:raw_date_literals, true)
23
+ options.delete(:raw_date_literals)
22
24
  @ast_cache = options
23
25
  @disable_ast_cache = false
24
26
  @function_registry = Dentaku::AST::FunctionRegistry.new
25
27
  end
26
28
 
27
- def self.add_function(name, type, body)
28
- Dentaku::AST::FunctionRegistry.default.register(name, type, body)
29
+ def self.add_function(name, type, body, callback = nil)
30
+ Dentaku::AST::FunctionRegistry.default.register(name, type, body, callback)
29
31
  end
30
32
 
31
- def add_function(name, type, body)
32
- @function_registry.register(name, type, body)
33
+ def self.add_functions(functions)
34
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
35
+ end
36
+
37
+ def add_function(name, type, body, callback = nil)
38
+ @function_registry.register(name, type, body, callback)
33
39
  self
34
40
  end
35
41
 
36
- def add_functions(fns)
37
- fns.each { |(name, type, body)| add_function(name, type, body) }
42
+ def add_functions(functions)
43
+ functions.each { |(name, type, body, callback)| add_function(name, type, body, callback) }
38
44
  self
39
45
  end
40
46
 
@@ -57,8 +63,7 @@ module Dentaku
57
63
  } if expression.is_a? Array
58
64
 
59
65
  store(data) do
60
- node = expression
61
- node = ast(node) unless node.is_a?(AST::Node)
66
+ node = ast(expression)
62
67
  unbound = node.dependencies(memory)
63
68
  unless unbound.empty?
64
69
  raise UnboundVariableError.new(unbound),
@@ -90,13 +95,15 @@ module Dentaku
90
95
  end
91
96
 
92
97
  def ast(expression)
98
+ return expression if expression.is_a?(AST::Node)
93
99
  return expression.map { |e| ast(e) } if expression.is_a? Array
94
100
 
95
101
  @ast_cache.fetch(expression) {
96
102
  options = {
103
+ aliases: aliases,
97
104
  case_sensitive: case_sensitive,
98
105
  function_registry: @function_registry,
99
- aliases: aliases
106
+ raw_date_literals: raw_date_literals
100
107
  }
101
108
 
102
109
  tokens = tokenizer.tokenize(expression, options)
@@ -1,7 +1,11 @@
1
1
  module Dentaku
2
2
  class DateArithmetic
3
3
  def initialize(date)
4
- @base = date
4
+ if date.respond_to?(:strftime)
5
+ @base = date
6
+ else
7
+ @base = Time.parse(date).to_datetime
8
+ end
5
9
  end
6
10
 
7
11
  def add(duration)
@@ -25,7 +29,7 @@ module Dentaku
25
29
 
26
30
  def sub(duration)
27
31
  case duration
28
- when DateTime, Numeric
32
+ when Date, DateTime, Numeric
29
33
  @base - duration
30
34
  when Dentaku::AST::Duration::Value
31
35
  case duration.unit
@@ -36,6 +40,8 @@ module Dentaku
36
40
  when :day
37
41
  @base - duration.value
38
42
  end
43
+ when Dentaku::TokenScanner::DATE_TIME_REGEXP
44
+ @base - Time.parse(duration).to_datetime
39
45
  else
40
46
  raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
41
47
  "'#{duration || duration.class}' is not coercible for date arithmetic"
@@ -11,6 +11,15 @@ module Dentaku
11
11
  end
12
12
  end
13
13
 
14
+ class MathDomainError < Error
15
+ attr_reader :function_name, :args
16
+
17
+ def initialize(function_name, args)
18
+ @function_name = function_name
19
+ @args = args
20
+ end
21
+ end
22
+
14
23
  class NodeError < Error
15
24
  attr_reader :child, :expect, :actual
16
25
 
@@ -58,7 +67,9 @@ module Dentaku
58
67
  private_class_method :new
59
68
 
60
69
  VALID_REASONS = %i[
61
- parse_error too_many_opening_parentheses too_many_closing_parentheses
70
+ parse_error
71
+ too_many_closing_parentheses
72
+ too_many_opening_parentheses
62
73
  unexpected_zero_width_match
63
74
  ].freeze
64
75
 
@@ -83,8 +94,11 @@ module Dentaku
83
94
  private_class_method :new
84
95
 
85
96
  VALID_REASONS = %i[
86
- invalid_operator invalid_value too_few_arguments
87
- too_much_arguments incompatible_type
97
+ incompatible_type
98
+ invalid_operator
99
+ invalid_value
100
+ too_few_arguments
101
+ wrong_number_of_arguments
88
102
  ].freeze
89
103
 
90
104
  def self.for(reason, **meta)
@@ -45,22 +45,33 @@ module Dentaku
45
45
 
46
46
  operator.peek(output)
47
47
 
48
+ output_size = output.length
48
49
  args_size = operator.arity || count
49
50
  min_size = operator.arity || operator.min_param_count || count
50
51
  max_size = operator.arity || operator.max_param_count || count
51
52
 
52
- if output.length < min_size || args_size < min_size
53
- fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
53
+ if output_size < min_size || args_size < min_size
54
+ expect = min_size == max_size ? min_size : min_size..max_size
55
+ fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
54
56
  end
55
57
 
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
58
+ if output_size > max_size && operations.empty? || args_size > max_size
59
+ expect = min_size == max_size ? min_size : min_size..max_size
60
+ fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
58
61
  end
59
62
 
60
- fail! :invalid_statement if output.size < args_size
61
- args = Array.new(args_size) { output.pop }.reverse
63
+ if operator == AST::Array && output.empty?
64
+ output.push(operator.new())
65
+ else
66
+ fail! :invalid_statement if output_size < args_size
67
+ args = Array.new(args_size) { output.pop }.reverse
62
68
 
63
- output.push operator.new(*args)
69
+ output.push operator.new(*args)
70
+ end
71
+
72
+ if operator.respond_to?(:callback) && !operator.callback.nil?
73
+ operator.callback.call(args)
74
+ end
64
75
  rescue ::ArgumentError => e
65
76
  raise Dentaku::ArgumentError, e.message
66
77
  rescue NodeError => e
@@ -320,9 +331,9 @@ module Dentaku
320
331
  when :node_invalid
321
332
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
322
333
  when :too_few_operands
323
- "#{meta.fetch(:operator)} has too few operands"
334
+ "#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
324
335
  when :too_many_operands
325
- "#{meta.fetch(:operator)} has too many operands"
336
+ "#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
326
337
  when :undefined_function
327
338
  "Undefined function #{meta.fetch(:function_name)}"
328
339
  when :unprocessed_token
@@ -7,24 +7,31 @@ module Dentaku
7
7
 
8
8
  def visit_operation(node)
9
9
  if node.left
10
- visit_operand(node.left, node.class.precedence, suffix: " ")
10
+ visit_operand(node.left, node.class.precedence, suffix: " ", dir: :left)
11
11
  end
12
12
 
13
13
  @output << node.display_operator
14
14
 
15
15
  if node.right
16
- visit_operand(node.right, node.class.precedence, prefix: " ")
16
+ visit_operand(node.right, node.class.precedence, prefix: " ", dir: :right)
17
17
  end
18
18
  end
19
19
 
20
- def visit_operand(node, precedence, prefix: "", suffix: "")
20
+ def visit_operand(node, precedence, prefix: "", suffix: "", dir: :none)
21
21
  @output << prefix
22
- @output << "(" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
22
+ @output << "(" if should_output?(node, precedence, dir == :right)
23
23
  node.accept(self)
24
- @output << ")" if node.is_a?(Dentaku::AST::Operation) && node.class.precedence < precedence
24
+ @output << ")" if should_output?(node, precedence, dir == :right)
25
25
  @output << suffix
26
26
  end
27
27
 
28
+ def should_output?(node, precedence, output_on_equal)
29
+ return false unless node.is_a?(Dentaku::AST::Operation)
30
+
31
+ target_precedence = node.class.precedence
32
+ target_precedence < precedence || (output_on_equal && target_precedence == precedence)
33
+ end
34
+
28
35
  def visit_function(node)
29
36
  @output << node.name
30
37
  @output << "("
@@ -94,6 +101,10 @@ module Dentaku
94
101
  @output << "NULL"
95
102
  end
96
103
 
104
+ def visit_array(node)
105
+ @output << node.value.to_s
106
+ end
107
+
97
108
  def to_s
98
109
  @output
99
110
  end