dentaku 3.5.3 → 3.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/rspec.yml +26 -0
  3. data/.github/workflows/rubocop.yml +14 -0
  4. data/CHANGELOG.md +22 -6
  5. data/README.md +1 -4
  6. data/dentaku.gemspec +1 -1
  7. data/lib/dentaku/ast/access.rb +0 -3
  8. data/lib/dentaku/ast/arithmetic.rb +36 -50
  9. data/lib/dentaku/ast/array.rb +1 -4
  10. data/lib/dentaku/ast/case.rb +12 -0
  11. data/lib/dentaku/ast/functions/all.rb +1 -5
  12. data/lib/dentaku/ast/functions/any.rb +1 -5
  13. data/lib/dentaku/ast/functions/enum.rb +13 -0
  14. data/lib/dentaku/ast/functions/map.rb +1 -5
  15. data/lib/dentaku/ast/functions/pluck.rb +6 -2
  16. data/lib/dentaku/ast/node.rb +2 -1
  17. data/lib/dentaku/ast/operation.rb +5 -0
  18. data/lib/dentaku/ast.rb +1 -1
  19. data/lib/dentaku/bulk_expression_solver.rb +37 -7
  20. data/lib/dentaku/calculator.rb +21 -5
  21. data/lib/dentaku/date_arithmetic.rb +24 -15
  22. data/lib/dentaku/dependency_resolver.rb +9 -4
  23. data/lib/dentaku/parser.rb +206 -213
  24. data/lib/dentaku/print_visitor.rb +2 -2
  25. data/lib/dentaku/token.rb +12 -0
  26. data/lib/dentaku/version.rb +1 -1
  27. data/lib/dentaku/visitor/infix.rb +1 -1
  28. data/spec/ast/addition_spec.rb +12 -7
  29. data/spec/ast/all_spec.rb +13 -0
  30. data/spec/ast/any_spec.rb +13 -0
  31. data/spec/ast/arithmetic_spec.rb +7 -0
  32. data/spec/ast/division_spec.rb +10 -6
  33. data/spec/ast/map_spec.rb +13 -0
  34. data/spec/ast/pluck_spec.rb +17 -0
  35. data/spec/bulk_expression_solver_spec.rb +24 -1
  36. data/spec/calculator_spec.rb +21 -3
  37. data/spec/dependency_resolver_spec.rb +18 -0
  38. data/spec/external_function_spec.rb +1 -1
  39. data/spec/parser_spec.rb +11 -2
  40. data/spec/print_visitor_spec.rb +5 -0
  41. data/spec/spec_helper.rb +1 -3
  42. data/spec/visitor_spec.rb +1 -1
  43. metadata +10 -9
@@ -20,16 +20,16 @@ describe Dentaku::AST::Division do
20
20
  expect(node.value.round(4)).to eq(0.8333)
21
21
  end
22
22
 
23
- it 'requires numeric operands' do
23
+ it 'requires operands that respond to /' do
24
24
  expect {
25
- described_class.new(five, t)
26
- }.to raise_error(Dentaku::NodeError, /requires numeric operands/)
25
+ described_class.new(five, t).value
26
+ }.to raise_error(Dentaku::ArgumentError, /requires operands that respond to \//)
27
27
 
28
28
  expression = Dentaku::AST::Multiplication.new(five, five)
29
29
  group = Dentaku::AST::Grouping.new(expression)
30
30
 
31
31
  expect {
32
- described_class.new(group, five)
32
+ described_class.new(group, five).value
33
33
  }.not_to raise_error
34
34
  end
35
35
 
@@ -44,17 +44,21 @@ describe Dentaku::AST::Division do
44
44
  value + other
45
45
  end
46
46
  end
47
+
48
+ def zero?
49
+ value.zero?
50
+ end
47
51
  end
48
52
 
49
53
  operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
50
54
  operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
51
55
 
52
56
  expect {
53
- described_class.new(operand_five, operand_six)
57
+ described_class.new(operand_five, operand_six).value
54
58
  }.not_to raise_error
55
59
 
56
60
  expect {
57
- described_class.new(operand_five, six)
61
+ described_class.new(operand_five, six).value
58
62
  }.not_to raise_error
59
63
  end
60
64
  end
data/spec/ast/map_spec.rb CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Map do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'operates on each value in an array' do
8
9
  result = Dentaku('SUM(MAP(vals, val, val + 1))', vals: [1, 2, 3])
9
10
  expect(result).to eq(9)
@@ -24,4 +25,16 @@ describe Dentaku::AST::Map do
24
25
  Dentaku::ParseError, 'MAP() requires second argument to be an identifier'
25
26
  )
26
27
  end
28
+
29
+ it 'treats missing keys in hashes as NULL in permissive mode' do
30
+ expect(
31
+ calculator.evaluate('MAP(items, item, item.value)', items: [{value: 1}, {}])
32
+ ).to eq([1, nil])
33
+ end
34
+
35
+ it 'raises an error if accessing a missing key in a hash in strict mode' do
36
+ expect {
37
+ calculator.evaluate!('MAP(items, item, item.value)', items: [{value: 1}, {}])
38
+ }.to raise_error(Dentaku::UnboundVariableError)
39
+ end
27
40
  end
@@ -4,6 +4,7 @@ require 'dentaku'
4
4
 
5
5
  describe Dentaku::AST::Pluck do
6
6
  let(:calculator) { Dentaku::Calculator.new }
7
+
7
8
  it 'operates on each value in an array' do
8
9
  result = Dentaku('PLUCK(users, age)', users: [
9
10
  {name: "Bob", age: 44},
@@ -12,6 +13,22 @@ describe Dentaku::AST::Pluck do
12
13
  expect(result).to eq([44, 27])
13
14
  end
14
15
 
16
+ it 'allows specifying a default for missing values' do
17
+ result = Dentaku!('PLUCK(users, age, -1)', users: [
18
+ {name: "Bob"},
19
+ {name: "Jane", age: 27}
20
+ ])
21
+ expect(result).to eq([-1, 27])
22
+ end
23
+
24
+ it 'returns nil if pluck key is missing from a hash' do
25
+ result = Dentaku!('PLUCK(users, age)', users: [
26
+ {name: "Bob"},
27
+ {name: "Jane", age: 27}
28
+ ])
29
+ expect(result).to eq([nil, 27])
30
+ end
31
+
15
32
  it 'works with an empty array' do
16
33
  result = Dentaku('PLUCK(users, age)', users: [])
17
34
  expect(result).to eq([])
@@ -33,13 +33,20 @@ RSpec.describe Dentaku::BulkExpressionSolver do
33
33
  }.to raise_error(Dentaku::UnboundVariableError)
34
34
  end
35
35
 
36
- it "lets you know if the result is a div/0 error" do
36
+ it "lets you know if the result is a div/0 error when dividing" do
37
37
  expressions = {more_apples: "1/0"}
38
38
  expect {
39
39
  described_class.new(expressions, calculator).solve!
40
40
  }.to raise_error(Dentaku::ZeroDivisionError)
41
41
  end
42
42
 
43
+ it "lets you know if the result is a div/0 error when taking modulo" do
44
+ expressions = {more_apples: "1%0"}
45
+ expect {
46
+ described_class.new(expressions, calculator).solve!
47
+ }.to raise_error(Dentaku::ZeroDivisionError)
48
+ end
49
+
43
50
  it "does not require keys to be parseable" do
44
51
  expressions = { "the value of x, incremented" => "x + 1" }
45
52
  solver = described_class.new(expressions, calculator.store("x" => 3))
@@ -107,6 +114,22 @@ RSpec.describe Dentaku::BulkExpressionSolver do
107
114
  end
108
115
 
109
116
  describe "#solve" do
117
+ it 'resolves capitalized keys when they are declared out of order' do
118
+ expressions = {
119
+ FIRST: "SECOND * 2",
120
+ SECOND: "THIRD * 2",
121
+ THIRD: 2,
122
+ }
123
+
124
+ result = described_class.new(expressions, calculator).solve
125
+
126
+ expect(result).to eq(
127
+ FIRST: 8,
128
+ SECOND: 4,
129
+ THIRD: 2
130
+ )
131
+ end
132
+
110
133
  it "returns :undefined when variables are unbound" do
111
134
  expressions = {more_apples: "apples + 1"}
112
135
  expect(described_class.new(expressions, calculator).solve)
@@ -30,7 +30,6 @@ describe Dentaku::Calculator do
30
30
  expect(calculator.evaluate('0 * 10 ^ -5')).to eq(0)
31
31
  expect(calculator.evaluate('3 + 0 * -3')).to eq(3)
32
32
  expect(calculator.evaluate('3 + 0 / -3')).to eq(3)
33
- expect(calculator.evaluate('15 % 8')).to eq(7)
34
33
  expect(calculator.evaluate('(((695759/735000)^(1/(1981-1991)))-1)*1000').round(4)).to eq(5.5018)
35
34
  expect(calculator.evaluate('0.253/0.253')).to eq(1)
36
35
  expect(calculator.evaluate('0.253/d', d: 0.253)).to eq(1)
@@ -40,11 +39,20 @@ describe Dentaku::Calculator do
40
39
  expect(calculator.evaluate('t + 1*24*60*60', t: Time.local(2017, 1, 1))).to eq(Time.local(2017, 1, 2))
41
40
  expect(calculator.evaluate("2 | 3 * 9")).to eq (27)
42
41
  expect(calculator.evaluate("2 & 3 * 9")).to eq (2)
43
- expect(calculator.evaluate("5%")).to eq (0.05)
44
42
  expect(calculator.evaluate('1 << 3')).to eq (8)
45
43
  expect(calculator.evaluate('0xFF >> 6')).to eq (3)
46
44
  end
47
45
 
46
+ it "differentiates between percentage and modulo operators" do
47
+ expect(calculator.evaluate('15 % 8')).to eq(7)
48
+ expect(calculator.evaluate('15 % (4 * 2)')).to eq(7)
49
+ expect(calculator.evaluate("5%")).to eq (0.05)
50
+ expect(calculator.evaluate("400/60%").round(2)).to eq (666.67)
51
+ expect(calculator.evaluate("(400/60%)*1").round(2)).to eq (666.67)
52
+ expect(calculator.evaluate("60% * 1").round(2)).to eq (0.60)
53
+ expect(calculator.evaluate("50% + 50%")).to eq (1.0)
54
+ end
55
+
48
56
  describe 'evaluate' do
49
57
  it 'returns nil when formula has error' do
50
58
  expect(calculator.evaluate('1 + + 1')).to be_nil
@@ -165,7 +173,6 @@ describe Dentaku::Calculator do
165
173
  expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
166
174
  end
167
175
 
168
-
169
176
  it 'stores nested hashes' do
170
177
  calculator.store(a: {basket: {of: 'apples'}}, b: 2)
171
178
  expect(calculator.evaluate!('a.basket.of')).to eq('apples')
@@ -516,6 +523,17 @@ describe Dentaku::Calculator do
516
523
  expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
517
524
  expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
518
525
  end
526
+
527
+ it 'from time object' do
528
+ value = Time.local(2023, 7, 13, 10, 42, 11)
529
+ value2 = Time.local(2023, 12, 1, 9, 42, 10)
530
+
531
+ expect(calculator.evaluate!('value + duration(1, month)', { value: value })).to eq(Time.local(2023, 8, 13, 10, 42, 11))
532
+ expect(calculator.evaluate!('value - duration(1, day)', { value: value })).to eq(Time.local(2023, 7, 12, 10, 42, 11))
533
+ expect(calculator.evaluate!('value - duration(1, year)', { value: value })).to eq(Time.local(2022, 7, 13, 10, 42, 11))
534
+ expect(calculator.evaluate!('value2 - value', { value: value, value2: value2 })).to eq(value2 - value)
535
+ expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
536
+ end
519
537
  end
520
538
 
521
539
  describe 'functions' do
@@ -0,0 +1,18 @@
1
+ require 'spec_helper'
2
+ require 'dentaku/dependency_resolver'
3
+
4
+ describe Dentaku::DependencyResolver do
5
+ it 'sorts expressions in dependency order' do
6
+ dependencies = {"first" => ["second"], "second" => ["third"], "third" => []}
7
+ expect(described_class.find_resolve_order(dependencies)).to eq(
8
+ ["third", "second", "first"]
9
+ )
10
+ end
11
+
12
+ it 'handles case differences' do
13
+ dependencies = {"FIRST" => ["second"], "SeCoNd" => ["third"], "THIRD" => []}
14
+ expect(described_class.find_resolve_order(dependencies)).to eq(
15
+ ["THIRD", "SeCoNd", "FIRST"]
16
+ )
17
+ end
18
+ end
@@ -166,7 +166,7 @@ describe Dentaku::Calculator do
166
166
  it 'adds multiple functions to default/global function registry' do
167
167
  described_class.add_functions([
168
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() }],
169
+ [:spongebob, :string, ->(x) { x.split("").each_with_index().map { |c, i| i.even? ? c.upcase : c.downcase }.join() }],
170
170
  ])
171
171
 
172
172
  expect(described_class.new.evaluate("1 + cube(3)")).to eq(28)
data/spec/parser_spec.rb CHANGED
@@ -27,6 +27,9 @@ describe Dentaku::Parser do
27
27
  it 'calculates bitwise OR' do
28
28
  node = parse('2|3')
29
29
  expect(node.value).to eq(3)
30
+
31
+ node = parse('(5 | 2) + 1')
32
+ expect(node.value).to eq(8)
30
33
  end
31
34
 
32
35
  it 'performs multiple operations in one stream' do
@@ -77,11 +80,17 @@ describe Dentaku::Parser do
77
80
  end
78
81
 
79
82
  it 'evaluates arrays' do
83
+ node = parse('{}')
84
+ expect(node.value).to eq([])
85
+
80
86
  node = parse('{1, 2, 3}')
81
87
  expect(node.value).to eq([1, 2, 3])
82
88
 
83
- node = parse('{}')
84
- expect(node.value).to eq([])
89
+ node = parse('{1, 2, 3} + {4,5,6}')
90
+ expect(node.value).to eq([1, 2, 3, 4, 5, 6])
91
+
92
+ node = parse('{1, 2, 3} - {2,3}')
93
+ expect(node.value).to eq([1])
85
94
  end
86
95
 
87
96
  context 'invalid expression' do
@@ -59,6 +59,11 @@ describe Dentaku::PrintVisitor do
59
59
  expect(repr).to eq('2017-12-24 23:59:59')
60
60
  end
61
61
 
62
+ it 'handles a percentage in a formula' do
63
+ repr = roundtrip('((3*4%) * 0.001)')
64
+ expect(repr).to eq('3 * 4% * 0.001')
65
+ end
66
+
62
67
  private
63
68
 
64
69
  def roundtrip(string)
data/spec/spec_helper.rb CHANGED
@@ -1,14 +1,12 @@
1
1
  require 'pry'
2
2
  require 'simplecov'
3
- require 'codecov'
4
3
 
5
4
  SimpleCov.formatters = SimpleCov::Formatter::MultiFormatter.new([
6
5
  SimpleCov::Formatter::HTMLFormatter,
7
- SimpleCov::Formatter::Codecov,
8
6
  ])
9
7
 
10
8
  SimpleCov.minimum_coverage 90
11
- SimpleCov.minimum_coverage_by_file 80
9
+ # SimpleCov.minimum_coverage_by_file 80
12
10
 
13
11
  SimpleCov.start do
14
12
  add_filter "spec/"
data/spec/visitor_spec.rb CHANGED
@@ -108,7 +108,7 @@ describe TestVisitor do
108
108
  it 'visits all concrete AST node types' do
109
109
  @visited = Set.new
110
110
 
111
- visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples')
111
+ visit_nodes('(1 + 7) * (8 ^ 2) / - 3.0 - apples * 5%')
112
112
  visit_nodes('1 < 2 and 3 <= 4 or 5 > 6 AND 7 >= 8 OR 9 != 10 and true')
113
113
  visit_nodes('IF(a[0] = NULL, "five", \'seven\')')
114
114
  visit_nodes('case (a % 5) when 0 then a else b end')
metadata CHANGED
@@ -1,17 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dentaku
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.5.3
4
+ version: 3.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Solomon White
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2024-07-04 00:00:00.000000000 Z
10
+ date: 2025-08-21 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
13
+ name: bigdecimal
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
16
  - - ">="
@@ -25,13 +24,13 @@ dependencies:
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: codecov
27
+ name: concurrent-ruby
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
30
  - - ">="
32
31
  - !ruby/object:Gem::Version
33
32
  version: '0'
34
- type: :development
33
+ type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
@@ -143,6 +142,8 @@ executables: []
143
142
  extensions: []
144
143
  extra_rdoc_files: []
145
144
  files:
145
+ - ".github/workflows/rspec.yml"
146
+ - ".github/workflows/rubocop.yml"
146
147
  - ".gitignore"
147
148
  - ".pryrc"
148
149
  - ".rubocop.yml"
@@ -259,6 +260,7 @@ files:
259
260
  - spec/bulk_expression_solver_spec.rb
260
261
  - spec/calculator_spec.rb
261
262
  - spec/dentaku_spec.rb
263
+ - spec/dependency_resolver_spec.rb
262
264
  - spec/exceptions_spec.rb
263
265
  - spec/external_function_spec.rb
264
266
  - spec/parser_spec.rb
@@ -274,7 +276,6 @@ homepage: http://github.com/rubysolo/dentaku
274
276
  licenses:
275
277
  - MIT
276
278
  metadata: {}
277
- post_install_message:
278
279
  rdoc_options: []
279
280
  require_paths:
280
281
  - lib
@@ -289,8 +290,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
289
290
  - !ruby/object:Gem::Version
290
291
  version: '0'
291
292
  requirements: []
292
- rubygems_version: 3.3.9
293
- signing_key:
293
+ rubygems_version: 3.6.2
294
294
  specification_version: 4
295
295
  summary: A formula language parser and evaluator
296
296
  test_files:
@@ -330,6 +330,7 @@ test_files:
330
330
  - spec/bulk_expression_solver_spec.rb
331
331
  - spec/calculator_spec.rb
332
332
  - spec/dentaku_spec.rb
333
+ - spec/dependency_resolver_spec.rb
333
334
  - spec/exceptions_spec.rb
334
335
  - spec/external_function_spec.rb
335
336
  - spec/parser_spec.rb