dentaku 3.5.1 → 3.5.3

Sign up to get free protection for your applications and to get access to all the features.
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