dentaku 3.5.3 → 3.5.5

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/CHANGELOG.md +22 -6
  5. data/README.md +1 -4
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +0 -3
  8. data/lib/dentaku/ast/arithmetic.rb +36 -50
  9. data/lib/dentaku/ast/array.rb +1 -4
  10. data/lib/dentaku/ast/case.rb +12 -0
  11. data/lib/dentaku/ast/functions/all.rb +1 -5
  12. data/lib/dentaku/ast/functions/any.rb +1 -5
  13. data/lib/dentaku/ast/functions/enum.rb +13 -0
  14. data/lib/dentaku/ast/functions/map.rb +1 -5
  15. data/lib/dentaku/ast/functions/pluck.rb +6 -2
  16. data/lib/dentaku/ast/node.rb +2 -1
  17. data/lib/dentaku/ast/operation.rb +5 -0
  18. data/lib/dentaku/ast.rb +1 -1
  19. data/lib/dentaku/bulk_expression_solver.rb +37 -7
  20. data/lib/dentaku/calculator.rb +21 -5
  21. data/lib/dentaku/date_arithmetic.rb +24 -15
  22. data/lib/dentaku/dependency_resolver.rb +9 -4
  23. data/lib/dentaku/parser.rb +206 -213
  24. data/lib/dentaku/print_visitor.rb +2 -2
  25. data/lib/dentaku/token.rb +12 -0
  26. data/lib/dentaku/version.rb +1 -1
  27. data/lib/dentaku/visitor/infix.rb +1 -1
  28. data/spec/ast/addition_spec.rb +12 -7
  29. data/spec/ast/all_spec.rb +13 -0
  30. data/spec/ast/any_spec.rb +13 -0
  31. data/spec/ast/arithmetic_spec.rb +7 -0
  32. data/spec/ast/division_spec.rb +10 -6
  33. data/spec/ast/map_spec.rb +13 -0
  34. data/spec/ast/pluck_spec.rb +17 -0
  35. data/spec/bulk_expression_solver_spec.rb +24 -1
  36. data/spec/calculator_spec.rb +21 -3
  37. data/spec/dependency_resolver_spec.rb +18 -0
  38. data/spec/external_function_spec.rb +1 -1
  39. data/spec/parser_spec.rb +11 -2
  40. data/spec/print_visitor_spec.rb +5 -0
  41. data/spec/spec_helper.rb +1 -3
  42. data/spec/visitor_spec.rb +1 -1
  43. metadata +10 -9
@@ -4,13 +4,18 @@ module Dentaku
4
4
  class DependencyResolver
5
5
  include TSort
6
6
 
7
- def self.find_resolve_order(vars_to_dependencies_hash)
8
- self.new(vars_to_dependencies_hash).tsort
7
+ def self.find_resolve_order(vars_to_dependencies_hash, case_sensitive = false)
8
+ self.new(vars_to_dependencies_hash).sort
9
9
  end
10
10
 
11
11
  def initialize(vars_to_dependencies_hash)
12
- # ensure variables are strings
13
- @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.to_s, v] }]
12
+ @key_mapping = Hash[vars_to_dependencies_hash.keys.map { |k| [k.downcase, k] }]
13
+ # ensure variables are normalized strings
14
+ @vars_to_deps = Hash[vars_to_dependencies_hash.map { |k, v| [k.downcase.to_s, v] }]
15
+ end
16
+
17
+ def sort
18
+ tsort.map { |k| @key_mapping.fetch(k, k) }
14
19
  end
15
20
 
16
21
  def tsort_each_node(&block)
@@ -43,8 +43,6 @@ module Dentaku
43
43
  operator = operations.pop
44
44
  fail! :invalid_statement if operator.nil?
45
45
 
46
- operator.peek(output)
47
-
48
46
  output_size = output.length
49
47
  args_size = operator.arity || count
50
48
  min_size = operator.arity || operator.min_param_count || count
@@ -55,17 +53,18 @@ module Dentaku
55
53
  fail! :too_few_operands, operator: operator, expect: expect, actual: output_size
56
54
  end
57
55
 
58
- if output_size > max_size && operations.empty? || args_size > max_size
56
+ if (output_size > max_size && operations.empty?) || args_size > max_size
59
57
  expect = min_size == max_size ? min_size : min_size..max_size
60
58
  fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
61
59
  end
62
60
 
61
+ args = []
63
62
  if operator == AST::Array && output.empty?
64
- output.push(operator.new())
63
+ # special case: empty array literal '{}'
64
+ output.push(operator.new)
65
65
  else
66
66
  fail! :invalid_statement if output_size < args_size
67
67
  args = Array.new(args_size) { output.pop }.reverse
68
-
69
68
  output.push operator.new(*args)
70
69
  end
71
70
 
@@ -81,250 +80,244 @@ module Dentaku
81
80
  def parse
82
81
  return AST::Nil.new if input.empty?
83
82
 
84
- while token = input.shift
85
- case token.category
86
- when :datetime
87
- output.push AST::DateTime.new(token)
83
+ i = 0
84
+ while i < input.length
85
+ token = input[i]
86
+ lookahead = input[i + 1]
87
+ process_token(token, lookahead, i, input)
88
+ i += 1
89
+ end
88
90
 
89
- when :numeric
90
- output.push AST::Numeric.new(token)
91
+ while operations.any?
92
+ consume
93
+ end
91
94
 
92
- when :logical
93
- output.push AST::Logical.new(token)
95
+ unless output.count == 1
96
+ fail! :invalid_statement
97
+ end
94
98
 
95
- when :string
96
- output.push AST::String.new(token)
99
+ output.first
100
+ end
97
101
 
98
- when :identifier
99
- output.push AST::Identifier.new(token, case_sensitive: case_sensitive)
102
+ def operation(token)
103
+ AST_OPERATIONS.fetch(token.value)
104
+ end
100
105
 
101
- when :operator, :comparator, :combinator
102
- op_class = operation(token)
106
+ def function(token)
107
+ function_registry.get(token.value)
108
+ end
103
109
 
104
- if op_class.right_associative?
105
- while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
106
- consume
107
- end
110
+ def function_registry
111
+ @function_registry ||= Dentaku::AST::FunctionRegistry.new
112
+ end
108
113
 
109
- operations.push op_class
110
- else
111
- while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
112
- consume
113
- end
114
+ private
114
115
 
115
- operations.push op_class
116
- end
116
+ def process_token(token, lookahead, index, tokens)
117
+ case token.category
118
+ when :datetime then output << AST::DateTime.new(token)
119
+ when :numeric then output << AST::Numeric.new(token)
120
+ when :logical then output << AST::Logical.new(token)
121
+ when :string then output << AST::String.new(token)
122
+ when :identifier then output << AST::Identifier.new(token, case_sensitive: case_sensitive)
123
+ when :operator, :comparator, :combinator
124
+ handle_operator(token, lookahead)
125
+ when :null
126
+ output << AST::Nil.new
127
+ when :function
128
+ handle_function(token)
129
+ when :case
130
+ handle_case(token, index, tokens)
131
+ when :access
132
+ handle_access(token)
133
+ when :array
134
+ handle_array(token)
135
+ when :grouping
136
+ handle_grouping(token, lookahead, tokens)
137
+ else
138
+ fail! :not_implemented_token_category, token_category: token.category
139
+ end
140
+ end
117
141
 
118
- when :null
119
- output.push AST::Nil.new
142
+ def handle_operator(token, lookahead)
143
+ op_class = operation(token).resolve_class(lookahead)
144
+ if op_class.right_associative?
145
+ while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
146
+ consume
147
+ end
148
+ else
149
+ while operations.last && operations.last < AST::Operation && op_class.precedence <= operations.last.precedence
150
+ consume
151
+ end
152
+ end
153
+ operations.push op_class
154
+ end
120
155
 
121
- when :function
122
- func = function(token)
123
- if func.nil?
124
- fail! :undefined_function, function_name: token.value
125
- end
156
+ def handle_function(token)
157
+ func = function(token)
158
+ fail! :undefined_function, function_name: token.value if func.nil?
159
+ arities.push 0
160
+ operations.push func
161
+ end
126
162
 
127
- arities.push 0
128
- operations.push func
129
-
130
- when :case
131
- case_index = operations.index { |o| o == AST::Case } || -1
132
- token_index = case_index + 1
133
-
134
- case token.value
135
- when :open
136
- # special handling for case nesting: strip out inner case
137
- # statements and parse their AST segments recursively
138
- if operations.include?(AST::Case)
139
- open_cases = 0
140
- case_end_index = nil
141
-
142
- input.each_with_index do |input_token, index|
143
- if input_token.category == :case
144
- if input_token.value == :open
145
- open_cases += 1
146
- end
147
-
148
- if input_token.value == :close
149
- if open_cases > 0
150
- open_cases -= 1
151
- else
152
- case_end_index = index
153
- break
154
- end
155
- end
163
+ def handle_case(token, index, tokens)
164
+ case_index = operations.index { |o| o == AST::Case } || -1
165
+ token_index = case_index + 1
166
+
167
+ case token.value
168
+ when :open
169
+ if operations.include?(AST::Case)
170
+ # nested case extraction (non-recursive outer parser)
171
+ open_cases = 0
172
+ case_end_index = nil
173
+ j = index + 1
174
+ while j < tokens.length
175
+ t = tokens[j]
176
+ if t.category == :case
177
+ if t.value == :open
178
+ open_cases += 1
179
+ elsif t.value == :close
180
+ if open_cases > 0
181
+ open_cases -= 1
182
+ else
183
+ case_end_index = j
184
+ break
156
185
  end
157
186
  end
158
- inner_case_inputs = input.slice!(0..case_end_index)
159
- subparser = Parser.new(
160
- inner_case_inputs,
161
- operations: [AST::Case],
162
- arities: [0],
163
- function_registry: @function_registry,
164
- case_sensitive: case_sensitive
165
- )
166
- subparser.parse
167
- output.concat(subparser.output)
168
- else
169
- operations.push AST::Case
170
- arities.push(0)
171
- end
172
- when :close
173
- if operations[token_index] == AST::CaseThen
174
- while operations.last != AST::Case
175
- consume
176
- end
177
-
178
- operations.push(AST::CaseConditional)
179
- consume(2)
180
- arities[-1] += 1
181
- elsif operations[token_index] == AST::CaseElse
182
- while operations.last != AST::Case
183
- consume
184
- end
185
-
186
- arities[-1] += 1
187
- end
188
-
189
- unless operations.count >= 1 && operations.last == AST::Case
190
- fail! :unprocessed_token, token_name: token.value
191
187
  end
192
- consume(arities.pop.succ)
193
- when :when
194
- if operations[token_index] == AST::CaseThen
195
- while ![AST::CaseWhen, AST::Case].include?(operations.last)
196
- consume
197
- end
198
- operations.push(AST::CaseConditional)
199
- consume(2)
200
- arities[-1] += 1
201
- elsif operations.last == AST::Case
202
- operations.push(AST::CaseSwitchVariable)
203
- consume
204
- end
205
-
206
- operations.push(AST::CaseWhen)
207
- when :then
208
- if operations[token_index] == AST::CaseWhen
209
- while ![AST::CaseThen, AST::Case].include?(operations.last)
210
- consume
211
- end
212
- end
213
- operations.push(AST::CaseThen)
214
- when :else
215
- if operations[token_index] == AST::CaseThen
216
- while operations.last != AST::Case
217
- consume
218
- end
219
-
220
- operations.push(AST::CaseConditional)
221
- consume(2)
222
- arities[-1] += 1
223
- end
224
-
225
- operations.push(AST::CaseElse)
226
- else
227
- fail! :unknown_case_token, token_name: token.value
188
+ j += 1
228
189
  end
190
+ inner_case_inputs = tokens.slice!(index + 1, case_end_index - index) || []
191
+ subparser = Parser.new(
192
+ inner_case_inputs,
193
+ operations: [AST::Case],
194
+ arities: [0],
195
+ function_registry: @function_registry,
196
+ case_sensitive: case_sensitive
197
+ )
198
+ subparser.parse
199
+ output.concat(subparser.output)
200
+ else
201
+ operations.push AST::Case
202
+ arities.push(0)
203
+ end
229
204
 
230
- when :access
231
- case token.value
232
- when :lbracket
233
- operations.push AST::Access
234
- when :rbracket
235
- while operations.any? && operations.last != AST::Access
236
- consume
237
- end
238
-
239
- unless operations.last == AST::Access
240
- fail! :unbalanced_bracket, token: token
241
- end
205
+ when :close
206
+ if operations[token_index] == AST::CaseThen
207
+ while operations.last != AST::Case
242
208
  consume
243
209
  end
244
-
245
- when :array
246
- case token.value
247
- when :array_start
248
- operations.push AST::Array
249
- arities.push 0
250
- when :array_end
251
- while operations.any? && operations.last != AST::Array
252
- consume
253
- end
254
-
255
- unless operations.last == AST::Array
256
- fail! :unbalanced_bracket, token: token
257
- end
258
-
259
- consume(arities.pop.succ)
210
+ operations.push(AST::CaseConditional)
211
+ consume(2)
212
+ arities[-1] += 1
213
+ elsif operations[token_index] == AST::CaseElse
214
+ while operations.last != AST::Case
215
+ consume
260
216
  end
217
+ arities[-1] += 1
218
+ end
219
+ fail! :unprocessed_token, token_name: token.value unless operations.count >= 1 && operations.last == AST::Case
220
+ consume(arities.pop.succ)
261
221
 
262
- when :grouping
263
- case token.value
264
- when :open
265
- if input.first && input.first.value == :close
266
- input.shift
267
- arities.pop
268
- consume(0)
269
- else
270
- operations.push AST::Grouping
271
- end
272
-
273
- when :close
274
- while operations.any? && operations.last != AST::Grouping
275
- consume
276
- end
277
-
278
- lparen = operations.pop
279
- unless lparen == AST::Grouping
280
- fail! :unbalanced_parenthesis, token
281
- end
282
-
283
- if operations.last && operations.last < AST::Function
284
- consume(arities.pop.succ)
285
- end
286
-
287
- when :comma
288
- fail! :invalid_statement if arities.empty?
289
- arities[-1] += 1
290
- while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
291
- consume
292
- end
222
+ when :when
223
+ if operations[token_index] == AST::CaseThen
224
+ while ![AST::CaseWhen, AST::Case].include?(operations.last)
225
+ consume
226
+ end
227
+ operations.push(AST::CaseConditional)
228
+ consume(2)
229
+ arities[-1] += 1
230
+ elsif operations.last == AST::Case
231
+ operations.push(AST::CaseSwitchVariable)
232
+ consume
233
+ end
234
+ operations.push(AST::CaseWhen)
293
235
 
294
- else
295
- fail! :unknown_grouping_token, token_name: token.value
236
+ when :then
237
+ if operations[token_index] == AST::CaseWhen
238
+ while ![AST::CaseThen, AST::Case].include?(operations.last)
239
+ consume
296
240
  end
241
+ end
242
+ operations.push(AST::CaseThen)
297
243
 
298
- else
299
- fail! :not_implemented_token_category, token_category: token.category
244
+ when :else
245
+ if operations[token_index] == AST::CaseThen
246
+ while operations.last != AST::Case
247
+ consume
248
+ end
249
+ operations.push(AST::CaseConditional)
250
+ consume(2)
251
+ arities[-1] += 1
300
252
  end
253
+ operations.push(AST::CaseElse)
254
+ else
255
+ fail! :unknown_case_token, token_name: token.value
301
256
  end
257
+ end
302
258
 
303
- while operations.any?
259
+ def handle_access(token)
260
+ case token.value
261
+ when :lbracket
262
+ operations.push AST::Access
263
+
264
+ when :rbracket
265
+ while operations.any? && operations.last != AST::Access
266
+ consume
267
+ end
268
+ fail! :unbalanced_bracket, token: token unless operations.last == AST::Access
304
269
  consume
305
270
  end
271
+ end
306
272
 
307
- unless output.count == 1
308
- fail! :invalid_statement
309
- end
273
+ def handle_array(token)
274
+ case token.value
275
+ when :array_start
276
+ operations.push AST::Array
277
+ arities.push 0
310
278
 
311
- output.first
279
+ when :array_end
280
+ while operations.any? && operations.last != AST::Array
281
+ consume
282
+ end
283
+ fail! :unbalanced_bracket, token: token unless operations.last == AST::Array
284
+ consume(arities.pop.succ)
285
+ end
312
286
  end
313
287
 
314
- def operation(token)
315
- AST_OPERATIONS.fetch(token.value)
316
- end
288
+ def handle_grouping(token, lookahead, tokens)
289
+ case token.value
290
+ when :open
291
+ if lookahead && lookahead.value == :close
292
+ # empty grouping (e.g. function with zero arguments) — we trigger consume later
293
+ tokens.delete_at(tokens.index(lookahead)) # remove the close to mimic previous shift behavior
294
+ arities.pop
295
+ consume(0)
296
+ else
297
+ operations.push AST::Grouping
298
+ end
317
299
 
318
- def function(token)
319
- function_registry.get(token.value)
320
- end
300
+ when :close
301
+ while operations.any? && operations.last != AST::Grouping
302
+ consume
303
+ end
304
+ lparen = operations.pop
305
+ fail! :unbalanced_parenthesis, token unless lparen == AST::Grouping
306
+ if operations.last && operations.last < AST::Function
307
+ consume(arities.pop.succ)
308
+ end
321
309
 
322
- def function_registry
323
- @function_registry ||= Dentaku::AST::FunctionRegistry.new
310
+ when :comma
311
+ fail! :invalid_statement if arities.empty?
312
+ arities[-1] += 1
313
+ while operations.any? && operations.last != AST::Grouping && operations.last != AST::Array
314
+ consume
315
+ end
316
+ else
317
+ fail! :unknown_grouping_token, token_name: token.value
318
+ end
324
319
  end
325
320
 
326
- private
327
-
328
321
  def fail!(reason, **meta)
329
322
  message =
330
323
  case reason
@@ -7,13 +7,13 @@ module Dentaku
7
7
 
8
8
  def visit_operation(node)
9
9
  if node.left
10
- visit_operand(node.left, node.class.precedence, suffix: " ", dir: :left)
10
+ visit_operand(node.left, node.class.precedence, suffix: node.operator_spacing, 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: " ", dir: :right)
16
+ visit_operand(node.right, node.class.precedence, prefix: node.operator_spacing, dir: :right)
17
17
  end
18
18
  end
19
19
 
data/lib/dentaku/token.rb CHANGED
@@ -20,10 +20,22 @@ module Dentaku
20
20
  length.zero?
21
21
  end
22
22
 
23
+ def operator?
24
+ is?(:operator)
25
+ end
26
+
23
27
  def grouping?
24
28
  is?(:grouping)
25
29
  end
26
30
 
31
+ def open?
32
+ grouping? && value == :open
33
+ end
34
+
35
+ def close?
36
+ grouping? && value == :close
37
+ end
38
+
27
39
  def is?(c)
28
40
  category == c
29
41
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "3.5.3"
2
+ VERSION = "3.5.5"
3
3
  end
@@ -83,4 +83,4 @@ module Dentaku
83
83
  end
84
84
  end
85
85
  end
86
- end
86
+ end
@@ -20,22 +20,21 @@ describe Dentaku::AST::Addition do
20
20
  expect(node.value).to eq(11)
21
21
  end
22
22
 
23
- it 'requires numeric operands' do
23
+ it 'requires operands that respond to +' do
24
24
  expect {
25
- described_class.new(five, t)
26
- }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
25
+ described_class.new(five, t).value
26
+ }.to raise_error(Dentaku::ArgumentError, /requires operands that respond to +/)
27
27
 
28
28
  expression = Dentaku::AST::Multiplication.new(five, five)
29
29
  group = Dentaku::AST::Grouping.new(expression)
30
30
 
31
31
  expect {
32
- described_class.new(group, five)
32
+ described_class.new(group, five).value
33
33
  }.not_to raise_error
34
34
  end
35
35
 
36
36
  it 'allows operands that respond to addition' do
37
37
  # Sample struct that has a custom definition for addition
38
-
39
38
  Addable = Struct.new(:value) do
40
39
  def +(other)
41
40
  case other
@@ -51,12 +50,18 @@ describe Dentaku::AST::Addition do
51
50
  operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
52
51
 
53
52
  expect {
54
- described_class.new(operand_five, operand_six)
53
+ described_class.new(operand_five, operand_six).value
55
54
  }.not_to raise_error
56
55
 
57
56
  expect {
58
- described_class.new(operand_five, six)
57
+ described_class.new(operand_five, six).value
59
58
  }.not_to raise_error
59
+ end
60
+
61
+ it 'does not try to parse nested string as date' do
62
+ a = ['2017-01-01', '2017-01-02']
63
+ b = ['2017-01-01']
60
64
 
65
+ expect(Dentaku('a + b', a: a, b: b)).to eq(['2017-01-01', '2017-01-02', '2017-01-01'])
61
66
  end
62
67
  end
data/spec/ast/all_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::All do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'performs ALL operation' do
8
9
  result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
9
10
  expect(result).to eq(false)
@@ -22,4 +23,16 @@ describe Dentaku::AST::All do
22
23
  Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
23
24
  )
24
25
  end
26
+
27
+ it 'treats missing keys in hashes as NULL in permissive mode' do
28
+ expect(
29
+ calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
30
+ ).to be_falsy
31
+ end
32
+
33
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
34
+ expect {
35
+ calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
36
+ }.to raise_error(Dentaku::UnboundVariableError)
37
+ end
25
38
  end
data/spec/ast/any_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Any do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'performs ANY operation' do
8
9
  result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
9
10
  expect(result).to eq(true)
@@ -20,4 +21,16 @@ describe Dentaku::AST::Any do
20
21
  it 'raises argument error if a string is passed as identifier' do
21
22
  expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
22
23
  end
24
+
25
+ it 'treats missing keys in hashes as NULL in permissive mode' do
26
+ expect(
27
+ calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
28
+ ).to be_truthy
29
+ end
30
+
31
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
32
+ expect {
33
+ calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
34
+ }.to raise_error(Dentaku::UnboundVariableError)
35
+ end
23
36
  end
@@ -111,6 +111,13 @@ describe Dentaku::AST::Arithmetic do
111
111
  end
112
112
  end
113
113
 
114
+ it 'does not try to parse nested string as date' do
115
+ a = ['2017-01-01', '2017-01-02']
116
+ b = ['2017-01-01']
117
+
118
+ expect(Dentaku('a - b', a: a, b: b)).to eq(['2017-01-02'])
119
+ end
120
+
114
121
  it 'raises ArgumentError if given individually valid but incompatible arguments' do
115
122
  expect { add(one, date) }.to raise_error(Dentaku::ArgumentError)
116
123
  expect { add(x, one, 'x' => [1]) }.to raise_error(Dentaku::ArgumentError)