dentaku 3.3.0 → 3.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +5 -10
  3. data/.travis.yml +3 -6
  4. data/CHANGELOG.md +38 -1
  5. data/README.md +2 -2
  6. data/dentaku.gemspec +0 -2
  7. data/lib/dentaku.rb +14 -6
  8. data/lib/dentaku/ast.rb +5 -0
  9. data/lib/dentaku/ast/access.rb +15 -1
  10. data/lib/dentaku/ast/arithmetic.rb +29 -6
  11. data/lib/dentaku/ast/array.rb +15 -1
  12. data/lib/dentaku/ast/case.rb +13 -3
  13. data/lib/dentaku/ast/case/case_conditional.rb +13 -2
  14. data/lib/dentaku/ast/case/case_else.rb +12 -4
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
  16. data/lib/dentaku/ast/case/case_then.rb +12 -4
  17. data/lib/dentaku/ast/case/case_when.rb +12 -4
  18. data/lib/dentaku/ast/function.rb +11 -2
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +36 -0
  21. data/lib/dentaku/ast/functions/any.rb +36 -0
  22. data/lib/dentaku/ast/functions/avg.rb +2 -2
  23. data/lib/dentaku/ast/functions/count.rb +8 -0
  24. data/lib/dentaku/ast/functions/duration.rb +51 -0
  25. data/lib/dentaku/ast/functions/if.rb +15 -2
  26. data/lib/dentaku/ast/functions/map.rb +36 -0
  27. data/lib/dentaku/ast/functions/mul.rb +3 -2
  28. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  29. data/lib/dentaku/ast/functions/round.rb +1 -1
  30. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  31. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  32. data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
  33. data/lib/dentaku/ast/functions/string_functions.rb +68 -4
  34. data/lib/dentaku/ast/functions/sum.rb +3 -2
  35. data/lib/dentaku/ast/grouping.rb +3 -1
  36. data/lib/dentaku/ast/identifier.rb +5 -1
  37. data/lib/dentaku/ast/negation.rb +3 -1
  38. data/lib/dentaku/ast/node.rb +4 -0
  39. data/lib/dentaku/ast/operation.rb +8 -0
  40. data/lib/dentaku/bulk_expression_solver.rb +34 -25
  41. data/lib/dentaku/calculator.rb +19 -6
  42. data/lib/dentaku/date_arithmetic.rb +45 -0
  43. data/lib/dentaku/exceptions.rb +4 -4
  44. data/lib/dentaku/flat_hash.rb +9 -2
  45. data/lib/dentaku/parser.rb +31 -14
  46. data/lib/dentaku/token_matcher.rb +1 -1
  47. data/lib/dentaku/token_scanner.rb +1 -1
  48. data/lib/dentaku/tokenizer.rb +7 -2
  49. data/lib/dentaku/version.rb +1 -1
  50. data/spec/ast/addition_spec.rb +7 -1
  51. data/spec/ast/and_function_spec.rb +6 -6
  52. data/spec/ast/and_spec.rb +1 -1
  53. data/spec/ast/arithmetic_spec.rb +57 -29
  54. data/spec/ast/avg_spec.rb +9 -5
  55. data/spec/ast/count_spec.rb +7 -7
  56. data/spec/ast/division_spec.rb +7 -1
  57. data/spec/ast/function_spec.rb +9 -9
  58. data/spec/ast/max_spec.rb +3 -3
  59. data/spec/ast/min_spec.rb +3 -3
  60. data/spec/ast/mul_spec.rb +10 -6
  61. data/spec/ast/negation_spec.rb +48 -0
  62. data/spec/ast/node_spec.rb +11 -8
  63. data/spec/ast/numeric_spec.rb +1 -1
  64. data/spec/ast/or_spec.rb +6 -6
  65. data/spec/ast/round_spec.rb +14 -4
  66. data/spec/ast/rounddown_spec.rb +14 -4
  67. data/spec/ast/roundup_spec.rb +14 -4
  68. data/spec/ast/string_functions_spec.rb +35 -0
  69. data/spec/ast/sum_spec.rb +10 -6
  70. data/spec/ast/switch_spec.rb +5 -5
  71. data/spec/bulk_expression_solver_spec.rb +18 -1
  72. data/spec/calculator_spec.rb +173 -28
  73. data/spec/dentaku_spec.rb +18 -5
  74. data/spec/external_function_spec.rb +29 -5
  75. data/spec/parser_spec.rb +85 -123
  76. data/spec/spec_helper.rb +6 -4
  77. data/spec/token_matcher_spec.rb +8 -8
  78. data/spec/token_scanner_spec.rb +4 -4
  79. data/spec/tokenizer_spec.rb +32 -13
  80. metadata +11 -4
@@ -0,0 +1,45 @@
1
+ module Dentaku
2
+ class DateArithmetic
3
+ def initialize(date)
4
+ @base = date
5
+ end
6
+
7
+ def add(duration)
8
+ case duration
9
+ when Numeric
10
+ @base + duration
11
+ when Dentaku::AST::Duration::Value
12
+ case duration.unit
13
+ when :year
14
+ Time.local(@base.year + duration.value, @base.month, @base.day).to_datetime
15
+ when :month
16
+ @base >> duration.value
17
+ when :day
18
+ @base + duration.value
19
+ end
20
+ else
21
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
22
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
23
+ end
24
+ end
25
+
26
+ def sub(duration)
27
+ case duration
28
+ when DateTime, Numeric
29
+ @base - duration
30
+ when Dentaku::AST::Duration::Value
31
+ case duration.unit
32
+ when :year
33
+ Time.local(@base.year - duration.value, @base.month, @base.day).to_datetime
34
+ when :month
35
+ @base << duration.value
36
+ when :day
37
+ @base - duration.value
38
+ end
39
+ else
40
+ raise Dentaku::ArgumentError.for(:incompatible_type, value: duration, for: Numeric),
41
+ "'#{duration || duration.class}' is not coercible for date arithmetic"
42
+ end
43
+ end
44
+ end
45
+ end
@@ -33,7 +33,7 @@ module Dentaku
33
33
  private_class_method :new
34
34
 
35
35
  VALID_REASONS = %i[
36
- node_invalid too_few_operands undefined_function
36
+ node_invalid too_few_operands too_many_operands undefined_function
37
37
  unprocessed_token unknown_case_token unbalanced_bracket
38
38
  unbalanced_parenthesis unknown_grouping_token not_implemented_token_category
39
39
  invalid_statement
@@ -44,7 +44,7 @@ module Dentaku
44
44
  raise ::ArgumentError, "Unhandled #{reason}"
45
45
  end
46
46
 
47
- new reason, meta
47
+ new(reason, **meta)
48
48
  end
49
49
  end
50
50
 
@@ -68,7 +68,7 @@ module Dentaku
68
68
  raise ::ArgumentError, "Unhandled #{reason}"
69
69
  end
70
70
 
71
- new reason, meta
71
+ new(reason, **meta)
72
72
  end
73
73
  end
74
74
 
@@ -92,7 +92,7 @@ module Dentaku
92
92
  raise ::ArgumentError, "Unhandled #{reason}"
93
93
  end
94
94
 
95
- new reason, meta
95
+ new(reason, **meta)
96
96
  end
97
97
  end
98
98
 
@@ -6,6 +6,13 @@ module Dentaku
6
6
  flatten_keys(acc)
7
7
  end
8
8
 
9
+ def self.from_hash_with_intermediates(h, key = [], acc = {})
10
+ acc.update(key => h) unless key.empty?
11
+ return unless h.is_a? Hash
12
+ h.each { |k, v| from_hash_with_intermediates(v, key + [k], acc) }
13
+ flatten_keys(acc)
14
+ end
15
+
9
16
  def self.flatten_keys(hash)
10
17
  hash.each_with_object({}) do |(k, v), h|
11
18
  h[flatten_key(k)] = v
@@ -19,8 +26,8 @@ module Dentaku
19
26
  key
20
27
  end
21
28
 
22
- def self.expand(h)
23
- h.each_with_object({}) do |(k, v), r|
29
+ def self.expand(hash)
30
+ hash.each_with_object({}) do |(k, v), r|
24
31
  hash_levels = k.to_s.split('.')
25
32
  hash_levels = hash_levels.map(&:to_sym) if k.is_a?(Symbol)
26
33
  child_hash = hash_levels[0...-1].reduce(r) { |h, n| h[n] ||= {} }
@@ -40,12 +40,23 @@ module Dentaku
40
40
  operator.peek(output)
41
41
 
42
42
  args_size = operator.arity || count
43
- if args_size > output.length
44
- fail! :too_few_operands, operator: operator, expect: args_size, actual: output.length
43
+ min_size = operator.arity || operator.min_param_count || count
44
+ max_size = operator.arity || operator.max_param_count || count
45
+
46
+ if output.length < min_size
47
+ fail! :too_few_operands, operator: operator, expect: min_size, actual: output.length
48
+ end
49
+
50
+ if output.length > max_size && operations.empty?
51
+ fail! :too_many_operands, operator: operator, expect: max_size, actual: output.length
45
52
  end
53
+
54
+ fail! :invalid_statement if output.size < args_size
46
55
  args = Array.new(args_size) { output.pop }.reverse
47
56
 
48
57
  output.push operator.new(*args)
58
+ rescue ::ArgumentError => e
59
+ raise Dentaku::ArgumentError, e.message
49
60
  rescue NodeError => e
50
61
  fail! :node_invalid, operator: operator, child: e.child, expect: e.expect, actual: e.actual
51
62
  end
@@ -111,17 +122,19 @@ module Dentaku
111
122
  open_cases = 0
112
123
  case_end_index = nil
113
124
 
114
- input.each_with_index do |token, index|
115
- if token.category == :case && token.value == :open
116
- open_cases += 1
117
- end
125
+ input.each_with_index do |input_token, index|
126
+ if input_token.category == :case
127
+ if input_token.value == :open
128
+ open_cases += 1
129
+ end
118
130
 
119
- if token.category == :case && token.value == :close
120
- if open_cases > 0
121
- open_cases -= 1
122
- else
123
- case_end_index = index
124
- break
131
+ if input_token.value == :close
132
+ if open_cases > 0
133
+ open_cases -= 1
134
+ else
135
+ case_end_index = index
136
+ break
137
+ end
125
138
  end
126
139
  end
127
140
  end
@@ -129,7 +142,8 @@ module Dentaku
129
142
  subparser = Parser.new(
130
143
  inner_case_inputs,
131
144
  operations: [AST::Case],
132
- arities: [0]
145
+ arities: [0],
146
+ function_registry: @function_registry
133
147
  )
134
148
  subparser.parse
135
149
  output.concat(subparser.output)
@@ -253,6 +267,7 @@ module Dentaku
253
267
  end
254
268
 
255
269
  when :comma
270
+ fail! :invalid_statement if arities.empty?
256
271
  arities[-1] += 1
257
272
  while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
258
273
  consume
@@ -299,6 +314,8 @@ module Dentaku
299
314
  "#{meta.fetch(:operator)} requires #{meta.fetch(:expect).join(', ')} operands, but got #{meta.fetch(:actual)}"
300
315
  when :too_few_operands
301
316
  "#{meta.fetch(:operator)} has too few operands"
317
+ when :too_many_operands
318
+ "#{meta.fetch(:operator)} has too many operands"
302
319
  when :undefined_function
303
320
  "Undefined function #{meta.fetch(:function_name)}"
304
321
  when :unprocessed_token
@@ -319,7 +336,7 @@ module Dentaku
319
336
  raise ::ArgumentError, "Unhandled #{reason}"
320
337
  end
321
338
 
322
- raise ParseError.for(reason, meta), message
339
+ raise ParseError.for(reason, **meta), message
323
340
  end
324
341
  end
325
342
  end
@@ -16,7 +16,7 @@ module Dentaku
16
16
  @range = (@min..@max)
17
17
  end
18
18
 
19
- def | (other_matcher)
19
+ def |(other_matcher)
20
20
  self.class.new(:nomatch, :nomatch, leaf_matchers + other_matcher.leaf_matchers)
21
21
  end
22
22
 
@@ -91,7 +91,7 @@ module Dentaku
91
91
 
92
92
  def numeric
93
93
  new(:numeric, '((?:\d+(\.\d+)?|\.\d+)(?:(e|E)(\+|-)?\d+)?)\b', lambda { |raw|
94
- raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i
94
+ raw =~ /\./ ? BigDecimal(raw) : raw.to_i
95
95
  })
96
96
  end
97
97
 
@@ -12,7 +12,7 @@ module Dentaku
12
12
  def tokenize(string, options = {})
13
13
  @nesting = 0
14
14
  @tokens = []
15
- @aliases = options.fetch(:aliases, Dentaku.aliases)
15
+ @aliases = options.fetch(:aliases, global_aliases)
16
16
  input = strip_comments(string.to_s.dup)
17
17
  input = replace_aliases(input)
18
18
  @case_sensitive = options.fetch(:case_sensitive, false)
@@ -84,6 +84,11 @@ module Dentaku
84
84
 
85
85
  private
86
86
 
87
+ def global_aliases
88
+ return {} unless Dentaku.respond_to?(:aliases)
89
+ Dentaku.aliases
90
+ end
91
+
87
92
  def fail!(reason, **meta)
88
93
  message =
89
94
  case reason
@@ -99,7 +104,7 @@ module Dentaku
99
104
  raise ::ArgumentError, "Unhandled #{reason}"
100
105
  end
101
106
 
102
- raise TokenizerError.for(reason, meta), message
107
+ raise TokenizerError.for(reason, **meta), message
103
108
  end
104
109
  end
105
110
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.3.0"
2
+ VERSION = "3.4.0"
3
3
  end
@@ -9,9 +9,15 @@ describe Dentaku::AST::Addition do
9
9
 
10
10
  let(:t) { Dentaku::AST::Numeric.new Dentaku::Token.new(:logical, true) }
11
11
 
12
+ it 'allows access to its sub-trees' do
13
+ node = described_class.new(five, six)
14
+ expect(node.left).to eq(five)
15
+ expect(node.right).to eq(six)
16
+ end
17
+
12
18
  it 'performs addition' do
13
19
  node = described_class.new(five, six)
14
- expect(node.value).to eq 11
20
+ expect(node.value).to eq(11)
15
21
  end
16
22
 
17
23
  it 'requires numeric operands' do
@@ -7,29 +7,29 @@ describe 'Dentaku::AST::And' do
7
7
 
8
8
  it 'returns false if any of the arguments is false' do
9
9
  result = Dentaku('AND(1 = 1, 0 = 1)')
10
- expect(result).to eq false
10
+ expect(result).to eq(false)
11
11
  end
12
12
 
13
13
  it 'supports nested expressions' do
14
14
  result = Dentaku('AND(y = 1, x = 1)', x: 1, y: 2)
15
- expect(result).to eq false
15
+ expect(result).to eq(false)
16
16
  end
17
17
 
18
18
  it 'returns true if all of the arguments are true' do
19
19
  result = Dentaku('AND(1 = 1, "2" = "2", true = true, true)')
20
- expect(result).to eq true
20
+ expect(result).to eq(true)
21
21
  end
22
22
 
23
23
  it 'returns true if all nested AND functions return true' do
24
24
  result = Dentaku('AND(AND(1 = 1), AND(true != false, AND(true)))')
25
- expect(result).to eq true
25
+ expect(result).to eq(true)
26
26
  end
27
27
 
28
28
  it 'raises an error if no arguments are passed' do
29
- expect { calculator.evaluate!('AND()') }.to raise_error(ArgumentError)
29
+ expect { calculator.evaluate!('AND()') }.to raise_error(Dentaku::ArgumentError)
30
30
  end
31
31
 
32
32
  it 'raises an error if a non logical argument is passed' do
33
- expect { calculator.evaluate!('AND("r")') }.to raise_error(ArgumentError)
33
+ expect { calculator.evaluate!('AND("r")') }.to raise_error(Dentaku::ArgumentError)
34
34
  end
35
35
  end
@@ -11,7 +11,7 @@ describe Dentaku::AST::And do
11
11
 
12
12
  it 'performs logical AND' do
13
13
  node = described_class.new(t, f)
14
- expect(node.value).to eq false
14
+ expect(node.value).to eq(false)
15
15
  end
16
16
 
17
17
  it 'requires logical operands' do
@@ -4,53 +4,81 @@ require 'dentaku/ast/arithmetic'
4
4
  require 'dentaku/token'
5
5
 
6
6
  describe Dentaku::AST::Arithmetic do
7
- let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
8
- let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
9
- let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
- let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
11
- let(:ctx) { {'x' => 1, 'y' => 2} }
7
+ let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
8
+ let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
9
+ let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
10
+ let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
11
+ let(:ctx) { {'x' => 1, 'y' => 2} }
12
+ let(:date) { Dentaku::AST::DateTime.new Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16)) }
12
13
 
13
14
  it 'performs an arithmetic operation with numeric operands' do
14
- expect(add(one, two)).to eq 3
15
- expect(sub(one, two)).to eq -1
16
- expect(mul(one, two)).to eq 2
17
- expect(div(one, two)).to eq 0.5
15
+ expect(add(one, two)).to eq(3)
16
+ expect(sub(one, two)).to eq(-1)
17
+ expect(mul(one, two)).to eq(2)
18
+ expect(div(one, two)).to eq(0.5)
19
+ expect(neg(one)).to eq(-1)
18
20
  end
19
21
 
20
22
  it 'performs an arithmetic operation with one numeric operand and one string operand' do
21
- expect(add(one, x)).to eq 2
22
- expect(sub(one, x)).to eq 0
23
- expect(mul(one, x)).to eq 1
24
- expect(div(one, x)).to eq 1
23
+ expect(add(one, x)).to eq(2)
24
+ expect(sub(one, x)).to eq(0)
25
+ expect(mul(one, x)).to eq(1)
26
+ expect(div(one, x)).to eq(1)
25
27
 
26
- expect(add(y, two)).to eq 4
27
- expect(sub(y, two)).to eq 0
28
- expect(mul(y, two)).to eq 4
29
- expect(div(y, two)).to eq 1
28
+ expect(add(y, two)).to eq(4)
29
+ expect(sub(y, two)).to eq(0)
30
+ expect(mul(y, two)).to eq(4)
31
+ expect(div(y, two)).to eq(1)
30
32
  end
31
33
 
32
34
  it 'performs an arithmetic operation with string operands' do
33
- expect(add(x, y)).to eq 3
34
- expect(sub(x, y)).to eq -1
35
- expect(mul(x, y)).to eq 2
36
- expect(div(x, y)).to eq 0.5
35
+ expect(add(x, y)).to eq(3)
36
+ expect(sub(x, y)).to eq(-1)
37
+ expect(mul(x, y)).to eq(2)
38
+ expect(div(x, y)).to eq(0.5)
39
+ expect(neg(x)).to eq(-1)
40
+ end
41
+
42
+ it 'correctly parses string operands to numeric values' do
43
+ expect(add(x, one, 'x' => '1')).to eq(2)
44
+ expect(add(x, one, 'x' => '1.1')).to eq(2.1)
45
+ expect(add(x, one, 'x' => '.1')).to eq(1.1)
46
+ expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
47
+ expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
48
+ end
49
+
50
+ it 'performs arithmetic on arrays' do
51
+ expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
52
+ end
53
+
54
+ it 'performs date arithmetic' do
55
+ expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
56
+ end
57
+
58
+ it 'raises ArgumentError if given individually valid but incompatible arguments' do
59
+ expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
60
+ expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)
37
61
  end
38
62
 
39
63
  private
40
64
 
41
- def add(left, right)
42
- Dentaku::AST::Addition.new(left, right).value(ctx)
65
+ def add(left, right, context = ctx)
66
+ Dentaku::AST::Addition.new(left, right).value(context)
67
+ end
68
+
69
+ def sub(left, right, context = ctx)
70
+ Dentaku::AST::Subtraction.new(left, right).value(context)
43
71
  end
44
72
 
45
- def sub(left, right)
46
- Dentaku::AST::Subtraction.new(left, right).value(ctx)
73
+ def mul(left, right, context = ctx)
74
+ Dentaku::AST::Multiplication.new(left, right).value(context)
47
75
  end
48
76
 
49
- def mul(left, right)
50
- Dentaku::AST::Multiplication.new(left, right).value(ctx)
77
+ def div(left, right, context = ctx)
78
+ Dentaku::AST::Division.new(left, right).value(context)
51
79
  end
52
80
 
53
- def div(left, right)
54
- Dentaku::AST::Division.new(left, right).value(ctx)
81
+ def neg(node, context = ctx)
82
+ Dentaku::AST::Negation.new(node).value(context)
55
83
  end
56
84
  end
@@ -5,29 +5,33 @@ require 'dentaku'
5
5
  describe 'Dentaku::AST::Function::Avg' do
6
6
  it 'returns the average of an array of Numeric values' do
7
7
  result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
8
- expect(result).to eq 1.7
8
+ expect(result).to eq(1.7)
9
9
  end
10
10
 
11
11
  it 'returns the average of a single entry array of a Numeric value' do
12
12
  result = Dentaku('AVG(x)', x: 2.3)
13
- expect(result).to eq 2.3
13
+ expect(result).to eq(2.3)
14
14
  end
15
15
 
16
16
  it 'returns the average even if a String is passed' do
17
17
  result = Dentaku('AVG(1, x, 1.8)', x: '2.3')
18
- expect(result).to eq 1.7
18
+ expect(result).to eq(1.7)
19
19
  end
20
20
 
21
21
  it 'returns the average even if an array is passed' do
22
22
  result = Dentaku('AVG(1, x, 2.3)', x: [4, 5])
23
- expect(result).to eq 3.075
23
+ expect(result).to eq(3.075)
24
24
  end
25
25
 
26
26
  context 'checking errors' do
27
27
  let(:calculator) { Dentaku::Calculator.new }
28
28
 
29
29
  it 'raises an error if no arguments are passed' do
30
- expect { calculator.evaluate!('AVG()') }.to raise_error(ArgumentError)
30
+ expect { calculator.evaluate!('AVG()') }.to raise_error(Dentaku::ArgumentError)
31
+ end
32
+
33
+ it 'raises an error if an empty array is passed' do
34
+ expect { calculator.evaluate!('AVG(x)', x: []) }.to raise_error(Dentaku::ArgumentError)
31
35
  end
32
36
  end
33
37
  end