dentaku 3.5.0 → 3.5.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +2 -2
  4. data/lib/dentaku/ast/arithmetic.rb +46 -25
  5. data/lib/dentaku/ast/bitwise.rb +23 -6
  6. data/lib/dentaku/ast/comparators.rb +11 -2
  7. data/lib/dentaku/ast/function_registry.rb +10 -1
  8. data/lib/dentaku/ast/functions/abs.rb +5 -0
  9. data/lib/dentaku/ast/functions/avg.rb +1 -1
  10. data/lib/dentaku/ast/functions/enum.rb +5 -4
  11. data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
  12. data/lib/dentaku/ast.rb +1 -0
  13. data/lib/dentaku/bulk_expression_solver.rb +1 -5
  14. data/lib/dentaku/calculator.rb +15 -8
  15. data/lib/dentaku/date_arithmetic.rb +5 -1
  16. data/lib/dentaku/exceptions.rb +11 -2
  17. data/lib/dentaku/parser.rb +19 -7
  18. data/lib/dentaku/print_visitor.rb +16 -5
  19. data/lib/dentaku/token_scanner.rb +8 -4
  20. data/lib/dentaku/tokenizer.rb +7 -3
  21. data/lib/dentaku/version.rb +1 -1
  22. data/lib/dentaku/visitor/infix.rb +4 -0
  23. data/spec/ast/abs_spec.rb +26 -0
  24. data/spec/ast/all_spec.rb +1 -1
  25. data/spec/ast/any_spec.rb +1 -1
  26. data/spec/ast/arithmetic_spec.rb +20 -5
  27. data/spec/ast/avg_spec.rb +5 -0
  28. data/spec/ast/comparator_spec.rb +8 -0
  29. data/spec/ast/filter_spec.rb +1 -1
  30. data/spec/ast/map_spec.rb +1 -1
  31. data/spec/ast/or_spec.rb +1 -1
  32. data/spec/ast/pluck_spec.rb +1 -1
  33. data/spec/bulk_expression_solver_spec.rb +9 -0
  34. data/spec/calculator_spec.rb +70 -16
  35. data/spec/external_function_spec.rb +89 -18
  36. data/spec/print_visitor_spec.rb +6 -0
  37. data/spec/tokenizer_spec.rb +12 -0
  38. data/spec/visitor/infix_spec.rb +22 -1
  39. data/spec/visitor_spec.rb +3 -2
  40. metadata +6 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a0b093e29b178197c0f92c1f63ec56342716c1fa84a4d12a73a7d355d42bc76
4
- data.tar.gz: 480ccf9248568006227518363a0ef6350843007389f2e94ac5610ae62028e87e
3
+ metadata.gz: 0b10f7d9e6a9d200c283dcf077fd56e8f4abe4922c02e3095ba20dbb29f6b81c
4
+ data.tar.gz: add2d3bf7c462edefb9a4c52d79595d3a161e1d78046dbb1fe90e8aa9979a13b
5
5
  SHA512:
6
- metadata.gz: 904292b2d2fd834701fd18900d689b9125579d58421fc58aaad03cd75c0fb556eeaeb5635dcd034a3f29e9f70f5c8fe0370e605acefd599c3dd75eae64436fdd
7
- data.tar.gz: 3b6ed8763b9241e55e85f1a1e5a09d2652ec91eee21c080ca825029e04867b8fe65b128ddfc557cab3ef4fae76abb30b936f5971326355ea763081f90ba66638
6
+ metadata.gz: 48c2571ea61f8bb9f8a7a4483b8d588741f2c44c4fdc2d04ecd2a5c5b75a94f414b5772a3e1dca21898f8d2ed6488e54925c6a689bfd721315c0c8e0992991d7
7
+ data.tar.gz: b469e9c4a69c6083b93cda29ea8bf5bf4ad9c91e4a6362e5880768492e21ecd476a09eb858134b8e6c78017207c6cf50479c7ebb3af661c7e20f3866f034b49a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Change Log
2
2
 
3
+ ## [v3.5.2]
4
+ - add ABS function
5
+ - add array support for AST visitors
6
+ - add support for function callbacks
7
+ - improve support for date / time values
8
+ - improve error messaging for invalid arity
9
+ - improve AVG function accuracy
10
+ - validate enum arguments at parse time
11
+ - support adding multiple functions at once to global registry
12
+ - fix bug in print visitor precedence checking
13
+ - fix handling of Math::DomainError
14
+ - fix invalid cast
15
+
16
+ ## [v3.5.1]
17
+ - add bitwise shift left and shift right operators
18
+ - improve numeric conversions
19
+ - improve parse exceptions
20
+ - improve bitwise exceptions
21
+ - include variable name in bulk expression exceptions
22
+
3
23
  ## [v3.5.0]
4
24
  - fix bug with function argument count
5
25
  - add XOR operator
@@ -224,6 +244,8 @@
224
244
  ## [v0.1.0] 2012-01-20
225
245
  - initial release
226
246
 
247
+ [v3.5.2]: https://github.com/rubysolo/dentaku/compare/v3.5.1...v3.5.2
248
+ [v3.5.1]: https://github.com/rubysolo/dentaku/compare/v3.5.0...v3.5.1
227
249
  [v3.5.0]: https://github.com/rubysolo/dentaku/compare/v3.4.2...v3.5.0
228
250
  [v3.4.2]: https://github.com/rubysolo/dentaku/compare/v3.4.1...v3.4.2
229
251
  [v3.4.1]: https://github.com/rubysolo/dentaku/compare/v3.4.0...v3.4.1
data/README.md CHANGED
@@ -137,7 +137,7 @@ application, AST caching will consume more memory with each new formula.
137
137
  BUILT-IN OPERATORS AND FUNCTIONS
138
138
  ---------------------------------
139
139
 
140
- Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`
140
+ Math: `+`, `-`, `*`, `/`, `%`, `^`, `|`, `&`, `<<`, `>>`
141
141
 
142
142
  Also, all functions from Ruby's Math module, including `SIN`, `COS`, `TAN`, etc.
143
143
 
@@ -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`
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,31 +32,43 @@ module Dentaku
29
32
  end
30
33
 
31
34
  def value(context = {})
32
- l = cast(left.value(context))
33
- r = cast(right.value(context))
34
- begin
35
- l.public_send(operator, r)
36
- rescue ::TypeError => e
37
- # Right cannot be converted to a suitable type for left. e.g. [] + 1
38
- raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
39
- end
35
+ calculate(left.value(context), right.value(context))
40
36
  end
41
37
 
42
38
  private
43
39
 
44
- def cast(val, prefer_integer = true)
40
+ def calculate(left_value, right_value)
41
+ l = cast(left_value)
42
+ r = cast(right_value)
43
+
44
+ l.public_send(operator, r)
45
+ rescue ::TypeError => e
46
+ # Right cannot be converted to a suitable type for left. e.g. [] + 1
47
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: r, for: l.class), e.message
48
+ end
49
+
50
+ def cast(val)
45
51
  validate_value(val)
46
- numeric(val, prefer_integer)
52
+ numeric(val)
53
+ end
54
+
55
+ def numeric(val)
56
+ case val.to_s
57
+ when DECIMAL then decimal(val)
58
+ when INTEGER then val.to_i
59
+ else val
60
+ end
47
61
  end
48
62
 
49
- def numeric(val, prefer_integer)
50
- v = BigDecimal(val, Float::DIG + 1)
51
- v = v.to_i if prefer_integer && v.frac.zero?
52
- v
53
- rescue ::TypeError
54
- # If we got a TypeError BigDecimal or to_i failed;
55
- # let value through so ruby things like Time - integer work
56
- val
63
+ def decimal(val)
64
+ BigDecimal(val.to_s, Float::DIG + 1)
65
+ end
66
+
67
+ def datetime?(val)
68
+ # val is a Date, Time, or DateTime
69
+ return true if val.respond_to?(:strftime)
70
+
71
+ val.to_s =~ Dentaku::TokenScanner::DATE_TIME_REGEXP
57
72
  end
58
73
 
59
74
  def valid_node?(node)
@@ -101,10 +116,13 @@ module Dentaku
101
116
  end
102
117
 
103
118
  def value(context = {})
104
- if left.type == :datetime
105
- Dentaku::DateArithmetic.new(left.value(context)).add(right.value(context))
119
+ left_value = left.value(context)
120
+ right_value = right.value(context)
121
+
122
+ if left.type == :datetime || datetime?(left_value)
123
+ Dentaku::DateArithmetic.new(left_value).add(right_value)
106
124
  else
107
- super
125
+ calculate(left_value, right_value)
108
126
  end
109
127
  end
110
128
  end
@@ -119,10 +137,13 @@ module Dentaku
119
137
  end
120
138
 
121
139
  def value(context = {})
122
- if left.type == :datetime
123
- Dentaku::DateArithmetic.new(left.value(context)).sub(right.value(context))
140
+ left_value = left.value(context)
141
+ right_value = right.value(context)
142
+
143
+ if left.type == :datetime || datetime?(left_value)
144
+ Dentaku::DateArithmetic.new(left_value).sub(right_value)
124
145
  else
125
- super
146
+ calculate(left_value, right_value)
126
147
  end
127
148
  end
128
149
  end
@@ -143,7 +164,7 @@ module Dentaku
143
164
  end
144
165
 
145
166
  def value(context = {})
146
- r = cast(right.value(context), false)
167
+ r = decimal(cast(right.value(context)))
147
168
  raise Dentaku::ZeroDivisionError if r.zero?
148
169
 
149
170
  cast(cast(left.value(context)) / r)
@@ -2,23 +2,40 @@ require_relative './operation'
2
2
 
3
3
  module Dentaku
4
4
  module AST
5
- class BitwiseOr < Operation
5
+ class Bitwise < Operation
6
6
  def value(context = {})
7
- left.value(context) | right.value(context)
7
+ left_value = left.value(context)
8
+ right_value = right.value(context)
9
+
10
+ left_value.public_send(operator, right_value)
11
+ rescue NoMethodError => e
12
+ raise Dentaku::ArgumentError.for(:invalid_operator, value: left_value, for: left_value.class)
13
+ rescue TypeError => e
14
+ raise Dentaku::ArgumentError.for(:invalid_operator, value: right_value, for: right_value.class)
8
15
  end
16
+ end
9
17
 
18
+ class BitwiseOr < Bitwise
10
19
  def operator
11
20
  :|
12
21
  end
13
22
  end
14
23
 
15
- class BitwiseAnd < Operation
16
- def value(context = {})
17
- left.value(context) & right.value(context)
24
+ class BitwiseAnd < Bitwise
25
+ def operator
26
+ :&
18
27
  end
28
+ end
19
29
 
30
+ class BitwiseShiftLeft < Bitwise
20
31
  def operator
21
- :&
32
+ :<<
33
+ end
34
+ end
35
+
36
+ class BitwiseShiftRight < Bitwise
37
+ def operator
38
+ :>>
22
39
  end
23
40
  end
24
41
  end
@@ -16,8 +16,8 @@ module Dentaku
16
16
  end
17
17
 
18
18
  def value(context = {})
19
- l = validate_value(left.value(context))
20
- r = validate_value(right.value(context))
19
+ l = validate_value(cast(left.value(context)))
20
+ r = validate_value(cast(right.value(context)))
21
21
 
22
22
  l.public_send(operator, r)
23
23
  rescue ::ArgumentError => e
@@ -26,6 +26,15 @@ module Dentaku
26
26
 
27
27
  private
28
28
 
29
+ def cast(val)
30
+ return val unless val.is_a?(::String)
31
+ return val unless val.match?(Arithmetic::DECIMAL) || val.match?(Arithmetic::INTEGER)
32
+
33
+ v = BigDecimal(val, Float::DIG + 1)
34
+ v = v.to_i if v.frac.zero?
35
+ v
36
+ end
37
+
29
38
  def validate_value(value)
30
39
  unless value.respond_to?(operator)
31
40
  raise Dentaku::ArgumentError.for(:invalid_operator, operation: self.class, operator: operator),
@@ -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
@@ -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'
@@ -89,13 +89,9 @@ module Dentaku
89
89
  def with_rescues(var_name, results, block)
90
90
  yield
91
91
 
92
- rescue UnboundVariableError, Dentaku::ZeroDivisionError => ex
92
+ rescue Dentaku::UnboundVariableError, Dentaku::ZeroDivisionError, Dentaku::ArgumentError => ex
93
93
  ex.recipient_variable = var_name
94
94
  results[var_name] = block.call(ex)
95
-
96
- rescue Dentaku::ArgumentError => ex
97
- results[var_name] = block.call(ex)
98
-
99
95
  ensure
100
96
  if results[var_name] == :undefined && calculator.memory.has_key?(var_name.downcase)
101
97
  results[var_name] = calculator.memory[var_name.downcase]
@@ -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
 
@@ -94,9 +100,10 @@ module Dentaku
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)
@@ -1,10 +1,9 @@
1
1
  module Dentaku
2
2
  class Error < StandardError
3
+ attr_accessor :recipient_variable
3
4
  end
4
5
 
5
6
  class UnboundVariableError < Error
6
- attr_accessor :recipient_variable
7
-
8
7
  attr_reader :unbound_variables
9
8
 
10
9
  def initialize(unbound_variables)
@@ -12,6 +11,15 @@ module Dentaku
12
11
  end
13
12
  end
14
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
+
15
23
  class NodeError < Error
16
24
  attr_reader :child, :expect, :actual
17
25
 
@@ -74,6 +82,7 @@ module Dentaku
74
82
 
75
83
  class ArgumentError < ::ArgumentError
76
84
  attr_reader :reason, :meta
85
+ attr_accessor :recipient_variable
77
86
 
78
87
  def initialize(reason, **meta)
79
88
  @reason = reason
@@ -10,8 +10,11 @@ module Dentaku
10
10
  pow: AST::Exponentiation,
11
11
  negate: AST::Negation,
12
12
  mod: AST::Modulo,
13
+
13
14
  bitor: AST::BitwiseOr,
14
15
  bitand: AST::BitwiseAnd,
16
+ bitshiftleft: AST::BitwiseShiftLeft,
17
+ bitshiftright: AST::BitwiseShiftRight,
15
18
 
16
19
  lt: AST::LessThan,
17
20
  gt: AST::GreaterThan,
@@ -38,24 +41,33 @@ module Dentaku
38
41
 
39
42
  def consume(count = 2)
40
43
  operator = operations.pop
44
+ fail! :invalid_statement if operator.nil?
45
+
41
46
  operator.peek(output)
42
47
 
48
+ output_size = output.length
43
49
  args_size = operator.arity || count
44
50
  min_size = operator.arity || operator.min_param_count || count
45
51
  max_size = operator.arity || operator.max_param_count || count
46
52
 
47
- if output.length < min_size || args_size < min_size
48
- 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
49
56
  end
50
57
 
51
- if output.length > max_size && operations.empty? || args_size > max_size
52
- 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
53
61
  end
54
62
 
55
- fail! :invalid_statement if output.size < args_size
63
+ fail! :invalid_statement if output_size < args_size
56
64
  args = Array.new(args_size) { output.pop }.reverse
57
65
 
58
66
  output.push operator.new(*args)
67
+
68
+ if operator.respond_to?(:callback) && !operator.callback.nil?
69
+ operator.callback.call(args)
70
+ end
59
71
  rescue ::ArgumentError => e
60
72
  raise Dentaku::ArgumentError, e.message
61
73
  rescue NodeError => e
@@ -315,9 +327,9 @@ module Dentaku
315
327
  when :node_invalid
316
328
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
317
329
  when :too_few_operands
318
- "#{meta.fetch(:operator)} has too few operands"
330
+ "#{meta.fetch(:operator)} has too few operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
319
331
  when :too_many_operands
320
- "#{meta.fetch(:operator)} has too many operands"
332
+ "#{meta.fetch(:operator)} has too many operands (given #{meta.fetch(:actual)}, expected #{meta.fetch(:expect)})"
321
333
  when :undefined_function
322
334
  "Undefined function #{meta.fetch(:function_name)}"
323
335
  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
@@ -7,6 +7,8 @@ module Dentaku
7
7
  class TokenScanner
8
8
  extend StringCasing
9
9
 
10
+ DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
11
+
10
12
  def initialize(category, regexp, converter = nil, condition = nil)
11
13
  @category = category
12
14
  @regexp = %r{\A(#{ regexp })}i
@@ -73,7 +75,9 @@ module Dentaku
73
75
 
74
76
  def scanners(options = {})
75
77
  @case_sensitive = options.fetch(:case_sensitive, false)
76
- @scanners.values
78
+ raw_date_literals = options.fetch(:raw_date_literals, true)
79
+
80
+ @scanners.select { |k, _| raw_date_literals || k != :datetime }.values
77
81
  end
78
82
 
79
83
  def whitespace
@@ -86,7 +90,7 @@ module Dentaku
86
90
 
87
91
  # NOTE: Convert to DateTime as Array(Time) returns the parts of the time for some reason
88
92
  def datetime
89
- new(:datetime, /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?/, lambda { |raw| Time.parse(raw).to_datetime })
93
+ new(:datetime, DATE_TIME_REGEXP, lambda { |raw| Time.parse(raw).to_datetime })
90
94
  end
91
95
 
92
96
  def numeric
@@ -120,9 +124,9 @@ module Dentaku
120
124
 
121
125
  def operator
122
126
  names = {
123
- pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&'
127
+ pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%', bitor: '|', bitand: '&', bitshiftleft: '<<', bitshiftright: '>>'
124
128
  }.invert
125
- new(:operator, '\^|\+|-|\*|\/|%|\||&', lambda { |raw| names[raw] })
129
+ new(:operator, '\^|\+|-|\*|\/|%|\||&|<<|>>', lambda { |raw| names[raw] })
126
130
  end
127
131
 
128
132
  def grouping
@@ -4,7 +4,7 @@ require 'dentaku/token_scanner'
4
4
 
5
5
  module Dentaku
6
6
  class Tokenizer
7
- attr_reader :case_sensitive, :aliases
7
+ attr_reader :aliases
8
8
 
9
9
  LPAREN = TokenMatcher.new(:grouping, :open)
10
10
  RPAREN = TokenMatcher.new(:grouping, :close)
@@ -15,10 +15,14 @@ module Dentaku
15
15
  @aliases = options.fetch(:aliases, global_aliases)
16
16
  input = strip_comments(string.to_s.dup)
17
17
  input = replace_aliases(input)
18
- @case_sensitive = options.fetch(:case_sensitive, false)
18
+
19
+ scanner_options = {
20
+ case_sensitive: options.fetch(:case_sensitive, false),
21
+ raw_date_literals: options.fetch(:raw_date_literals, true)
22
+ }
19
23
 
20
24
  until input.empty?
21
- scanned = TokenScanner.scanners(case_sensitive: case_sensitive).any? do |scanner|
25
+ scanned = TokenScanner.scanners(scanner_options).any? do |scanner|
22
26
  scanned, input = scan(input, scanner)
23
27
  scanned
24
28
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.0"
2
+ VERSION = "3.5.2"
3
3
  end
@@ -77,6 +77,10 @@ module Dentaku
77
77
  def visit_nil(node)
78
78
  process(node)
79
79
  end
80
+
81
+ def visit_array(node)
82
+ process(node)
83
+ end
80
84
  end
81
85
  end
82
86
  end