dentaku 3.5.1 → 3.5.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -1
  3. data/README.md +1 -1
  4. data/lib/dentaku/ast/arithmetic.rb +34 -12
  5. data/lib/dentaku/ast/comparators.rb +1 -2
  6. data/lib/dentaku/ast/function_registry.rb +10 -1
  7. data/lib/dentaku/ast/functions/abs.rb +5 -0
  8. data/lib/dentaku/ast/functions/avg.rb +1 -1
  9. data/lib/dentaku/ast/functions/enum.rb +5 -4
  10. data/lib/dentaku/ast/functions/if.rb +3 -7
  11. data/lib/dentaku/ast/functions/intercept.rb +33 -0
  12. data/lib/dentaku/ast/functions/reduce.rb +61 -0
  13. data/lib/dentaku/ast/functions/ruby_math.rb +2 -0
  14. data/lib/dentaku/ast.rb +4 -1
  15. data/lib/dentaku/calculator.rb +17 -10
  16. data/lib/dentaku/date_arithmetic.rb +8 -2
  17. data/lib/dentaku/exceptions.rb +17 -3
  18. data/lib/dentaku/parser.rb +20 -9
  19. data/lib/dentaku/print_visitor.rb +16 -5
  20. data/lib/dentaku/token_scanner.rb +12 -3
  21. data/lib/dentaku/tokenizer.rb +7 -3
  22. data/lib/dentaku/version.rb +1 -1
  23. data/lib/dentaku/visitor/infix.rb +4 -0
  24. data/spec/ast/abs_spec.rb +26 -0
  25. data/spec/ast/addition_spec.rb +4 -4
  26. data/spec/ast/all_spec.rb +1 -1
  27. data/spec/ast/any_spec.rb +1 -1
  28. data/spec/ast/arithmetic_spec.rb +61 -12
  29. data/spec/ast/avg_spec.rb +5 -0
  30. data/spec/ast/division_spec.rb +25 -0
  31. data/spec/ast/filter_spec.rb +1 -1
  32. data/spec/ast/intercept_spec.rb +30 -0
  33. data/spec/ast/map_spec.rb +1 -1
  34. data/spec/ast/pluck_spec.rb +1 -1
  35. data/spec/ast/reduce_spec.rb +22 -0
  36. data/spec/bulk_expression_solver_spec.rb +17 -0
  37. data/spec/calculator_spec.rb +99 -17
  38. data/spec/external_function_spec.rb +89 -18
  39. data/spec/parser_spec.rb +3 -0
  40. data/spec/print_visitor_spec.rb +6 -0
  41. data/spec/tokenizer_spec.rb +6 -4
  42. data/spec/visitor/infix_spec.rb +22 -1
  43. data/spec/visitor_spec.rb +2 -1
  44. 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 = with_external_funcs.evaluate('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(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
32
- expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
33
- expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
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(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
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(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
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(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)
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 = Dentaku::Calculator.new
144
+ calculator1 = described_class.new
86
145
  calculator1.add_function(:my_function, :numeric, ->(x) { 2 * x + 1 })
87
146
 
88
- calculator2 = Dentaku::Calculator.new
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
- Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
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
- 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)
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
@@ -79,6 +79,9 @@ describe Dentaku::Parser do
79
79
  it 'evaluates arrays' do
80
80
  node = parse('{1, 2, 3}')
81
81
  expect(node.value).to eq([1, 2, 3])
82
+
83
+ node = parse('{}')
84
+ expect(node.value).to eq([])
82
85
  end
83
86
 
84
87
  context 'invalid expression' do
@@ -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")')
@@ -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(8)
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
 
@@ -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}, "val", val % 2 == 0)')
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.1
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: 2022-10-24 00:00:00.000000000 Z
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.7
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