dentaku 3.5.0 → 3.5.2

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 (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