dentaku 3.2.0 → 3.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.rubocop.yml +5 -10
- data/.travis.yml +4 -6
- data/CHANGELOG.md +86 -2
- data/README.md +7 -6
- data/dentaku.gemspec +1 -1
- data/lib/dentaku/ast/access.rb +21 -1
- data/lib/dentaku/ast/arithmetic.rb +51 -15
- data/lib/dentaku/ast/array.rb +41 -0
- data/lib/dentaku/ast/bitwise.rb +30 -5
- data/lib/dentaku/ast/case/case_conditional.rb +17 -2
- data/lib/dentaku/ast/case/case_else.rb +17 -3
- data/lib/dentaku/ast/case/case_switch_variable.rb +14 -0
- data/lib/dentaku/ast/case/case_then.rb +17 -3
- data/lib/dentaku/ast/case/case_when.rb +21 -3
- data/lib/dentaku/ast/case.rb +19 -3
- data/lib/dentaku/ast/comparators.rb +38 -28
- data/lib/dentaku/ast/function.rb +11 -3
- data/lib/dentaku/ast/function_registry.rb +21 -0
- data/lib/dentaku/ast/functions/all.rb +23 -0
- data/lib/dentaku/ast/functions/and.rb +2 -2
- data/lib/dentaku/ast/functions/any.rb +23 -0
- data/lib/dentaku/ast/functions/avg.rb +2 -2
- data/lib/dentaku/ast/functions/count.rb +8 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +37 -0
- data/lib/dentaku/ast/functions/filter.rb +23 -0
- data/lib/dentaku/ast/functions/if.rb +19 -2
- data/lib/dentaku/ast/functions/map.rb +23 -0
- data/lib/dentaku/ast/functions/or.rb +4 -4
- data/lib/dentaku/ast/functions/pluck.rb +30 -0
- data/lib/dentaku/ast/functions/round.rb +1 -1
- data/lib/dentaku/ast/functions/rounddown.rb +1 -1
- data/lib/dentaku/ast/functions/roundup.rb +1 -1
- data/lib/dentaku/ast/functions/ruby_math.rb +50 -3
- data/lib/dentaku/ast/functions/string_functions.rb +105 -12
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +3 -1
- data/lib/dentaku/ast/identifier.rb +16 -4
- data/lib/dentaku/ast/literal.rb +10 -0
- data/lib/dentaku/ast/negation.rb +7 -1
- data/lib/dentaku/ast/nil.rb +4 -0
- data/lib/dentaku/ast/node.rb +8 -0
- data/lib/dentaku/ast/operation.rb +17 -0
- data/lib/dentaku/ast/string.rb +7 -0
- data/lib/dentaku/ast.rb +8 -0
- data/lib/dentaku/bulk_expression_solver.rb +38 -27
- data/lib/dentaku/calculator.rb +21 -8
- data/lib/dentaku/date_arithmetic.rb +45 -0
- data/lib/dentaku/exceptions.rb +11 -8
- data/lib/dentaku/flat_hash.rb +9 -2
- data/lib/dentaku/parser.rb +57 -16
- data/lib/dentaku/print_visitor.rb +101 -0
- data/lib/dentaku/token_matcher.rb +1 -1
- data/lib/dentaku/token_scanner.rb +9 -3
- data/lib/dentaku/tokenizer.rb +7 -2
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +82 -0
- data/lib/dentaku.rb +20 -7
- data/spec/ast/addition_spec.rb +7 -1
- data/spec/ast/all_spec.rb +25 -0
- data/spec/ast/and_function_spec.rb +6 -6
- data/spec/ast/and_spec.rb +1 -1
- data/spec/ast/any_spec.rb +23 -0
- data/spec/ast/arithmetic_spec.rb +64 -29
- data/spec/ast/avg_spec.rb +9 -5
- data/spec/ast/comparator_spec.rb +31 -1
- data/spec/ast/count_spec.rb +7 -7
- data/spec/ast/division_spec.rb +7 -1
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +20 -15
- data/spec/ast/map_spec.rb +27 -0
- data/spec/ast/max_spec.rb +16 -3
- data/spec/ast/min_spec.rb +16 -3
- data/spec/ast/mul_spec.rb +11 -6
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +11 -8
- data/spec/ast/numeric_spec.rb +1 -1
- data/spec/ast/or_spec.rb +7 -7
- data/spec/ast/pluck_spec.rb +32 -0
- data/spec/ast/round_spec.rb +14 -4
- data/spec/ast/rounddown_spec.rb +14 -4
- data/spec/ast/roundup_spec.rb +14 -4
- data/spec/ast/string_functions_spec.rb +73 -0
- data/spec/ast/sum_spec.rb +11 -6
- data/spec/ast/switch_spec.rb +5 -5
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/bulk_expression_solver_spec.rb +37 -1
- data/spec/calculator_spec.rb +341 -32
- data/spec/dentaku_spec.rb +19 -6
- data/spec/external_function_spec.rb +32 -6
- data/spec/parser_spec.rb +100 -123
- data/spec/print_visitor_spec.rb +66 -0
- data/spec/spec_helper.rb +6 -4
- data/spec/token_matcher_spec.rb +8 -8
- data/spec/token_scanner_spec.rb +4 -4
- data/spec/tokenizer_spec.rb +56 -13
- data/spec/visitor/infix_spec.rb +31 -0
- data/spec/visitor_spec.rb +138 -0
- metadata +52 -7
data/spec/calculator_spec.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'dentaku'
|
3
|
-
|
4
3
|
describe Dentaku::Calculator do
|
5
4
|
let(:calculator) { described_class.new }
|
5
|
+
let(:with_case_sensitivity) { described_class.new(case_sensitive: true) }
|
6
6
|
let(:with_memory) { described_class.new.store(apples: 3) }
|
7
7
|
let(:with_aliases) { described_class.new(aliases: { round: ['rrround'] }) }
|
8
8
|
let(:without_nested_data) { described_class.new(nested_data_support: false) }
|
@@ -39,6 +39,93 @@ describe Dentaku::Calculator do
|
|
39
39
|
expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
|
40
40
|
expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
|
41
41
|
expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
|
42
|
+
expect(calculator.evaluate("5%")).to eq (0.05)
|
43
|
+
expect(calculator.evaluate('1 << 3')).to eq (8)
|
44
|
+
expect(calculator.evaluate('0xFF >> 6')).to eq (3)
|
45
|
+
end
|
46
|
+
|
47
|
+
describe 'evaluate' do
|
48
|
+
it 'returns nil when formula has error' do
|
49
|
+
expect(calculator.evaluate('1 + + 1')).to be_nil
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'suppresses unbound variable errors' do
|
53
|
+
expect(calculator.evaluate('AND(a,b)')).to be_nil
|
54
|
+
expect(calculator.evaluate('IF(a, 1, 0)')).to be_nil
|
55
|
+
expect(calculator.evaluate('MAX(a,b)')).to be_nil
|
56
|
+
expect(calculator.evaluate('MIN(a,b)')).to be_nil
|
57
|
+
expect(calculator.evaluate('NOT(a)')).to be_nil
|
58
|
+
expect(calculator.evaluate('OR(a,b)')).to be_nil
|
59
|
+
expect(calculator.evaluate('ROUND(a)')).to be_nil
|
60
|
+
expect(calculator.evaluate('ROUNDDOWN(a)')).to be_nil
|
61
|
+
expect(calculator.evaluate('ROUNDUP(a)')).to be_nil
|
62
|
+
expect(calculator.evaluate('SUM(a,b)')).to be_nil
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'suppresses numeric coercion errors' do
|
66
|
+
expect(calculator.evaluate('MAX(a,b)', a: nil, b: nil)).to be_nil
|
67
|
+
expect(calculator.evaluate('MIN(a,b)', a: nil, b: nil)).to be_nil
|
68
|
+
expect(calculator.evaluate('ROUND(a)', a: nil)).to be_nil
|
69
|
+
expect(calculator.evaluate('ROUNDDOWN(a)', a: nil)).to be_nil
|
70
|
+
expect(calculator.evaluate('ROUNDUP(a)', a: nil)).to be_nil
|
71
|
+
expect(calculator.evaluate('SUM(a,b)', a: nil, b: nil)).to be_nil
|
72
|
+
expect(calculator.evaluate('1.0 & "bar"')).to be_nil
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'treats explicit nil as logical false' do
|
76
|
+
expect(calculator.evaluate('AND(a,b)', a: nil, b: nil)).to be_falsy
|
77
|
+
expect(calculator.evaluate('IF(a,1,0)', a: nil, b: nil)).to eq(0)
|
78
|
+
expect(calculator.evaluate('NOT(a)', a: nil, b: nil)).to be_truthy
|
79
|
+
expect(calculator.evaluate('OR(a,b)', a: nil, b: nil)).to be_falsy
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'supports lazy evaluation of variables' do
|
83
|
+
expect(calculator.evaluate('x + 1', x: -> { 1 })).to eq(2)
|
84
|
+
expect { calculator.evaluate('2', x: -> { raise 'boom' }) }.not_to raise_error
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
describe 'ast' do
|
89
|
+
it 'raises parsing errors' do
|
90
|
+
expect { calculator.ast('()') }.to raise_error(Dentaku::ParseError)
|
91
|
+
expect { calculator.ast('(}') }.to raise_error(Dentaku::TokenizerError)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
describe 'evaluate!' do
|
96
|
+
it 'raises exception when formula has error' do
|
97
|
+
expect { calculator.evaluate!('1 + + 1') }.to raise_error(Dentaku::ParseError)
|
98
|
+
expect { calculator.evaluate!('(1 > 5) OR LEFT("abc", 1)') }.to raise_error(Dentaku::ParseError)
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'raises unbound variable errors' do
|
102
|
+
expect { calculator.evaluate!('AND(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
|
103
|
+
expect { calculator.evaluate!('IF(a, 1, 0)') }.to raise_error(Dentaku::UnboundVariableError)
|
104
|
+
expect { calculator.evaluate!('MAX(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
|
105
|
+
expect { calculator.evaluate!('MIN(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
|
106
|
+
expect { calculator.evaluate!('NOT(a)') }.to raise_error(Dentaku::UnboundVariableError)
|
107
|
+
expect { calculator.evaluate!('OR(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
|
108
|
+
expect { calculator.evaluate!('ROUND(a)') }.to raise_error(Dentaku::UnboundVariableError)
|
109
|
+
expect { calculator.evaluate!('ROUNDDOWN(a)') }.to raise_error(Dentaku::UnboundVariableError)
|
110
|
+
expect { calculator.evaluate!('ROUNDUP(a)') }.to raise_error(Dentaku::UnboundVariableError)
|
111
|
+
expect { calculator.evaluate!('SUM(a,b)') }.to raise_error(Dentaku::UnboundVariableError)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'raises numeric coersion errors' do
|
115
|
+
expect { calculator.evaluate!('MAX(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
|
116
|
+
expect { calculator.evaluate!('MIN(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
|
117
|
+
expect { calculator.evaluate!('ROUND(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
118
|
+
expect { calculator.evaluate!('ROUNDDOWN(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
119
|
+
expect { calculator.evaluate!('ROUNDUP(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
120
|
+
expect { calculator.evaluate!('SUM(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
|
121
|
+
expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
122
|
+
expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
123
|
+
expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'raises argument error if a function is called with incorrect arity' do
|
127
|
+
expect { calculator.evaluate!('IF(a,b)', a: 1, b: 1) }.to raise_error(Dentaku::ParseError)
|
128
|
+
end
|
42
129
|
end
|
43
130
|
|
44
131
|
it 'supports unicode characters in identifiers' do
|
@@ -57,35 +144,37 @@ describe Dentaku::Calculator do
|
|
57
144
|
|
58
145
|
it 'can store the value `false`' do
|
59
146
|
calculator.store('i_am_false', false)
|
60
|
-
expect(calculator.evaluate!('i_am_false')).to eq
|
147
|
+
expect(calculator.evaluate!('i_am_false')).to eq(false)
|
61
148
|
end
|
62
149
|
|
63
150
|
it 'can store multiple values' do
|
64
151
|
calculator.store(first: 1, second: 2)
|
65
|
-
expect(calculator.evaluate!('first')).to eq
|
66
|
-
expect(calculator.evaluate!('second')).to eq
|
152
|
+
expect(calculator.evaluate!('first')).to eq(1)
|
153
|
+
expect(calculator.evaluate!('second')).to eq(2)
|
67
154
|
end
|
68
155
|
|
69
156
|
it 'stores formulas' do
|
70
157
|
calculator.store_formula('area', 'length * width')
|
71
|
-
expect(calculator.evaluate!('area', length: 5, width: 5)).to eq
|
158
|
+
expect(calculator.evaluate!('area', length: 5, width: 5)).to eq(25)
|
72
159
|
end
|
73
160
|
|
74
161
|
it 'stores nested hashes' do
|
75
162
|
calculator.store(a: {basket: {of: 'apples'}}, b: 2)
|
76
|
-
expect(calculator.evaluate!('a.basket.of')).to eq
|
77
|
-
expect(calculator.evaluate!('
|
163
|
+
expect(calculator.evaluate!('a.basket.of')).to eq('apples')
|
164
|
+
expect(calculator.evaluate!('a.basket')).to eq(of: 'apples')
|
165
|
+
expect(calculator.evaluate!('b')).to eq(2)
|
78
166
|
end
|
79
167
|
|
80
168
|
it 'stores arrays' do
|
81
169
|
calculator.store(a: [1, 2, 3])
|
82
|
-
expect(calculator.evaluate!('a[0]')).to eq
|
83
|
-
expect(calculator.evaluate!('a[x]', x: 1)).to eq
|
84
|
-
expect(calculator.evaluate!('a[x+1]', x: 1)).to eq
|
170
|
+
expect(calculator.evaluate!('a[0]')).to eq(1)
|
171
|
+
expect(calculator.evaluate!('a[x]', x: 1)).to eq(2)
|
172
|
+
expect(calculator.evaluate!('a[x+1]', x: 1)).to eq(3)
|
85
173
|
end
|
86
174
|
|
87
|
-
it '
|
175
|
+
it 'evaluates arrays' do
|
88
176
|
expect(calculator.evaluate([1, 2, 3])).to eq([1, 2, 3])
|
177
|
+
expect(calculator.evaluate!('{1,2,3}')).to eq([1, 2, 3])
|
89
178
|
end
|
90
179
|
end
|
91
180
|
|
@@ -111,6 +200,11 @@ describe Dentaku::Calculator do
|
|
111
200
|
it "finds no dependencies in array literals" do
|
112
201
|
expect(calculator.dependencies([1, 2, 3])).to eq([])
|
113
202
|
end
|
203
|
+
|
204
|
+
it "finds dependencies in item expressions" do
|
205
|
+
expect(calculator.dependencies('MAP(vals, val, val + step)')).to eq(['vals', 'step'])
|
206
|
+
expect(calculator.dependencies('ALL(people, person, person.age < adult)')).to eq(['people', 'adult'])
|
207
|
+
end
|
114
208
|
end
|
115
209
|
|
116
210
|
describe 'solve!' do
|
@@ -122,6 +216,15 @@ describe Dentaku::Calculator do
|
|
122
216
|
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
123
217
|
end
|
124
218
|
|
219
|
+
it "prefers variables over values in memory if they have no dependencies" do
|
220
|
+
expect(with_memory.solve!(
|
221
|
+
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
222
|
+
weekly_apple_budget: "apples * 7",
|
223
|
+
pear: "1",
|
224
|
+
apples: "4"
|
225
|
+
)).to eq(apples: 4, pear: 1, weekly_apple_budget: 28, weekly_fruit_budget: 32)
|
226
|
+
end
|
227
|
+
|
125
228
|
it "preserves hash keys" do
|
126
229
|
expect(calculator.solve!(
|
127
230
|
'meaning_of_life' => 'age + kids',
|
@@ -138,7 +241,7 @@ describe Dentaku::Calculator do
|
|
138
241
|
|
139
242
|
it 'is case-insensitive' do
|
140
243
|
result = with_memory.solve!(total_fruit: "Apples + pears", pears: 10)
|
141
|
-
expect(result[:total_fruit]).to eq
|
244
|
+
expect(result[:total_fruit]).to eq(13)
|
142
245
|
end
|
143
246
|
|
144
247
|
it "lets you know if a variable is unbound" do
|
@@ -158,14 +261,28 @@ describe Dentaku::Calculator do
|
|
158
261
|
width: "length * 2",
|
159
262
|
)
|
160
263
|
|
161
|
-
expect(result[:weight]).to eq
|
264
|
+
expect(result[:weight]).to eq(130.368)
|
265
|
+
end
|
266
|
+
|
267
|
+
it 'raises an exception if there are cyclic dependencies' do
|
268
|
+
expect {
|
269
|
+
calculator.solve!(
|
270
|
+
make_money: "have_money",
|
271
|
+
have_money: "make_money"
|
272
|
+
)
|
273
|
+
}.to raise_error(TSort::Cyclic)
|
162
274
|
end
|
163
275
|
end
|
164
276
|
|
165
277
|
describe 'solve' do
|
166
278
|
it "returns :undefined when variables are unbound" do
|
167
|
-
expressions = {more_apples: "apples + 1"}
|
168
|
-
expect(calculator.solve(expressions)).to eq(more_apples: :undefined)
|
279
|
+
expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
|
280
|
+
expect(calculator.solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
|
281
|
+
end
|
282
|
+
|
283
|
+
it "returns :undefined when variables are nil" do
|
284
|
+
expressions = {more_apples: "apples + 1", compare_apples: "apples > 1"}
|
285
|
+
expect(calculator.store(apples: nil).solve(expressions)).to eq(more_apples: :undefined, compare_apples: :undefined)
|
169
286
|
end
|
170
287
|
|
171
288
|
it "allows passing in a custom value to an error handler" do
|
@@ -198,6 +315,33 @@ describe Dentaku::Calculator do
|
|
198
315
|
d: 0,
|
199
316
|
)
|
200
317
|
end
|
318
|
+
|
319
|
+
it 'returns undefined if there are cyclic dependencies' do
|
320
|
+
expect {
|
321
|
+
result = calculator.solve(
|
322
|
+
make_money: "have_money",
|
323
|
+
have_money: "make_money"
|
324
|
+
)
|
325
|
+
expect(result).to eq(
|
326
|
+
make_money: :undefined,
|
327
|
+
have_money: :undefined
|
328
|
+
)
|
329
|
+
}.not_to raise_error
|
330
|
+
end
|
331
|
+
|
332
|
+
it "integrates with custom functions" do
|
333
|
+
calculator.add_function(:custom, :integer, -> { 1 })
|
334
|
+
|
335
|
+
result = calculator.solve(
|
336
|
+
a: "1",
|
337
|
+
b: "CUSTOM() - a"
|
338
|
+
)
|
339
|
+
|
340
|
+
expect(result).to eq(
|
341
|
+
a: 1,
|
342
|
+
b: 0
|
343
|
+
)
|
344
|
+
end
|
201
345
|
end
|
202
346
|
|
203
347
|
it 'evaluates a statement with no variables' do
|
@@ -217,14 +361,18 @@ describe Dentaku::Calculator do
|
|
217
361
|
unbound = 'foo * 1.5'
|
218
362
|
expect { calculator.evaluate!(unbound) }.to raise_error(Dentaku::UnboundVariableError)
|
219
363
|
expect { calculator.evaluate!(unbound) }.to raise_error do |error|
|
220
|
-
expect(error.unbound_variables).to eq
|
364
|
+
expect(error.unbound_variables).to eq(['foo'])
|
221
365
|
end
|
222
366
|
expect { calculator.evaluate!('a + b') }.to raise_error do |error|
|
223
|
-
expect(error.unbound_variables).to eq
|
367
|
+
expect(error.unbound_variables).to eq(['a', 'b'])
|
224
368
|
end
|
225
369
|
expect(calculator.evaluate(unbound)).to be_nil
|
226
|
-
|
227
|
-
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'accepts a block for custom handling of unbound variables' do
|
373
|
+
unbound = 'foo * 1.5'
|
374
|
+
expect(calculator.evaluate(unbound) { :bar }).to eq(:bar)
|
375
|
+
expect(calculator.evaluate(unbound) { |e| e }).to eq(unbound)
|
228
376
|
end
|
229
377
|
|
230
378
|
it 'fails to evaluate incomplete statements' do
|
@@ -278,20 +426,35 @@ describe Dentaku::Calculator do
|
|
278
426
|
expect(calculator.evaluate('some_boolean OR 7 < 5', some_boolean: false)).to be_falsey
|
279
427
|
end
|
280
428
|
|
281
|
-
it 'compares
|
429
|
+
it 'compares time variables' do
|
282
430
|
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_truthy
|
283
431
|
expect(calculator.evaluate('t1 < t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
284
432
|
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 1).to_datetime, t2: Time.local(2017, 1, 2).to_datetime)).to be_falsy
|
285
433
|
expect(calculator.evaluate('t1 > t2', t1: Time.local(2017, 1, 2).to_datetime, t2: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
286
434
|
end
|
287
435
|
|
288
|
-
it 'compares
|
436
|
+
it 'compares time literals with time variables' do
|
289
437
|
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_truthy
|
290
438
|
expect(calculator.evaluate('t1 < 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_falsy
|
291
439
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 1).to_datetime)).to be_falsy
|
292
440
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
|
293
441
|
end
|
294
442
|
|
443
|
+
it 'supports date arithmetic' do
|
444
|
+
expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
445
|
+
expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
446
|
+
expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1)
|
447
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
|
448
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
449
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
450
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
|
451
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
|
452
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
|
453
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
|
454
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
|
455
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
|
456
|
+
end
|
457
|
+
|
295
458
|
describe 'functions' do
|
296
459
|
it 'include IF' do
|
297
460
|
expect(calculator.evaluate('if(foo < 8, 10, 20)', foo: 2)).to eq(10)
|
@@ -303,7 +466,7 @@ describe Dentaku::Calculator do
|
|
303
466
|
it 'include ROUND' do
|
304
467
|
expect(calculator.evaluate('round(8.2)')).to eq(8)
|
305
468
|
expect(calculator.evaluate('round(8.8)')).to eq(9)
|
306
|
-
expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal
|
469
|
+
expect(calculator.evaluate('round(8.75, 1)')).to eq(BigDecimal('8.8'))
|
307
470
|
|
308
471
|
expect(calculator.evaluate('ROUND(apples * 0.93)', apples: 10)).to eq(9)
|
309
472
|
end
|
@@ -319,10 +482,92 @@ describe Dentaku::Calculator do
|
|
319
482
|
it 'evaluates functions with negative numbers' do
|
320
483
|
expect(calculator.evaluate('if (-1 < 5, -1, 5)')).to eq(-1)
|
321
484
|
expect(calculator.evaluate('if (-1 = -1, -1, 5)')).to eq(-1)
|
322
|
-
expect(calculator.evaluate('round(-1.23, 1)')).to eq(BigDecimal
|
485
|
+
expect(calculator.evaluate('round(-1.23, 1)')).to eq(BigDecimal('-1.2'))
|
323
486
|
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
|
324
487
|
end
|
325
488
|
|
489
|
+
describe "any" do
|
490
|
+
it "enumerates values and returns true if any evaluation is truthy" do
|
491
|
+
expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy
|
492
|
+
expect(calculator.evaluate!('any(xs, x, x > 3)', xs: 3)).to be_falsy
|
493
|
+
expect(calculator.evaluate!('any({1,2,3,4}, x, x > 3)')).to be_truthy
|
494
|
+
expect(calculator.evaluate!('any({1,2,3,4}, x, x > 10)')).to be_falsy
|
495
|
+
expect(calculator.evaluate!('any(users, u, u.age > 33)', users: [
|
496
|
+
{name: "Bob", age: 44},
|
497
|
+
{name: "Jane", age: 27}
|
498
|
+
])).to be_truthy
|
499
|
+
expect(calculator.evaluate!('any(users, u, u.age < 18)', users: [
|
500
|
+
{name: "Bob", age: 44},
|
501
|
+
{name: "Jane", age: 27}
|
502
|
+
])).to be_falsy
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
describe "all" do
|
507
|
+
it "enumerates values and returns true if all evaluations are truthy" do
|
508
|
+
expect(calculator.evaluate!('all(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_falsy
|
509
|
+
expect(calculator.evaluate!('any(xs, x, x > 2)', xs: 3)).to be_truthy
|
510
|
+
expect(calculator.evaluate!('all({1,2,3,4}, x, x > 0)')).to be_truthy
|
511
|
+
expect(calculator.evaluate!('all({1,2,3,4}, x, x > 10)')).to be_falsy
|
512
|
+
expect(calculator.evaluate!('all(users, u, u.age > 33)', users: [
|
513
|
+
{name: "Bob", age: 44},
|
514
|
+
{name: "Jane", age: 27}
|
515
|
+
])).to be_falsy
|
516
|
+
expect(calculator.evaluate!('all(users, u, u.age < 50)', users: [
|
517
|
+
{name: "Bob", age: 44},
|
518
|
+
{name: "Jane", age: 27}
|
519
|
+
])).to be_truthy
|
520
|
+
end
|
521
|
+
end
|
522
|
+
|
523
|
+
describe "map" do
|
524
|
+
it "maps values" do
|
525
|
+
expect(calculator.evaluate!('map(xs, x, x * 2)', xs: [1, 2, 3, 4])).to eq([2, 4, 6, 8])
|
526
|
+
expect(calculator.evaluate!('map({1,2,3,4}, x, x * 2)')).to eq([2, 4, 6, 8])
|
527
|
+
expect(calculator.evaluate!('map(users, u, u.age)', users: [
|
528
|
+
{name: "Bob", age: 44},
|
529
|
+
{name: "Jane", age: 27}
|
530
|
+
])).to eq([44, 27])
|
531
|
+
expect(calculator.evaluate!('map(users, u, u.age)', users: [
|
532
|
+
{"name" => "Bob", "age" => 44},
|
533
|
+
{"name" => "Jane", "age" => 27}
|
534
|
+
])).to eq([44, 27])
|
535
|
+
expect(calculator.evaluate!('map(users, u, u.name)', users: [
|
536
|
+
{name: "Bob", age: 44},
|
537
|
+
{name: "Jane", age: 27}
|
538
|
+
])).to eq(["Bob", "Jane"])
|
539
|
+
expect(calculator.evaluate!('map(users, u, u.name)', users: [
|
540
|
+
{"name" => "Bob", "age" => 44},
|
541
|
+
{"name" => "Jane", "age" => 27}
|
542
|
+
])).to eq(["Bob", "Jane"])
|
543
|
+
expect(calculator.evaluate!('map(users, u, IF(u.age < 30, u, null))', users: [
|
544
|
+
{"name" => "Bob", "age" => 44},
|
545
|
+
{"name" => "Jane", "age" => 27}
|
546
|
+
])).to eq([nil, { "name" => "Jane", "age" => 27 }])
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
describe "pluck" do
|
551
|
+
it "plucks values from array of hashes" do
|
552
|
+
expect(calculator.evaluate!('pluck(users, age)', users: [
|
553
|
+
{name: "Bob", age: 44},
|
554
|
+
{name: "Jane", age: 27}
|
555
|
+
])).to eq([44, 27])
|
556
|
+
expect(calculator.evaluate!('pluck(users, age)', users: [
|
557
|
+
{"name" => "Bob", "age" => 44},
|
558
|
+
{"name" => "Jane", "age" => 27}
|
559
|
+
])).to eq([44, 27])
|
560
|
+
expect(calculator.evaluate!('pluck(users, name)', users: [
|
561
|
+
{name: "Bob", age: 44},
|
562
|
+
{name: "Jane", age: 27}
|
563
|
+
])).to eq(["Bob", "Jane"])
|
564
|
+
expect(calculator.evaluate!('pluck(users, name)', users: [
|
565
|
+
{"name" => "Bob", "age" => 44},
|
566
|
+
{"name" => "Jane", "age" => 27}
|
567
|
+
])).to eq(["Bob", "Jane"])
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
326
571
|
it 'evaluates functions with stored variables' do
|
327
572
|
calculator.store("multi_color" => true, "number_of_sheets" => 5000, "sheets_per_minute_black" => 2000, "sheets_per_minute_color" => 1000)
|
328
573
|
result = calculator.evaluate('number_of_sheets / if(multi_color, sheets_per_minute_color, sheets_per_minute_black)')
|
@@ -419,14 +664,14 @@ describe Dentaku::Calculator do
|
|
419
664
|
expect(calculator.evaluate(formula, number: 6)).to eq(2)
|
420
665
|
end
|
421
666
|
|
422
|
-
it '
|
667
|
+
it 'raises an exception when no match and there is no default value' do
|
423
668
|
formula = <<-FORMULA
|
424
669
|
CASE number
|
425
670
|
WHEN 42
|
426
671
|
THEN 1
|
427
672
|
END
|
428
673
|
FORMULA
|
429
|
-
expect { calculator.evaluate(formula, number: 2) }
|
674
|
+
expect { calculator.evaluate!(formula, number: 2) }
|
430
675
|
.to raise_error("No block matched the switch value '2'")
|
431
676
|
end
|
432
677
|
|
@@ -459,6 +704,31 @@ describe Dentaku::Calculator do
|
|
459
704
|
expect(value).to eq(5)
|
460
705
|
end
|
461
706
|
|
707
|
+
it 'handles nested case statements with case-sensitivity' do
|
708
|
+
formula = <<-FORMULA
|
709
|
+
CASE fruit
|
710
|
+
WHEN 'apple'
|
711
|
+
THEN 1 * quantity
|
712
|
+
WHEN 'banana'
|
713
|
+
THEN
|
714
|
+
CASE QUANTITY
|
715
|
+
WHEN 1 THEN 2
|
716
|
+
WHEN 10 THEN
|
717
|
+
CASE type
|
718
|
+
WHEN 'organic' THEN 5
|
719
|
+
END
|
720
|
+
END
|
721
|
+
END
|
722
|
+
FORMULA
|
723
|
+
value = with_case_sensitivity.evaluate(
|
724
|
+
formula,
|
725
|
+
type: 'organic',
|
726
|
+
quantity: 1,
|
727
|
+
QUANTITY: 10,
|
728
|
+
fruit: 'banana')
|
729
|
+
expect(value).to eq(5)
|
730
|
+
end
|
731
|
+
|
462
732
|
it 'handles multiple nested case statements' do
|
463
733
|
formula = <<-FORMULA
|
464
734
|
CASE fruit
|
@@ -492,12 +762,23 @@ describe Dentaku::Calculator do
|
|
492
762
|
Math.methods(false).each do |method|
|
493
763
|
it method do
|
494
764
|
if Math.method(method).arity == 2
|
495
|
-
expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq
|
765
|
+
expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
|
766
|
+
expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
|
767
|
+
expect { calculator.evaluate!("#{method}(x)", x: 1) }.to raise_error(Dentaku::ParseError)
|
496
768
|
else
|
497
|
-
expect(calculator.evaluate("#{method}(1)")).to eq
|
769
|
+
expect(calculator.evaluate("#{method}(1)")).to eq(Math.send(method, 1))
|
770
|
+
unless [:atanh, :frexp, :lgamma].include?(method)
|
771
|
+
expect(calculator.evaluate("#{method}(1) + 1")).to be_within(0.00001).of(Math.send(method, 1) + 1)
|
772
|
+
end
|
498
773
|
end
|
499
774
|
end
|
500
775
|
end
|
776
|
+
|
777
|
+
it 'are defined with a properly named class that represents it to support AST marshaling' do
|
778
|
+
expect {
|
779
|
+
Marshal.dump(calculator.ast('SQRT(20)'))
|
780
|
+
}.not_to raise_error
|
781
|
+
end
|
501
782
|
end
|
502
783
|
|
503
784
|
describe 'disable_cache' do
|
@@ -528,7 +809,7 @@ describe Dentaku::Calculator do
|
|
528
809
|
end
|
529
810
|
|
530
811
|
it 'clears all items from cache' do
|
531
|
-
expect(calculator.ast_cache.length).to eq
|
812
|
+
expect(calculator.ast_cache.length).to eq(3)
|
532
813
|
calculator.clear_cache
|
533
814
|
expect(calculator.ast_cache.keys).to be_empty
|
534
815
|
end
|
@@ -551,21 +832,31 @@ describe Dentaku::Calculator do
|
|
551
832
|
it 'concatenates strings' do
|
552
833
|
expect(
|
553
834
|
calculator.evaluate('CONCAT(s1, s2, s3)', 's1' => 'ab', 's2' => 'cd', 's3' => 'ef')
|
554
|
-
).to eq
|
835
|
+
).to eq('abcdef')
|
836
|
+
end
|
837
|
+
|
838
|
+
it 'manipulates string arguments' do
|
839
|
+
expect(calculator.evaluate("left('ABCD', 2)")).to eq('AB')
|
840
|
+
expect(calculator.evaluate("right('ABCD', 2)")).to eq('CD')
|
841
|
+
expect(calculator.evaluate("mid('ABCD', 2, 2)")).to eq('BC')
|
842
|
+
expect(calculator.evaluate("len('ABCD')")).to eq(4)
|
843
|
+
expect(calculator.evaluate("find('BC', 'ABCD')")).to eq(2)
|
844
|
+
expect(calculator.evaluate("substitute('ABCD', 'BC', 'XY')")).to eq('AXYD')
|
845
|
+
expect(calculator.evaluate("contains('BC', 'ABCD')")).to be_truthy
|
555
846
|
end
|
556
847
|
end
|
557
848
|
|
558
849
|
describe 'zero-arity functions' do
|
559
850
|
it 'can be used in formulas' do
|
560
851
|
calculator.add_function(:two, :numeric, -> { 2 })
|
561
|
-
expect(calculator.evaluate("max(two(), 1)")).to eq
|
562
|
-
expect(calculator.evaluate("max(1, two())")).to eq
|
852
|
+
expect(calculator.evaluate("max(two(), 1)")).to eq(2)
|
853
|
+
expect(calculator.evaluate("max(1, two())")).to eq(2)
|
563
854
|
end
|
564
855
|
end
|
565
856
|
|
566
857
|
describe 'aliases' do
|
567
858
|
it 'accepts aliases as instance option' do
|
568
|
-
expect(with_aliases.evaluate('rrround(5.1)')).to eq
|
859
|
+
expect(with_aliases.evaluate('rrround(5.1)')).to eq(5)
|
569
860
|
end
|
570
861
|
end
|
571
862
|
|
@@ -584,4 +875,22 @@ describe Dentaku::Calculator do
|
|
584
875
|
end.to raise_error(Dentaku::UnboundVariableError)
|
585
876
|
end
|
586
877
|
end
|
878
|
+
|
879
|
+
describe 'identifier cache' do
|
880
|
+
it 'reduces call count by caching results of resolved identifiers' do
|
881
|
+
called = 0
|
882
|
+
calculator.store_formula("A1", "B1+B1+B1")
|
883
|
+
calculator.store_formula("B1", "C1+C1+C1+C1")
|
884
|
+
calculator.store_formula("C1", "D1")
|
885
|
+
calculator.store("D1", proc { called += 1; 1 })
|
886
|
+
|
887
|
+
expect {
|
888
|
+
Dentaku.enable_identifier_cache!
|
889
|
+
}.to change {
|
890
|
+
called = 0
|
891
|
+
calculator.evaluate("A1")
|
892
|
+
called
|
893
|
+
}.from(12).to(1)
|
894
|
+
end
|
895
|
+
end
|
587
896
|
end
|
data/spec/dentaku_spec.rb
CHANGED
@@ -22,18 +22,31 @@ describe Dentaku do
|
|
22
22
|
|
23
23
|
it 'raises a parse error for bad logic expressions' do
|
24
24
|
expect {
|
25
|
-
Dentaku('true AND')
|
25
|
+
Dentaku!('true AND')
|
26
26
|
}.to raise_error(Dentaku::ParseError)
|
27
27
|
end
|
28
28
|
|
29
29
|
it 'evaluates with class-level shortcut functions' do
|
30
|
-
expect(
|
31
|
-
expect(
|
32
|
-
expect {
|
30
|
+
expect(described_class.evaluate('2+2')).to eq(4)
|
31
|
+
expect(described_class.evaluate!('2+2')).to eq(4)
|
32
|
+
expect { described_class.evaluate!('a+1') }.to raise_error(Dentaku::UnboundVariableError)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'accepts a block for custom handling of unbound variables' do
|
36
|
+
unbound = 'apples * 1.5'
|
37
|
+
expect(described_class.evaluate(unbound) { :bar }).to eq(:bar)
|
38
|
+
expect(described_class.evaluate(unbound) { |e| e }).to eq(unbound)
|
33
39
|
end
|
34
40
|
|
35
41
|
it 'evaluates with class-level aliases' do
|
36
|
-
|
37
|
-
expect(
|
42
|
+
described_class.aliases = { roundup: ['roundupup'] }
|
43
|
+
expect(described_class.evaluate('roundupup(6.1)')).to eq(7)
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'sets caching opt-in flags' do
|
47
|
+
expect {
|
48
|
+
described_class.enable_caching!
|
49
|
+
}.to change { described_class.cache_ast? }.from(false).to(true)
|
50
|
+
.and change { described_class.cache_dependency_order? }.from(false).to(true)
|
38
51
|
end
|
39
52
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
+
require 'dentaku'
|
2
3
|
require 'dentaku/calculator'
|
3
4
|
|
4
5
|
describe Dentaku::Calculator do
|
@@ -14,6 +15,7 @@ describe Dentaku::Calculator do
|
|
14
15
|
[:pow, :numeric, ->(mantissa, exponent) { mantissa**exponent }],
|
15
16
|
[:biggest, :numeric, ->(*args) { args.max }],
|
16
17
|
[:smallest, :numeric, ->(*args) { args.min }],
|
18
|
+
[:optional, :numeric, ->(x, y, z = 0) { x + y + z }],
|
17
19
|
]
|
18
20
|
|
19
21
|
c.add_functions(fns)
|
@@ -39,6 +41,13 @@ describe Dentaku::Calculator do
|
|
39
41
|
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
40
42
|
end
|
41
43
|
|
44
|
+
it 'includes OPTIONAL' do
|
45
|
+
expect(with_external_funcs.evaluate('OPTIONAL(1,2)')).to eq(3)
|
46
|
+
expect(with_external_funcs.evaluate('OPTIONAL(1,2,3)')).to eq(6)
|
47
|
+
expect { with_external_funcs.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
|
48
|
+
expect { with_external_funcs.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
|
49
|
+
end
|
50
|
+
|
42
51
|
it 'supports array parameters' do
|
43
52
|
calculator = described_class.new
|
44
53
|
calculator.add_function(
|
@@ -59,6 +68,19 @@ describe Dentaku::Calculator do
|
|
59
68
|
expect(calculator.evaluate("hey!()")).to eq("hey!")
|
60
69
|
end
|
61
70
|
|
71
|
+
it 'defines for a given function a properly named class that represents it to support AST marshaling' do
|
72
|
+
calculator = described_class.new
|
73
|
+
expect {
|
74
|
+
calculator.add_function(:ho, :string, -> {})
|
75
|
+
}.to change {
|
76
|
+
Dentaku::AST::Function.const_defined?("Ho")
|
77
|
+
}.from(false).to(true)
|
78
|
+
|
79
|
+
expect {
|
80
|
+
Marshal.dump(calculator.ast('MAX(1, 2)'))
|
81
|
+
}.not_to raise_error
|
82
|
+
end
|
83
|
+
|
62
84
|
it 'does not store functions across all calculators' do
|
63
85
|
calculator1 = Dentaku::Calculator.new
|
64
86
|
calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
|
@@ -66,15 +88,19 @@ describe Dentaku::Calculator do
|
|
66
88
|
calculator2 = Dentaku::Calculator.new
|
67
89
|
calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
|
68
90
|
|
69
|
-
expect(calculator1.evaluate("1 + my_function(2)")). to eq
|
70
|
-
expect(calculator2.evaluate("1 + my_function(2)")). to eq
|
91
|
+
expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
|
92
|
+
expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
|
71
93
|
|
72
|
-
expect {
|
94
|
+
expect {
|
95
|
+
Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
|
96
|
+
}.to raise_error(Dentaku::ParseError)
|
73
97
|
end
|
74
98
|
|
75
|
-
|
76
|
-
|
77
|
-
|
99
|
+
describe 'Dentaku::Calculator.add_function' do
|
100
|
+
it 'adds to default/global function registry' do
|
101
|
+
Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
|
102
|
+
expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
|
103
|
+
end
|
78
104
|
end
|
79
105
|
end
|
80
106
|
end
|