dentaku 3.5.1 → 3.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -1
- data/README.md +1 -1
- data/lib/dentaku/ast/arithmetic.rb +34 -12
- data/lib/dentaku/ast/comparators.rb +1 -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/if.rb +3 -7
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/reduce.rb +61 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
- data/lib/dentaku/ast.rb +4 -1
- data/lib/dentaku/calculator.rb +17 -10
- data/lib/dentaku/date_arithmetic.rb +8 -2
- data/lib/dentaku/exceptions.rb +17 -3
- data/lib/dentaku/parser.rb +20 -9
- data/lib/dentaku/print_visitor.rb +16 -5
- data/lib/dentaku/token_scanner.rb +12 -3
- 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/addition_spec.rb +4 -4
- data/spec/ast/all_spec.rb +1 -1
- data/spec/ast/any_spec.rb +1 -1
- data/spec/ast/arithmetic_spec.rb +61 -12
- data/spec/ast/avg_spec.rb +5 -0
- data/spec/ast/division_spec.rb +25 -0
- data/spec/ast/filter_spec.rb +1 -1
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +1 -1
- data/spec/ast/pluck_spec.rb +1 -1
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/bulk_expression_solver_spec.rb +17 -0
- data/spec/calculator_spec.rb +99 -17
- data/spec/external_function_spec.rb +89 -18
- data/spec/parser_spec.rb +3 -0
- data/spec/print_visitor_spec.rb +6 -0
- data/spec/tokenizer_spec.rb +6 -4
- data/spec/visitor/infix_spec.rb +22 -1
- data/spec/visitor_spec.rb +2 -1
- metadata +12 -3
@@ -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/parser_spec.rb
CHANGED
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
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'dentaku/exceptions'
|
1
2
|
require 'dentaku/tokenizer'
|
2
3
|
|
3
4
|
describe Dentaku::Tokenizer do
|
@@ -234,9 +235,9 @@ describe Dentaku::Tokenizer do
|
|
234
235
|
end
|
235
236
|
|
236
237
|
it 'tokenizes Time literals' do
|
237
|
-
tokens = tokenizer.tokenize('2017-01-01 2017-01-2 2017-1-03 2017-01-04 12:23:42 2017-1-5 1:2:3 2017-1-06 1:02:30 2017-01-07 12:34:56 Z 2017-01-08 1:2:3 +0800')
|
238
|
-
expect(tokens.length).to eq(
|
239
|
-
expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
|
238
|
+
tokens = tokenizer.tokenize('2017-01-01 2017-01-2 2017-1-03 2017-01-04 12:23:42 2017-1-5 1:2:3 2017-1-06 1:02:30 2017-01-07 12:34:56 Z 2017-01-08 1:2:3 +0800 2017-01-08T01:02:03.456Z')
|
239
|
+
expect(tokens.length).to eq(9)
|
240
|
+
expect(tokens.map(&:category)).to eq([:datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime, :datetime])
|
240
241
|
expect(tokens.map(&:value)).to eq([
|
241
242
|
Time.local(2017, 1, 1).to_datetime,
|
242
243
|
Time.local(2017, 1, 2).to_datetime,
|
@@ -245,7 +246,8 @@ describe Dentaku::Tokenizer do
|
|
245
246
|
Time.local(2017, 1, 5, 1, 2, 3).to_datetime,
|
246
247
|
Time.local(2017, 1, 6, 1, 2, 30).to_datetime,
|
247
248
|
Time.utc(2017, 1, 7, 12, 34, 56).to_datetime,
|
248
|
-
Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime
|
249
|
+
Time.new(2017, 1, 8, 1, 2, 3, "+08:00").to_datetime,
|
250
|
+
Time.utc(2017, 1, 8, 1, 2, 3, 456000).to_datetime
|
249
251
|
])
|
250
252
|
end
|
251
253
|
|
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
@@ -114,7 +114,7 @@ describe TestVisitor do
|
|
114
114
|
visit_nodes('case (a % 5) when 0 then a else b end')
|
115
115
|
visit_nodes('0xCAFE & (0xDECAF << 3) | (0xBEEF >> 5)')
|
116
116
|
visit_nodes('2017-12-24 23:59:59')
|
117
|
-
visit_nodes('ALL({1, 2, 3},
|
117
|
+
visit_nodes('ALL({1, 2, 3}, val, val % 2 == 0)')
|
118
118
|
visit_nodes('ANY(vals, val, val > 1)')
|
119
119
|
visit_nodes('COUNT({1, 2, 3})')
|
120
120
|
visit_nodes('PLUCK(users, age)')
|
@@ -122,6 +122,7 @@ describe TestVisitor do
|
|
122
122
|
visit_nodes('duration(1, day)')
|
123
123
|
visit_nodes('MAP(vals, val, val + 1)')
|
124
124
|
visit_nodes('FILTER(vals, val, val > 1)')
|
125
|
+
visit_nodes('REDUCE(vals, memo, val, memo + val)')
|
125
126
|
|
126
127
|
@expected = Set.new(Dentaku::AST::constants - generic_subclasses)
|
127
128
|
expect(@visited.sort).to eq(@expected.sort)
|
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.3
|
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: 2024-07-04 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
|
@@ -179,6 +180,7 @@ files:
|
|
179
180
|
- lib/dentaku/ast/functions/enum.rb
|
180
181
|
- lib/dentaku/ast/functions/filter.rb
|
181
182
|
- lib/dentaku/ast/functions/if.rb
|
183
|
+
- lib/dentaku/ast/functions/intercept.rb
|
182
184
|
- lib/dentaku/ast/functions/map.rb
|
183
185
|
- lib/dentaku/ast/functions/max.rb
|
184
186
|
- lib/dentaku/ast/functions/min.rb
|
@@ -186,6 +188,7 @@ files:
|
|
186
188
|
- lib/dentaku/ast/functions/not.rb
|
187
189
|
- lib/dentaku/ast/functions/or.rb
|
188
190
|
- lib/dentaku/ast/functions/pluck.rb
|
191
|
+
- lib/dentaku/ast/functions/reduce.rb
|
189
192
|
- lib/dentaku/ast/functions/round.rb
|
190
193
|
- lib/dentaku/ast/functions/rounddown.rb
|
191
194
|
- lib/dentaku/ast/functions/roundup.rb
|
@@ -220,6 +223,7 @@ files:
|
|
220
223
|
- lib/dentaku/tokenizer.rb
|
221
224
|
- lib/dentaku/version.rb
|
222
225
|
- lib/dentaku/visitor/infix.rb
|
226
|
+
- spec/ast/abs_spec.rb
|
223
227
|
- spec/ast/addition_spec.rb
|
224
228
|
- spec/ast/all_spec.rb
|
225
229
|
- spec/ast/and_function_spec.rb
|
@@ -233,6 +237,7 @@ files:
|
|
233
237
|
- spec/ast/division_spec.rb
|
234
238
|
- spec/ast/filter_spec.rb
|
235
239
|
- spec/ast/function_spec.rb
|
240
|
+
- spec/ast/intercept_spec.rb
|
236
241
|
- spec/ast/map_spec.rb
|
237
242
|
- spec/ast/max_spec.rb
|
238
243
|
- spec/ast/min_spec.rb
|
@@ -242,6 +247,7 @@ files:
|
|
242
247
|
- spec/ast/numeric_spec.rb
|
243
248
|
- spec/ast/or_spec.rb
|
244
249
|
- spec/ast/pluck_spec.rb
|
250
|
+
- spec/ast/reduce_spec.rb
|
245
251
|
- spec/ast/round_spec.rb
|
246
252
|
- spec/ast/rounddown_spec.rb
|
247
253
|
- spec/ast/roundup_spec.rb
|
@@ -283,11 +289,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
283
289
|
- !ruby/object:Gem::Version
|
284
290
|
version: '0'
|
285
291
|
requirements: []
|
286
|
-
rubygems_version: 3.3.
|
292
|
+
rubygems_version: 3.3.9
|
287
293
|
signing_key:
|
288
294
|
specification_version: 4
|
289
295
|
summary: A formula language parser and evaluator
|
290
296
|
test_files:
|
297
|
+
- spec/ast/abs_spec.rb
|
291
298
|
- spec/ast/addition_spec.rb
|
292
299
|
- spec/ast/all_spec.rb
|
293
300
|
- spec/ast/and_function_spec.rb
|
@@ -301,6 +308,7 @@ test_files:
|
|
301
308
|
- spec/ast/division_spec.rb
|
302
309
|
- spec/ast/filter_spec.rb
|
303
310
|
- spec/ast/function_spec.rb
|
311
|
+
- spec/ast/intercept_spec.rb
|
304
312
|
- spec/ast/map_spec.rb
|
305
313
|
- spec/ast/max_spec.rb
|
306
314
|
- spec/ast/min_spec.rb
|
@@ -310,6 +318,7 @@ test_files:
|
|
310
318
|
- spec/ast/numeric_spec.rb
|
311
319
|
- spec/ast/or_spec.rb
|
312
320
|
- spec/ast/pluck_spec.rb
|
321
|
+
- spec/ast/reduce_spec.rb
|
313
322
|
- spec/ast/round_spec.rb
|
314
323
|
- spec/ast/rounddown_spec.rb
|
315
324
|
- spec/ast/roundup_spec.rb
|