dentaku 3.3.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -8
  3. data/.travis.yml +3 -4
  4. data/CHANGELOG.md +37 -1
  5. data/README.md +2 -2
  6. data/dentaku.gemspec +0 -2
  7. data/lib/dentaku.rb +14 -6
  8. data/lib/dentaku/ast.rb +5 -0
  9. data/lib/dentaku/ast/access.rb +15 -1
  10. data/lib/dentaku/ast/arithmetic.rb +28 -5
  11. data/lib/dentaku/ast/array.rb +15 -1
  12. data/lib/dentaku/ast/case.rb +8 -0
  13. data/lib/dentaku/ast/case/case_conditional.rb +8 -0
  14. data/lib/dentaku/ast/case/case_else.rb +12 -4
  15. data/lib/dentaku/ast/case/case_switch_variable.rb +8 -0
  16. data/lib/dentaku/ast/case/case_then.rb +12 -4
  17. data/lib/dentaku/ast/case/case_when.rb +12 -4
  18. data/lib/dentaku/ast/function.rb +10 -1
  19. data/lib/dentaku/ast/function_registry.rb +21 -0
  20. data/lib/dentaku/ast/functions/all.rb +36 -0
  21. data/lib/dentaku/ast/functions/any.rb +36 -0
  22. data/lib/dentaku/ast/functions/avg.rb +2 -2
  23. data/lib/dentaku/ast/functions/count.rb +8 -0
  24. data/lib/dentaku/ast/functions/duration.rb +51 -0
  25. data/lib/dentaku/ast/functions/if.rb +15 -2
  26. data/lib/dentaku/ast/functions/map.rb +36 -0
  27. data/lib/dentaku/ast/functions/mul.rb +3 -2
  28. data/lib/dentaku/ast/functions/pluck.rb +29 -0
  29. data/lib/dentaku/ast/functions/round.rb +1 -1
  30. data/lib/dentaku/ast/functions/rounddown.rb +1 -1
  31. data/lib/dentaku/ast/functions/roundup.rb +1 -1
  32. data/lib/dentaku/ast/functions/ruby_math.rb +47 -3
  33. data/lib/dentaku/ast/functions/string_functions.rb +68 -4
  34. data/lib/dentaku/ast/functions/sum.rb +3 -2
  35. data/lib/dentaku/ast/grouping.rb +3 -1
  36. data/lib/dentaku/ast/identifier.rb +5 -1
  37. data/lib/dentaku/ast/negation.rb +3 -1
  38. data/lib/dentaku/ast/node.rb +4 -0
  39. data/lib/dentaku/ast/operation.rb +8 -0
  40. data/lib/dentaku/bulk_expression_solver.rb +36 -25
  41. data/lib/dentaku/calculator.rb +19 -6
  42. data/lib/dentaku/date_arithmetic.rb +45 -0
  43. data/lib/dentaku/exceptions.rb +4 -4
  44. data/lib/dentaku/flat_hash.rb +7 -0
  45. data/lib/dentaku/parser.rb +14 -3
  46. data/lib/dentaku/tokenizer.rb +1 -1
  47. data/lib/dentaku/version.rb +1 -1
  48. data/spec/ast/addition_spec.rb +6 -0
  49. data/spec/ast/arithmetic_spec.rb +41 -13
  50. data/spec/ast/avg_spec.rb +4 -0
  51. data/spec/ast/division_spec.rb +6 -0
  52. data/spec/ast/function_spec.rb +1 -1
  53. data/spec/ast/mul_spec.rb +4 -0
  54. data/spec/ast/negation_spec.rb +48 -0
  55. data/spec/ast/node_spec.rb +4 -1
  56. data/spec/ast/round_spec.rb +10 -0
  57. data/spec/ast/rounddown_spec.rb +10 -0
  58. data/spec/ast/roundup_spec.rb +10 -0
  59. data/spec/ast/string_functions_spec.rb +35 -0
  60. data/spec/ast/sum_spec.rb +4 -0
  61. data/spec/bulk_expression_solver_spec.rb +27 -0
  62. data/spec/calculator_spec.rb +144 -3
  63. data/spec/dentaku_spec.rb +18 -5
  64. data/spec/external_function_spec.rb +29 -5
  65. data/spec/parser_spec.rb +13 -0
  66. data/spec/tokenizer_spec.rb +24 -5
  67. metadata +11 -4
@@ -27,13 +27,26 @@ describe Dentaku do
27
27
  end
28
28
 
29
29
  it 'evaluates with class-level shortcut functions' do
30
- expect(Dentaku.evaluate('2+2')).to eq(4)
31
- expect(Dentaku.evaluate!('2+2')).to eq(4)
32
- expect { Dentaku.evaluate!('a+1') }.to raise_error(Dentaku::UnboundVariableError)
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
- Dentaku.aliases = { roundup: ['roundupup'] }
37
- expect(Dentaku.evaluate('roundupup(6.1)')).to eq(7)
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,17 +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(1 + 2 * 2 + 1)
70
- expect(calculator2.evaluate("1 + my_function(2)")). to eq(1 + 4 * 2 + 3)
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
94
  expect {
73
95
  Dentaku::Calculator.new.evaluate!("1 + my_function(2)")
74
96
  }.to raise_error(Dentaku::ParseError)
75
97
  end
76
98
 
77
- it 'self.add_function adds to default/global function registry' do
78
- Dentaku::Calculator.add_function(:global_function, :numeric, ->(x) { 10 + x**2 })
79
- expect(Dentaku::Calculator.new.evaluate("global_function(3) + 5")).to eq(10 + 3**2 + 5)
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
80
104
  end
81
105
  end
82
106
  end
@@ -71,6 +71,11 @@ describe Dentaku::Parser do
71
71
  expect(node.value("x" => 3)).to eq(4)
72
72
  end
73
73
 
74
+ it 'evaluates arrays' do
75
+ node = parse('{1, 2, 3}')
76
+ expect(node.value).to eq([1, 2, 3])
77
+ end
78
+
74
79
  context 'invalid expression' do
75
80
  it 'raises a parse error for bad math' do
76
81
  expect {
@@ -97,6 +102,14 @@ describe Dentaku::Parser do
97
102
  expect {
98
103
  parse("5 + 5, x")
99
104
  }.to raise_error(Dentaku::ParseError)
105
+
106
+ expect {
107
+ parse("{1, 2, }")
108
+ }.to raise_error(Dentaku::ParseError)
109
+
110
+ expect {
111
+ parse("CONCAT('1', '2', )")
112
+ }.to raise_error(Dentaku::ParseError)
100
113
  end
101
114
 
102
115
  it 'raises parse errors for malformed case statements' do
@@ -231,15 +231,15 @@ describe Dentaku::Tokenizer do
231
231
  ])
232
232
  end
233
233
 
234
- describe 'functions' do
235
- it 'include IF' do
234
+ describe 'tokenizing function calls' do
235
+ it 'handles IF' do
236
236
  tokens = tokenizer.tokenize('if(x < 10, y, z)')
237
237
  expect(tokens.length).to eq(10)
238
238
  expect(tokens.map(&:category)).to eq([:function, :grouping, :identifier, :comparator, :numeric, :grouping, :identifier, :grouping, :identifier, :grouping])
239
239
  expect(tokens.map(&:value)).to eq([:if, :open, 'x', :lt, 10, :comma, 'y', :comma, 'z', :close])
240
240
  end
241
241
 
242
- it 'include ROUND/UP/DOWN' do
242
+ it 'handles ROUND/UP/DOWN' do
243
243
  tokens = tokenizer.tokenize('round(8.2)')
244
244
  expect(tokens.length).to eq(4)
245
245
  expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :grouping])
@@ -261,13 +261,32 @@ describe Dentaku::Tokenizer do
261
261
  expect(tokens.map(&:value)).to eq([:rounddown, :open, BigDecimal('8.2'), :close])
262
262
  end
263
263
 
264
- it 'include NOT' do
264
+ it 'handles NOT' do
265
265
  tokens = tokenizer.tokenize('not(8 < 5)')
266
266
  expect(tokens.length).to eq(6)
267
267
  expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :comparator, :numeric, :grouping])
268
268
  expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
269
269
  end
270
270
 
271
+ it 'handles ANY/ALL' do
272
+ %i( any all ).each do |fn|
273
+ tokens = tokenizer.tokenize("#{fn}(users, u, u.age > 18)")
274
+ expect(tokens.length).to eq(10)
275
+ expect(tokens.map { |t| [t.category, t.value] }).to eq([
276
+ [:function, fn ], # function call (any/all)
277
+ [:grouping, :open ], # (
278
+ [:identifier, "users"], # users
279
+ [:grouping, :comma ], # ,
280
+ [:identifier, "u" ], # u
281
+ [:grouping, :comma ], # ,
282
+ [:identifier, "u.age"], # u.age
283
+ [:comparator, :gt ], # >
284
+ [:numeric, 18 ], # 18
285
+ [:grouping, :close ] # )
286
+ ])
287
+ end
288
+ end
289
+
271
290
  it 'handles whitespace after function name' do
272
291
  tokens = tokenizer.tokenize('not (8 < 5)')
273
292
  expect(tokens.length).to eq(6)
@@ -275,7 +294,7 @@ describe Dentaku::Tokenizer do
275
294
  expect(tokens.map(&:value)).to eq([:not, :open, 8, :lt, 5, :close])
276
295
  end
277
296
 
278
- it 'can end with a bang' do
297
+ it 'handles when function ends with a bang' do
279
298
  tokens = tokenizer.tokenize('exp!(5 * 3)')
280
299
  expect(tokens.length).to eq(6)
281
300
  expect(tokens.map(&:category)).to eq([:function, :grouping, :numeric, :operator, :numeric, :grouping])
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.3.1
4
+ version: 3.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-03-26 00:00:00.000000000 Z
11
+ date: 2020-12-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: codecov
@@ -156,15 +156,20 @@ files:
156
156
  - lib/dentaku/ast/datetime.rb
157
157
  - lib/dentaku/ast/function.rb
158
158
  - lib/dentaku/ast/function_registry.rb
159
+ - lib/dentaku/ast/functions/all.rb
159
160
  - lib/dentaku/ast/functions/and.rb
161
+ - lib/dentaku/ast/functions/any.rb
160
162
  - lib/dentaku/ast/functions/avg.rb
161
163
  - lib/dentaku/ast/functions/count.rb
164
+ - lib/dentaku/ast/functions/duration.rb
162
165
  - lib/dentaku/ast/functions/if.rb
166
+ - lib/dentaku/ast/functions/map.rb
163
167
  - lib/dentaku/ast/functions/max.rb
164
168
  - lib/dentaku/ast/functions/min.rb
165
169
  - lib/dentaku/ast/functions/mul.rb
166
170
  - lib/dentaku/ast/functions/not.rb
167
171
  - lib/dentaku/ast/functions/or.rb
172
+ - lib/dentaku/ast/functions/pluck.rb
168
173
  - lib/dentaku/ast/functions/round.rb
169
174
  - lib/dentaku/ast/functions/rounddown.rb
170
175
  - lib/dentaku/ast/functions/roundup.rb
@@ -184,6 +189,7 @@ files:
184
189
  - lib/dentaku/ast/string.rb
185
190
  - lib/dentaku/bulk_expression_solver.rb
186
191
  - lib/dentaku/calculator.rb
192
+ - lib/dentaku/date_arithmetic.rb
187
193
  - lib/dentaku/dependency_resolver.rb
188
194
  - lib/dentaku/exceptions.rb
189
195
  - lib/dentaku/flat_hash.rb
@@ -208,6 +214,7 @@ files:
208
214
  - spec/ast/max_spec.rb
209
215
  - spec/ast/min_spec.rb
210
216
  - spec/ast/mul_spec.rb
217
+ - spec/ast/negation_spec.rb
211
218
  - spec/ast/node_spec.rb
212
219
  - spec/ast/numeric_spec.rb
213
220
  - spec/ast/or_spec.rb
@@ -248,8 +255,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
248
255
  - !ruby/object:Gem::Version
249
256
  version: '0'
250
257
  requirements: []
251
- rubyforge_project: dentaku
252
- rubygems_version: 2.7.6
258
+ rubygems_version: 3.1.4
253
259
  signing_key:
254
260
  specification_version: 4
255
261
  summary: A formula language parser and evaluator
@@ -267,6 +273,7 @@ test_files:
267
273
  - spec/ast/max_spec.rb
268
274
  - spec/ast/min_spec.rb
269
275
  - spec/ast/mul_spec.rb
276
+ - spec/ast/negation_spec.rb
270
277
  - spec/ast/node_spec.rb
271
278
  - spec/ast/numeric_spec.rb
272
279
  - spec/ast/or_spec.rb