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.
- checksums.yaml +4 -4
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/CHANGELOG.md +22 -6
- data/README.md +1 -4
- data/dentaku.gemspec +1 -1
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +36 -50
- data/lib/dentaku/ast/array.rb +1 -4
- data/lib/dentaku/ast/case.rb +12 -0
- data/lib/dentaku/ast/functions/all.rb +1 -5
- data/lib/dentaku/ast/functions/any.rb +1 -5
- data/lib/dentaku/ast/functions/enum.rb +13 -0
- data/lib/dentaku/ast/functions/map.rb +1 -5
- data/lib/dentaku/ast/functions/pluck.rb +6 -2
- data/lib/dentaku/ast/node.rb +2 -1
- data/lib/dentaku/ast/operation.rb +5 -0
- data/lib/dentaku/ast.rb +1 -1
- data/lib/dentaku/bulk_expression_solver.rb +37 -7
- data/lib/dentaku/calculator.rb +21 -5
- data/lib/dentaku/date_arithmetic.rb +24 -15
- data/lib/dentaku/dependency_resolver.rb +9 -4
- data/lib/dentaku/parser.rb +206 -213
- data/lib/dentaku/print_visitor.rb +2 -2
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/version.rb +1 -1
- data/lib/dentaku/visitor/infix.rb +1 -1
- data/spec/ast/addition_spec.rb +12 -7
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/arithmetic_spec.rb +7 -0
- data/spec/ast/division_spec.rb +10 -6
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/bulk_expression_solver_spec.rb +24 -1
- data/spec/calculator_spec.rb +21 -3
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/external_function_spec.rb +1 -1
- data/spec/parser_spec.rb +11 -2
- data/spec/print_visitor_spec.rb +5 -0
- data/spec/spec_helper.rb +1 -3
- data/spec/visitor_spec.rb +1 -1
- metadata +10 -9
data/spec/ast/division_spec.rb
CHANGED
@@ -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
|
23
|
+
it 'requires operands that respond to /' do
|
24
24
|
expect {
|
25
|
-
described_class.new(five, t)
|
26
|
-
}.to raise_error(Dentaku::
|
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
|
data/spec/ast/pluck_spec.rb
CHANGED
@@ -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)
|
data/spec/calculator_spec.rb
CHANGED
@@ -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
|
data/spec/print_visitor_spec.rb
CHANGED
@@ -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.
|
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:
|
10
|
+
date: 2025-08-21 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
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:
|
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: :
|
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.
|
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
|