dentaku 3.5.0 → 3.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +22 -0
- data/README.md +2 -2
- data/lib/dentaku/ast/arithmetic.rb +46 -25
- data/lib/dentaku/ast/bitwise.rb +23 -6
- data/lib/dentaku/ast/comparators.rb +11 -2
- data/lib/dentaku/ast/function_registry.rb +10 -1
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/avg.rb +1 -1
- data/lib/dentaku/ast/functions/enum.rb +5 -4
- data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
- data/lib/dentaku/ast.rb +1 -0
- data/lib/dentaku/bulk_expression_solver.rb +1 -5
- data/lib/dentaku/calculator.rb +15 -8
- data/lib/dentaku/date_arithmetic.rb +5 -1
- data/lib/dentaku/exceptions.rb +11 -2
- data/lib/dentaku/parser.rb +19 -7
- data/lib/dentaku/print_visitor.rb +16 -5
- data/lib/dentaku/token_scanner.rb +8 -4
- data/lib/dentaku/tokenizer.rb +7 -3
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +4 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/all_spec.rb +1 -1
- data/spec/ast/any_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +20 -5
- data/spec/ast/avg_spec.rb +5 -0
- data/spec/ast/comparator_spec.rb +8 -0
- data/spec/ast/filter_spec.rb +1 -1
- data/spec/ast/map_spec.rb +1 -1
- data/spec/ast/or_spec.rb +1 -1
- data/spec/ast/pluck_spec.rb +1 -1
- data/spec/bulk_expression_solver_spec.rb +9 -0
- data/spec/calculator_spec.rb +70 -16
- data/spec/external_function_spec.rb +89 -18
- data/spec/print_visitor_spec.rb +6 -0
- data/spec/tokenizer_spec.rb +12 -0
- data/spec/visitor/infix_spec.rb +22 -1
- data/spec/visitor_spec.rb +3 -2
- metadata +6 -3
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/abs'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Abs' do
|
6
|
+
it 'returns the absolute value of number' do
|
7
|
+
result = Dentaku('ABS(-4.2)')
|
8
|
+
expect(result).to eq(4.2)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns the correct value for positive number' do
|
12
|
+
result = Dentaku('ABS(1.3)')
|
13
|
+
expect(result).to eq(1.3)
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'returns the correct value for zero' do
|
17
|
+
result = Dentaku('ABS(0)')
|
18
|
+
expect(result).to eq(0)
|
19
|
+
end
|
20
|
+
|
21
|
+
context 'checking errors' do
|
22
|
+
it 'raises an error if argument is not numeric' do
|
23
|
+
expect { Dentaku!("ABS(2020-1-1)") }.to raise_error(Dentaku::ArgumentError)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/spec/ast/all_spec.rb
CHANGED
@@ -19,7 +19,7 @@ describe Dentaku::AST::All do
|
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
21
|
expect { calculator.evaluate!('ALL({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
-
Dentaku::
|
22
|
+
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
23
23
|
)
|
24
24
|
end
|
25
25
|
end
|
data/spec/ast/any_spec.rb
CHANGED
@@ -18,6 +18,6 @@ describe Dentaku::AST::Any do
|
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
|
-
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::
|
21
|
+
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
22
22
|
end
|
23
23
|
end
|
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -4,12 +4,12 @@ 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
|
8
|
-
let(:two) { Dentaku::AST::Numeric.new
|
9
|
-
let(:x) { Dentaku::AST::Identifier.new
|
10
|
-
let(:y) { Dentaku::AST::Identifier.new
|
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
11
|
let(:ctx) { {'x' => 1, 'y' => 2} }
|
12
|
-
let(:date) { Dentaku::AST::DateTime.new
|
12
|
+
let(:date) { Dentaku::AST::DateTime.new(Dentaku::Token.new(:datetime, DateTime.new(2020, 4, 16))) }
|
13
13
|
|
14
14
|
it 'performs an arithmetic operation with numeric operands' do
|
15
15
|
expect(add(one, two)).to eq(3)
|
@@ -45,6 +45,21 @@ describe Dentaku::AST::Arithmetic do
|
|
45
45
|
expect(add(x, one, 'x' => '.1')).to eq(1.1)
|
46
46
|
expect { add(x, one, 'x' => 'invalid') }.to raise_error(Dentaku::ArgumentError)
|
47
47
|
expect { add(x, one, 'x' => '') }.to raise_error(Dentaku::ArgumentError)
|
48
|
+
|
49
|
+
int_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1"))
|
50
|
+
int_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1"))
|
51
|
+
decimal_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "1.0"))
|
52
|
+
decimal_neg_one = Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, "-1.0"))
|
53
|
+
|
54
|
+
[int_one, int_neg_one].permutation(2).each do |(left, right)|
|
55
|
+
expect(add(left, right).class).to eq(Integer)
|
56
|
+
end
|
57
|
+
|
58
|
+
[decimal_one, decimal_neg_one].each do |left|
|
59
|
+
[int_one, int_neg_one, decimal_one, decimal_neg_one].each do |right|
|
60
|
+
expect(add(left, right).class).to eq(BigDecimal)
|
61
|
+
end
|
62
|
+
end
|
48
63
|
end
|
49
64
|
|
50
65
|
it 'performs arithmetic on arrays' do
|
data/spec/ast/avg_spec.rb
CHANGED
@@ -3,6 +3,11 @@ require 'dentaku/ast/functions/avg'
|
|
3
3
|
require 'dentaku'
|
4
4
|
|
5
5
|
describe 'Dentaku::AST::Function::Avg' do
|
6
|
+
it 'returns the average of an array of Numeric values as BigDecimal' do
|
7
|
+
result = Dentaku('AVG(1, 2)')
|
8
|
+
expect(result).to eq(1.5)
|
9
|
+
end
|
10
|
+
|
6
11
|
it 'returns the average of an array of Numeric values' do
|
7
12
|
result = Dentaku('AVG(1, x, 1.8)', x: 2.3)
|
8
13
|
expect(result).to eq(1.7)
|
data/spec/ast/comparator_spec.rb
CHANGED
@@ -5,7 +5,9 @@ require 'dentaku/token'
|
|
5
5
|
|
6
6
|
describe Dentaku::AST::Comparator do
|
7
7
|
let(:one) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 1) }
|
8
|
+
let(:one_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '1') }
|
8
9
|
let(:two) { Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, 2) }
|
10
|
+
let(:two_str) { Dentaku::AST::String.new Dentaku::Token.new(:string, '2') }
|
9
11
|
let(:x) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'x') }
|
10
12
|
let(:y) { Dentaku::AST::Identifier.new Dentaku::Token.new(:identifier, 'y') }
|
11
13
|
let(:nilly) do
|
@@ -21,6 +23,12 @@ describe Dentaku::AST::Comparator do
|
|
21
23
|
expect(equal(x, y).value(ctx)).to be_falsey
|
22
24
|
end
|
23
25
|
|
26
|
+
it 'performs conversion from string to numeric operands' do
|
27
|
+
expect(less_than(one, two_str).value(ctx)).to be_truthy
|
28
|
+
expect(less_than(one_str, two_str).value(ctx)).to be_truthy
|
29
|
+
expect(less_than(one_str, two).value(ctx)).to be_truthy
|
30
|
+
end
|
31
|
+
|
24
32
|
it 'raises a dentaku argument error when incorrect arguments are passed in' do
|
25
33
|
expect { less_than(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
26
34
|
expect { less_than_or_equal(one, nilly).value(ctx) }.to raise_error Dentaku::ArgumentError
|
data/spec/ast/filter_spec.rb
CHANGED
@@ -19,7 +19,7 @@ describe Dentaku::AST::Filter do
|
|
19
19
|
|
20
20
|
it 'raises argument error if a string is passed as identifier' do
|
21
21
|
expect { calculator.evaluate!('FILTER({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(
|
22
|
-
Dentaku::
|
22
|
+
Dentaku::ParseError, 'FILTER() requires second argument to be an identifier'
|
23
23
|
)
|
24
24
|
end
|
25
25
|
end
|
data/spec/ast/map_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe Dentaku::AST::Map do
|
|
21
21
|
|
22
22
|
it 'raises argument error if a string is passed as identifier' do
|
23
23
|
expect { calculator.evaluate!('MAP({1, 2, 3}, "val", val + 1)') }.to raise_error(
|
24
|
-
Dentaku::
|
24
|
+
Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
|
25
25
|
)
|
26
26
|
end
|
27
27
|
end
|
data/spec/ast/or_spec.rb
CHANGED
data/spec/ast/pluck_spec.rb
CHANGED
@@ -21,7 +21,7 @@ describe Dentaku::AST::Pluck do
|
|
21
21
|
expect do Dentaku.evaluate!('PLUCK(users, "age")', users: [
|
22
22
|
{name: "Bob", age: 44},
|
23
23
|
{name: "Jane", age: 27}
|
24
|
-
]) end.to raise_error(Dentaku::
|
24
|
+
]) end.to raise_error(Dentaku::ParseError, 'PLUCK() requires second argument to be an identifier')
|
25
25
|
end
|
26
26
|
|
27
27
|
it 'raises argument error if a non array of hashes is passed as collection' do
|
@@ -143,6 +143,15 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
143
143
|
expect(exception.recipient_variable).to eq('more_apples')
|
144
144
|
end
|
145
145
|
|
146
|
+
it 'stores the recipient variable on the exception when there is an ArgumentError' do
|
147
|
+
expressions = {apples: "NULL", more_apples: "1 + apples"}
|
148
|
+
exception = nil
|
149
|
+
described_class.new(expressions, calculator).solve do |ex|
|
150
|
+
exception = ex
|
151
|
+
end
|
152
|
+
expect(exception.recipient_variable).to eq('more_apples')
|
153
|
+
end
|
154
|
+
|
146
155
|
it 'safely handles argument errors' do
|
147
156
|
expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
|
148
157
|
result = described_class.new(expressions, calculator.store(m: 3)).solve
|
data/spec/calculator_spec.rb
CHANGED
@@ -22,6 +22,7 @@ describe Dentaku::Calculator do
|
|
22
22
|
expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
|
23
23
|
expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
|
24
24
|
expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
|
25
|
+
expect(calculator.evaluate('1353+91-1-3322-22')).to eq(-1901)
|
25
26
|
expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
|
26
27
|
expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
|
27
28
|
expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
|
@@ -40,6 +41,8 @@ describe Dentaku::Calculator do
|
|
40
41
|
expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
|
41
42
|
expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
|
42
43
|
expect(calculator.evaluate("5%")).to eq (0.05)
|
44
|
+
expect(calculator.evaluate('1 << 3')).to eq (8)
|
45
|
+
expect(calculator.evaluate('0xFF >> 6')).to eq (3)
|
43
46
|
end
|
44
47
|
|
45
48
|
describe 'evaluate' do
|
@@ -67,6 +70,7 @@ describe Dentaku::Calculator do
|
|
67
70
|
expect(calculator.evaluate('ROUNDDOWN(a)', a: nil)).to be_nil
|
68
71
|
expect(calculator.evaluate('ROUNDUP(a)', a: nil)).to be_nil
|
69
72
|
expect(calculator.evaluate('SUM(a,b)', a: nil, b: nil)).to be_nil
|
73
|
+
expect(calculator.evaluate('1.0 & "bar"')).to be_nil
|
70
74
|
end
|
71
75
|
|
72
76
|
it 'treats explicit nil as logical false' do
|
@@ -82,6 +86,13 @@ describe Dentaku::Calculator do
|
|
82
86
|
end
|
83
87
|
end
|
84
88
|
|
89
|
+
describe 'ast' do
|
90
|
+
it 'raises parsing errors' do
|
91
|
+
expect { calculator.ast('()') }.to raise_error(Dentaku::ParseError)
|
92
|
+
expect { calculator.ast('(}') }.to raise_error(Dentaku::TokenizerError)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
85
96
|
describe 'evaluate!' do
|
86
97
|
it 'raises exception when formula has error' do
|
87
98
|
expect { calculator.evaluate!('1 + + 1') }.to raise_error(Dentaku::ParseError)
|
@@ -108,6 +119,9 @@ describe Dentaku::Calculator do
|
|
108
119
|
expect { calculator.evaluate!('ROUNDDOWN(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
109
120
|
expect { calculator.evaluate!('ROUNDUP(a)', a: nil) }.to raise_error(Dentaku::ArgumentError)
|
110
121
|
expect { calculator.evaluate!('SUM(a,b)', a: nil, b: nil) }.to raise_error(Dentaku::ArgumentError)
|
122
|
+
expect { calculator.evaluate!('"foo" & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
123
|
+
expect { calculator.evaluate!('1.0 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
124
|
+
expect { calculator.evaluate!('1 & "bar"') }.to raise_error(Dentaku::ArgumentError)
|
111
125
|
end
|
112
126
|
|
113
127
|
it 'raises argument error if a function is called with incorrect arity' do
|
@@ -316,6 +330,11 @@ describe Dentaku::Calculator do
|
|
316
330
|
}.not_to raise_error
|
317
331
|
end
|
318
332
|
|
333
|
+
it 'allows to compare "-" or "-."' do
|
334
|
+
expect { calculator.solve("IF('-' = '-', 0, 1)") }.not_to raise_error
|
335
|
+
expect { calculator.solve("IF('-.'= '-.', 0, 1)") }.not_to raise_error
|
336
|
+
end
|
337
|
+
|
319
338
|
it "integrates with custom functions" do
|
320
339
|
calculator.add_function(:custom, :integer, -> { 1 })
|
321
340
|
|
@@ -427,19 +446,42 @@ describe Dentaku::Calculator do
|
|
427
446
|
expect(calculator.evaluate('t1 > 2017-01-02', t1: Time.local(2017, 1, 3).to_datetime)).to be_truthy
|
428
447
|
end
|
429
448
|
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
449
|
+
describe 'disabling date literals' do
|
450
|
+
it 'does not parse formulas with minus signs as dates' do
|
451
|
+
calculator = described_class.new(raw_date_literals: false)
|
452
|
+
expect(calculator.evaluate!('2020-01-01')).to eq(2018)
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
describe 'supports date arithmetic' do
|
457
|
+
it 'from hardcoded string' do
|
458
|
+
expect(calculator.evaluate!('2020-01-01 + 30').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
459
|
+
expect(calculator.evaluate!('2020-01-01 - 1').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
460
|
+
expect(calculator.evaluate!('2020-01-01 - 2019-12-31')).to eq(1)
|
461
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, day)').to_date).to eq(Time.local(2020, 1, 2).to_date)
|
462
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, day)').to_date).to eq(Time.local(2019, 12, 31).to_date)
|
463
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, days)').to_date).to eq(Time.local(2020, 1, 31).to_date)
|
464
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, month)').to_date).to eq(Time.local(2020, 2, 1).to_date)
|
465
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, month)').to_date).to eq(Time.local(2019, 12, 1).to_date)
|
466
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, months)').to_date).to eq(Time.local(2022, 7, 1).to_date)
|
467
|
+
expect(calculator.evaluate!('2020-01-01 + duration(1, year)').to_date).to eq(Time.local(2021, 1, 1).to_date)
|
468
|
+
expect(calculator.evaluate!('2020-01-01 - duration(1, year)').to_date).to eq(Time.local(2019, 1, 1).to_date)
|
469
|
+
expect(calculator.evaluate!('2020-01-01 + duration(30, years)').to_date).to eq(Time.local(2050, 1, 1).to_date)
|
470
|
+
end
|
471
|
+
|
472
|
+
it 'from string variable' do
|
473
|
+
value = '2023-01-01'
|
474
|
+
|
475
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
|
476
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
|
477
|
+
end
|
478
|
+
|
479
|
+
it 'from date object' do
|
480
|
+
value = Date.parse('2023-01-01').to_date
|
481
|
+
|
482
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eql(Date.parse('2023-02-01'))
|
483
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eql(Date.parse('2022-12-01'))
|
484
|
+
end
|
443
485
|
end
|
444
486
|
|
445
487
|
describe 'functions' do
|
@@ -458,6 +500,13 @@ describe Dentaku::Calculator do
|
|
458
500
|
expect(calculator.evaluate('ROUND(apples * 0.93)', apples: 10)).to eq(9)
|
459
501
|
end
|
460
502
|
|
503
|
+
it 'include ABS' do
|
504
|
+
expect(calculator.evaluate('abs(-2.2)')).to eq(2.2)
|
505
|
+
expect(calculator.evaluate('abs(5)')).to eq(5)
|
506
|
+
|
507
|
+
expect(calculator.evaluate('ABS(x * -1)', x: 10)).to eq(10)
|
508
|
+
end
|
509
|
+
|
461
510
|
it 'include NOT' do
|
462
511
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: true)).to be_falsey
|
463
512
|
expect(calculator.evaluate('NOT(some_boolean)', some_boolean: false)).to be_truthy
|
@@ -745,9 +794,9 @@ describe Dentaku::Calculator do
|
|
745
794
|
end
|
746
795
|
end
|
747
796
|
|
748
|
-
describe 'math
|
797
|
+
describe 'math support' do
|
749
798
|
Math.methods(false).each do |method|
|
750
|
-
it method do
|
799
|
+
it "includes `#{method}`" do
|
751
800
|
if Math.method(method).arity == 2
|
752
801
|
expect(calculator.evaluate("#{method}(x,y)", x: 1, y: '2')).to eq(Math.send(method, 1, 2))
|
753
802
|
expect(calculator.evaluate("#{method}(x,y) + 1", x: 1, y: '2')).to be_within(0.00001).of(Math.send(method, 1, 2) + 1)
|
@@ -761,11 +810,16 @@ describe Dentaku::Calculator do
|
|
761
810
|
end
|
762
811
|
end
|
763
812
|
|
764
|
-
it '
|
813
|
+
it 'defines a properly named class to support AST marshaling' do
|
765
814
|
expect {
|
766
815
|
Marshal.dump(calculator.ast('SQRT(20)'))
|
767
816
|
}.not_to raise_error
|
768
817
|
end
|
818
|
+
|
819
|
+
it 'properly handles a Math::DomainError' do
|
820
|
+
expect(calculator.evaluate('asin(2)')).to be_nil
|
821
|
+
expect { calculator.evaluate!('asin(2)') }.to raise_error(Dentaku::MathDomainError)
|
822
|
+
end
|
769
823
|
end
|
770
824
|
|
771
825
|
describe 'disable_cache' do
|
@@ -5,8 +5,7 @@ require 'dentaku/calculator'
|
|
5
5
|
describe Dentaku::Calculator do
|
6
6
|
describe 'functions' do
|
7
7
|
describe 'external functions' do
|
8
|
-
|
9
|
-
let(:with_external_funcs) do
|
8
|
+
let(:custom_calculator) do
|
10
9
|
c = described_class.new
|
11
10
|
|
12
11
|
c.add_function(:now, :string, -> { Time.now.to_s })
|
@@ -22,30 +21,30 @@ describe Dentaku::Calculator do
|
|
22
21
|
end
|
23
22
|
|
24
23
|
it 'includes NOW' do
|
25
|
-
now =
|
24
|
+
now = custom_calculator.evaluate('NOW()')
|
26
25
|
expect(now).not_to be_nil
|
27
26
|
expect(now).not_to be_empty
|
28
27
|
end
|
29
28
|
|
30
29
|
it 'includes POW' do
|
31
|
-
expect(
|
32
|
-
expect(
|
33
|
-
expect(
|
30
|
+
expect(custom_calculator.evaluate('POW(2,3)')).to eq(8)
|
31
|
+
expect(custom_calculator.evaluate('POW(3,2)')).to eq(9)
|
32
|
+
expect(custom_calculator.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
|
34
33
|
end
|
35
34
|
|
36
35
|
it 'includes BIGGEST' do
|
37
|
-
expect(
|
36
|
+
expect(custom_calculator.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
|
38
37
|
end
|
39
38
|
|
40
39
|
it 'includes SMALLEST' do
|
41
|
-
expect(
|
40
|
+
expect(custom_calculator.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
42
41
|
end
|
43
42
|
|
44
43
|
it 'includes OPTIONAL' do
|
45
|
-
expect(
|
46
|
-
expect(
|
47
|
-
expect {
|
48
|
-
expect {
|
44
|
+
expect(custom_calculator.evaluate('OPTIONAL(1,2)')).to eq(3)
|
45
|
+
expect(custom_calculator.evaluate('OPTIONAL(1,2,3)')).to eq(6)
|
46
|
+
expect { custom_calculator.dependencies('OPTIONAL()') }.to raise_error(Dentaku::ParseError)
|
47
|
+
expect { custom_calculator.dependencies('OPTIONAL(1,2,3,4)') }.to raise_error(Dentaku::ParseError)
|
49
48
|
end
|
50
49
|
|
51
50
|
it 'supports array parameters' do
|
@@ -62,6 +61,66 @@ describe Dentaku::Calculator do
|
|
62
61
|
end
|
63
62
|
end
|
64
63
|
|
64
|
+
describe 'with callbacks' do
|
65
|
+
let(:custom_calculator) do
|
66
|
+
c = described_class.new
|
67
|
+
|
68
|
+
@counts = Hash.new(0)
|
69
|
+
|
70
|
+
@initial_time = "2023-02-03"
|
71
|
+
@last_time = @initial_time
|
72
|
+
|
73
|
+
c.add_function(
|
74
|
+
:reverse,
|
75
|
+
:stringl,
|
76
|
+
->(a) { a.reverse },
|
77
|
+
lambda do |args|
|
78
|
+
args.each do |arg|
|
79
|
+
@counts[arg.value] += 1 if arg.type == :string
|
80
|
+
end
|
81
|
+
end
|
82
|
+
)
|
83
|
+
|
84
|
+
fns = [
|
85
|
+
[:biggest_callback, :numeric, ->(*args) { args.max }, ->(args) { args.each { |arg| raise Dentaku::ArgumentError unless arg.type == :numeric } }],
|
86
|
+
[:pythagoras, :numeric, ->(l1, l2) { Math.sqrt(l1**2 + l2**2) }, ->(e) { @last_time = Time.now.to_s }],
|
87
|
+
[:callback_lambda, :string, ->() { " " }, ->() { "lambda executed" }],
|
88
|
+
[:no_lambda_function, :numeric, ->(a) { a**a }],
|
89
|
+
]
|
90
|
+
|
91
|
+
c.add_functions(fns)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'includes BIGGEST_CALLBACK' do
|
95
|
+
expect(custom_calculator.evaluate('BIGGEST_CALLBACK(1, 2, 5, 4)')).to eq(5)
|
96
|
+
expect { custom_calculator.dependencies('BIGGEST_CALLBACK(1, 3, 6, "hi", 10)') }.to raise_error(Dentaku::ArgumentError)
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'includes REVERSE' do
|
100
|
+
expect(custom_calculator.evaluate('REVERSE(\'Dentaku\')')).to eq('ukatneD')
|
101
|
+
expect { custom_calculator.evaluate('REVERSE(22)') }.to raise_error(NoMethodError)
|
102
|
+
expect(@counts["Dentaku"]).to eq(1)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'includes PYTHAGORAS' do
|
106
|
+
expect(custom_calculator.evaluate('PYTHAGORAS(8, 7)')).to eq(10.63014581273465)
|
107
|
+
expect(custom_calculator.evaluate('PYTHAGORAS(3, 4)')).to eq(5)
|
108
|
+
expect(@last_time).not_to eq(@initial_time)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'exposes the `callback` method of a function' do
|
112
|
+
expect(Dentaku::AST::Function::Callback_lambda.callback.call()).to eq("lambda executed")
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'does not add a `callback` method to built-in functions' do
|
116
|
+
expect { Dentaku::AST::If.callback.call }.to raise_error(NoMethodError)
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'defaults `callback` method to nil if not specified' do
|
120
|
+
expect(Dentaku::AST::Function::No_lambda_function.callback).to eq(nil)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
65
124
|
it 'allows registering "bang" functions' do
|
66
125
|
calculator = described_class.new
|
67
126
|
calculator.add_function(:hey!, :string, -> { "hey!" })
|
@@ -82,24 +141,36 @@ describe Dentaku::Calculator do
|
|
82
141
|
end
|
83
142
|
|
84
143
|
it 'does not store functions across all calculators' do
|
85
|
-
calculator1 =
|
144
|
+
calculator1 = described_class.new
|
86
145
|
calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
|
87
146
|
|
88
|
-
calculator2 =
|
147
|
+
calculator2 = described_class.new
|
89
148
|
calculator2.add_function(:my_function, :numeric, ->(x) { 4 * x + 3 })
|
90
149
|
|
91
150
|
expect(calculator1.evaluate!("1 + my_function(2)")). to eq(1 + 2 * 2 + 1)
|
92
151
|
expect(calculator2.evaluate!("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
|
93
152
|
|
94
153
|
expect {
|
95
|
-
|
154
|
+
described_class.new.evaluate!("1 + my_function(2)")
|
96
155
|
}.to raise_error(Dentaku::ParseError)
|
97
156
|
end
|
98
157
|
|
99
158
|
describe 'Dentaku::Calculator.add_function' do
|
100
|
-
it 'adds to default/global function registry' do
|
101
|
-
|
102
|
-
expect(
|
159
|
+
it 'adds a function to default/global function registry' do
|
160
|
+
described_class.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
|
161
|
+
expect(described_class.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
describe 'Dentaku::Calculator.add_functions' do
|
166
|
+
it 'adds multiple functions to default/global function registry' do
|
167
|
+
described_class.add_functions([
|
168
|
+
[:cube, :numeric, ->(x) { x**3 }],
|
169
|
+
[:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c,i| i.even? ? c.upcase : c.downcase }.join() }],
|
170
|
+
])
|
171
|
+
|
172
|
+
expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
|
173
|
+
expect(described_class.new.evaluate("spongebob('How are you today?')")).to eq("HoW ArE YoU ToDaY?")
|
103
174
|
end
|
104
175
|
end
|
105
176
|
end
|
data/spec/print_visitor_spec.rb
CHANGED
@@ -8,6 +8,12 @@ describe Dentaku::PrintVisitor do
|
|
8
8
|
expect(repr).to eq('5 + 4')
|
9
9
|
end
|
10
10
|
|
11
|
+
it 'handles grouping correctly' do
|
12
|
+
formula = '10 - (0 - 10)'
|
13
|
+
repr = roundtrip(formula)
|
14
|
+
expect(repr).to eq(formula)
|
15
|
+
end
|
16
|
+
|
11
17
|
it 'quotes string literals' do
|
12
18
|
repr = roundtrip('Concat(\'a\', "B")')
|
13
19
|
expect(repr).to eq('CONCAT("a", "B")')
|
data/spec/tokenizer_spec.rb
CHANGED
@@ -89,6 +89,18 @@ describe Dentaku::Tokenizer do
|
|
89
89
|
expect(tokens.map(&:value)).to eq([2, :bitand, 3])
|
90
90
|
end
|
91
91
|
|
92
|
+
it 'tokenizes bitwise SHIFT LEFT' do
|
93
|
+
tokens = tokenizer.tokenize('2 << 3')
|
94
|
+
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
95
|
+
expect(tokens.map(&:value)).to eq([2, :bitshiftleft, 3])
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'tokenizes bitwise SHIFT RIGHT' do
|
99
|
+
tokens = tokenizer.tokenize('2 >> 3')
|
100
|
+
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
101
|
+
expect(tokens.map(&:value)).to eq([2, :bitshiftright, 3])
|
102
|
+
end
|
103
|
+
|
92
104
|
it 'ignores whitespace' do
|
93
105
|
tokens = tokenizer.tokenize('1 / 1 ')
|
94
106
|
expect(tokens.map(&:category)).to eq([:numeric, :operator, :numeric])
|
data/spec/visitor/infix_spec.rb
CHANGED
@@ -10,6 +10,21 @@ class ArrayProcessor
|
|
10
10
|
@expression = []
|
11
11
|
end
|
12
12
|
|
13
|
+
def visit_array(node)
|
14
|
+
@expression << "{"
|
15
|
+
|
16
|
+
head, *tail = node.value
|
17
|
+
|
18
|
+
process(head) if head
|
19
|
+
|
20
|
+
tail.each do |v|
|
21
|
+
@expression << ","
|
22
|
+
process(v)
|
23
|
+
end
|
24
|
+
|
25
|
+
@expression << "}"
|
26
|
+
end
|
27
|
+
|
13
28
|
def process(node)
|
14
29
|
@expression << node.to_s
|
15
30
|
end
|
@@ -22,10 +37,16 @@ RSpec.describe Dentaku::Visitor::Infix do
|
|
22
37
|
expect(processor.expression).to eq ['5', '+', '3']
|
23
38
|
end
|
24
39
|
|
40
|
+
it 'supports array nodes' do
|
41
|
+
processor = ArrayProcessor.new
|
42
|
+
processor.visit(ast('{1, 2, 3}'))
|
43
|
+
expect(processor.expression).to eq ['{', '1', ',', '2', ',', '3', '}']
|
44
|
+
end
|
45
|
+
|
25
46
|
private
|
26
47
|
|
27
48
|
def ast(expression)
|
28
49
|
tokens = Dentaku::Tokenizer.new.tokenize(expression)
|
29
50
|
Dentaku::Parser.new(tokens).parse
|
30
51
|
end
|
31
|
-
end
|
52
|
+
end
|
data/spec/visitor_spec.rb
CHANGED
@@ -90,6 +90,7 @@ describe TestVisitor do
|
|
90
90
|
def generic_subclasses
|
91
91
|
[
|
92
92
|
:Arithmetic,
|
93
|
+
:Bitwise,
|
93
94
|
:Combinator,
|
94
95
|
:Comparator,
|
95
96
|
:Function,
|
@@ -111,9 +112,9 @@ describe TestVisitor do
|
|
111
112
|
visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
|
112
113
|
visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
|
113
114
|
visit_nodes('case (a % 5) when 0 then a else b end')
|
114
|
-
visit_nodes('0xCAFE & 0xDECAF | 0xBEEF')
|
115
|
+
visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
|
115
116
|
visit_nodes('2017-12-24 23:59:59')
|
116
|
-
visit_nodes('ALL({1, 2, 3},
|
117
|
+
visit_nodes('ALL({1, 2, 3}, val, val % 2 == 0)')
|
117
118
|
visit_nodes('ANY(vals, val, val > 1)')
|
118
119
|
visit_nodes('COUNT({1, 2, 3})')
|
119
120
|
visit_nodes('PLUCK(users, age)')
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dentaku
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.5.
|
4
|
+
version: 3.5.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-12-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -170,6 +170,7 @@ files:
|
|
170
170
|
- lib/dentaku/ast/datetime.rb
|
171
171
|
- lib/dentaku/ast/function.rb
|
172
172
|
- lib/dentaku/ast/function_registry.rb
|
173
|
+
- lib/dentaku/ast/functions/abs.rb
|
173
174
|
- lib/dentaku/ast/functions/all.rb
|
174
175
|
- lib/dentaku/ast/functions/and.rb
|
175
176
|
- lib/dentaku/ast/functions/any.rb
|
@@ -220,6 +221,7 @@ files:
|
|
220
221
|
- lib/dentaku/tokenizer.rb
|
221
222
|
- lib/dentaku/version.rb
|
222
223
|
- lib/dentaku/visitor/infix.rb
|
224
|
+
- spec/ast/abs_spec.rb
|
223
225
|
- spec/ast/addition_spec.rb
|
224
226
|
- spec/ast/all_spec.rb
|
225
227
|
- spec/ast/and_function_spec.rb
|
@@ -283,11 +285,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
283
285
|
- !ruby/object:Gem::Version
|
284
286
|
version: '0'
|
285
287
|
requirements: []
|
286
|
-
rubygems_version: 3.3.
|
288
|
+
rubygems_version: 3.3.7
|
287
289
|
signing_key:
|
288
290
|
specification_version: 4
|
289
291
|
summary: A formula language parser and evaluator
|
290
292
|
test_files:
|
293
|
+
- spec/ast/abs_spec.rb
|
291
294
|
- spec/ast/addition_spec.rb
|
292
295
|
- spec/ast/all_spec.rb
|
293
296
|
- spec/ast/and_function_spec.rb
|