dentaku 3.5.2 → 3.5.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/README.md +1 -1
- data/dentaku.gemspec +1 -0
- data/lib/dentaku/ast/access.rb +0 -3
- data/lib/dentaku/ast/arithmetic.rb +25 -35
- data/lib/dentaku/ast/array.rb +1 -4
- 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/if.rb +3 -7
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +1 -5
- data/lib/dentaku/ast/functions/pluck.rb +6 -2
- data/lib/dentaku/ast/functions/reduce.rb +61 -0
- data/lib/dentaku/ast/node.rb +2 -1
- data/lib/dentaku/ast.rb +3 -1
- data/lib/dentaku/bulk_expression_solver.rb +37 -7
- data/lib/dentaku/calculator.rb +23 -7
- data/lib/dentaku/date_arithmetic.rb +26 -15
- data/lib/dentaku/dependency_resolver.rb +9 -4
- data/lib/dentaku/exceptions.rb +8 -3
- data/lib/dentaku/parser.rb +8 -5
- data/lib/dentaku/token.rb +12 -0
- data/lib/dentaku/token_scanner.rb +7 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/addition_spec.rb +4 -4
- data/spec/ast/all_spec.rb +13 -0
- data/spec/ast/any_spec.rb +13 -0
- data/spec/ast/arithmetic_spec.rb +43 -2
- data/spec/ast/division_spec.rb +25 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +13 -0
- data/spec/ast/pluck_spec.rb +17 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/bulk_expression_solver_spec.rb +33 -0
- data/spec/calculator_spec.rb +66 -7
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/parser_spec.rb +3 -0
- data/spec/tokenizer_spec.rb +6 -4
- data/spec/visitor_spec.rb +2 -1
- metadata +25 -3
data/lib/dentaku/exceptions.rb
CHANGED
@@ -67,7 +67,9 @@ module Dentaku
|
|
67
67
|
private_class_method :new
|
68
68
|
|
69
69
|
VALID_REASONS = %i[
|
70
|
-
parse_error
|
70
|
+
parse_error
|
71
|
+
too_many_closing_parentheses
|
72
|
+
too_many_opening_parentheses
|
71
73
|
unexpected_zero_width_match
|
72
74
|
].freeze
|
73
75
|
|
@@ -92,8 +94,11 @@ module Dentaku
|
|
92
94
|
private_class_method :new
|
93
95
|
|
94
96
|
VALID_REASONS = %i[
|
95
|
-
|
96
|
-
|
97
|
+
incompatible_type
|
98
|
+
invalid_operator
|
99
|
+
invalid_value
|
100
|
+
too_few_arguments
|
101
|
+
wrong_number_of_arguments
|
97
102
|
].freeze
|
98
103
|
|
99
104
|
def self.for(reason, **meta)
|
data/lib/dentaku/parser.rb
CHANGED
@@ -43,8 +43,6 @@ module Dentaku
|
|
43
43
|
operator = operations.pop
|
44
44
|
fail! :invalid_statement if operator.nil?
|
45
45
|
|
46
|
-
operator.peek(output)
|
47
|
-
|
48
46
|
output_size = output.length
|
49
47
|
args_size = operator.arity || count
|
50
48
|
min_size = operator.arity || operator.min_param_count || count
|
@@ -60,10 +58,14 @@ module Dentaku
|
|
60
58
|
fail! :too_many_operands, operator: operator, expect: expect, actual: output_size
|
61
59
|
end
|
62
60
|
|
63
|
-
|
64
|
-
|
61
|
+
if operator == AST::Array && output.empty?
|
62
|
+
output.push(operator.new())
|
63
|
+
else
|
64
|
+
fail! :invalid_statement if output_size < args_size
|
65
|
+
args = Array.new(args_size) { output.pop }.reverse
|
65
66
|
|
66
|
-
|
67
|
+
output.push operator.new(*args)
|
68
|
+
end
|
67
69
|
|
68
70
|
if operator.respond_to?(:callback) && !operator.callback.nil?
|
69
71
|
operator.callback.call(args)
|
@@ -96,6 +98,7 @@ module Dentaku
|
|
96
98
|
|
97
99
|
when :operator, :comparator, :combinator
|
98
100
|
op_class = operation(token)
|
101
|
+
op_class = op_class.resolve_class(input.first)
|
99
102
|
|
100
103
|
if op_class.right_associative?
|
101
104
|
while operations.last && operations.last < AST::Operation && op_class.precedence < operations.last.precedence
|
data/lib/dentaku/token.rb
CHANGED
@@ -20,10 +20,22 @@ module Dentaku
|
|
20
20
|
length.zero?
|
21
21
|
end
|
22
22
|
|
23
|
+
def operator?
|
24
|
+
is?(:operator)
|
25
|
+
end
|
26
|
+
|
23
27
|
def grouping?
|
24
28
|
is?(:grouping)
|
25
29
|
end
|
26
30
|
|
31
|
+
def open?
|
32
|
+
grouping? && value == :open
|
33
|
+
end
|
34
|
+
|
35
|
+
def close?
|
36
|
+
grouping? && value == :close
|
37
|
+
end
|
38
|
+
|
27
39
|
def is?(c)
|
28
40
|
category == c
|
29
41
|
end
|
@@ -7,7 +7,7 @@ module Dentaku
|
|
7
7
|
class TokenScanner
|
8
8
|
extend StringCasing
|
9
9
|
|
10
|
-
DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}( \d{1,2}:\d{1,2}:\d{1,2})? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
|
10
|
+
DATE_TIME_REGEXP = /\d{2}\d{2}?-\d{1,2}-\d{1,2}([ |T]\d{1,2}:\d{1,2}:\d{1,2}(\.\d*)?)? ?(Z|((\+|\-)\d{2}\:?\d{2}))?(?!\d)/.freeze
|
11
11
|
|
12
12
|
def initialize(category, regexp, converter = nil, condition = nil)
|
13
13
|
@category = category
|
@@ -51,7 +51,8 @@ module Dentaku
|
|
51
51
|
:comparator,
|
52
52
|
:boolean,
|
53
53
|
:function,
|
54
|
-
:identifier
|
54
|
+
:identifier,
|
55
|
+
:quoted_identifier
|
55
56
|
]
|
56
57
|
end
|
57
58
|
|
@@ -180,6 +181,10 @@ module Dentaku
|
|
180
181
|
def identifier
|
181
182
|
new(:identifier, '[[[:word:]]\.]+\b', lambda { |raw| standardize_case(raw.strip) })
|
182
183
|
end
|
184
|
+
|
185
|
+
def quoted_identifier
|
186
|
+
new(:identifier, '`[^`]*`', lambda { |raw| raw.gsub(/^`|`$/, '') })
|
187
|
+
end
|
183
188
|
end
|
184
189
|
|
185
190
|
register_default_scanners
|
data/lib/dentaku/version.rb
CHANGED
data/spec/ast/addition_spec.rb
CHANGED
@@ -36,10 +36,10 @@ describe Dentaku::AST::Addition do
|
|
36
36
|
it 'allows operands that respond to addition' do
|
37
37
|
# Sample struct that has a custom definition for addition
|
38
38
|
|
39
|
-
|
39
|
+
Addable = Struct.new(:value) do
|
40
40
|
def +(other)
|
41
41
|
case other
|
42
|
-
when
|
42
|
+
when Addable
|
43
43
|
value + other.value
|
44
44
|
when Numeric
|
45
45
|
value + other
|
@@ -47,8 +47,8 @@ describe Dentaku::AST::Addition do
|
|
47
47
|
end
|
48
48
|
end
|
49
49
|
|
50
|
-
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric,
|
51
|
-
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric,
|
50
|
+
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(5))
|
51
|
+
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Addable.new(6))
|
52
52
|
|
53
53
|
expect {
|
54
54
|
described_class.new(operand_five, operand_six)
|
data/spec/ast/all_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::All do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ALL operation' do
|
8
9
|
result = Dentaku('ALL(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(false)
|
@@ -22,4 +23,16 @@ describe Dentaku::AST::All do
|
|
22
23
|
Dentaku::ParseError, 'ALL() requires second argument to be an identifier'
|
23
24
|
)
|
24
25
|
end
|
26
|
+
|
27
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
28
|
+
expect(
|
29
|
+
calculator.evaluate('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
30
|
+
).to be_falsy
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
34
|
+
expect {
|
35
|
+
calculator.evaluate!('ALL(items, item, item.value)', items: [{value: 1}, {}])
|
36
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
37
|
+
end
|
25
38
|
end
|
data/spec/ast/any_spec.rb
CHANGED
@@ -4,6 +4,7 @@ require 'dentaku'
|
|
4
4
|
|
5
5
|
describe Dentaku::AST::Any do
|
6
6
|
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
7
8
|
it 'performs ANY operation' do
|
8
9
|
result = Dentaku('ANY(vals, val, val > 1)', vals: [1, 2, 3])
|
9
10
|
expect(result).to eq(true)
|
@@ -20,4 +21,16 @@ describe Dentaku::AST::Any do
|
|
20
21
|
it 'raises argument error if a string is passed as identifier' do
|
21
22
|
expect { calculator.evaluate!('ANY({1, 2, 3}, "val", val % 2 == 0)') }.to raise_error(Dentaku::ParseError)
|
22
23
|
end
|
24
|
+
|
25
|
+
it 'treats missing keys in hashes as NULL in permissive mode' do
|
26
|
+
expect(
|
27
|
+
calculator.evaluate('ANY(items, item, item.value)', items: [{value: 1}, {}])
|
28
|
+
).to be_truthy
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'raises an error if accessing a missing key in a hash in strict mode' do
|
32
|
+
expect {
|
33
|
+
calculator.evaluate!('ANY(items, item, item.value)', items: [{}, {value: 1}])
|
34
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
35
|
+
end
|
23
36
|
end
|
data/spec/ast/arithmetic_spec.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'dentaku/ast/arithmetic'
|
3
|
-
|
4
|
-
require 'dentaku/token'
|
3
|
+
require 'dentaku'
|
5
4
|
|
6
5
|
describe Dentaku::AST::Arithmetic do
|
7
6
|
let(:one) { Dentaku::AST::Numeric.new(Dentaku::Token.new(:numeric, 1)) }
|
@@ -64,10 +63,52 @@ describe Dentaku::AST::Arithmetic do
|
|
64
63
|
|
65
64
|
it 'performs arithmetic on arrays' do
|
66
65
|
expect(add(x, y, 'x' => [1], 'y' => [2])).to eq([1, 2])
|
66
|
+
expect(sub(x, y, 'x' => [1], 'y' => [2])).to eq([1])
|
67
67
|
end
|
68
68
|
|
69
69
|
it 'performs date arithmetic' do
|
70
70
|
expect(add(date, one)).to eq(DateTime.new(2020, 4, 17))
|
71
|
+
expect(sub(date, one)).to eq(DateTime.new(2020, 4, 15))
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'performs arithmetic on object which implements arithmetic' do
|
75
|
+
CanHazMath = Struct.new(:value) do
|
76
|
+
extend Forwardable
|
77
|
+
|
78
|
+
def_delegators :value, :zero?
|
79
|
+
|
80
|
+
def coerce(other)
|
81
|
+
case other
|
82
|
+
when Numeric
|
83
|
+
[other, value]
|
84
|
+
else
|
85
|
+
super
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
[:+, :-, :/, :*].each do |operand|
|
90
|
+
define_method(operand) do |other|
|
91
|
+
case other
|
92
|
+
when CanHazMath
|
93
|
+
value.public_send(operand, other.value)
|
94
|
+
when Numeric
|
95
|
+
value.public_send(operand, other)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
op_one = CanHazMath.new(1)
|
102
|
+
op_two = CanHazMath.new(2)
|
103
|
+
|
104
|
+
[op_two, two].each do |left|
|
105
|
+
[op_one, one].each do |right|
|
106
|
+
expect(add(x, y, 'x' => left, 'y' => right)).to eq(3)
|
107
|
+
expect(sub(x, y, 'x' => left, 'y' => right)).to eq(1)
|
108
|
+
expect(mul(x, y, 'x' => left, 'y' => right)).to eq(2)
|
109
|
+
expect(div(x, y, 'x' => left, 'y' => right)).to eq(2)
|
110
|
+
end
|
111
|
+
end
|
71
112
|
end
|
72
113
|
|
73
114
|
it 'raises ArgumentError if given individually valid but incompatible arguments' do
|
data/spec/ast/division_spec.rb
CHANGED
@@ -32,4 +32,29 @@ describe Dentaku::AST::Division do
|
|
32
32
|
described_class.new(group, five)
|
33
33
|
}.not_to raise_error
|
34
34
|
end
|
35
|
+
|
36
|
+
it 'allows operands that respond to division' do
|
37
|
+
# Sample struct that has a custom definition for division
|
38
|
+
Divisible = Struct.new(:value) do
|
39
|
+
def /(other)
|
40
|
+
case other
|
41
|
+
when Divisible
|
42
|
+
value + other.value
|
43
|
+
when Numeric
|
44
|
+
value + other
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
operand_five = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(5))
|
50
|
+
operand_six = Dentaku::AST::Numeric.new Dentaku::Token.new(:numeric, Divisible.new(6))
|
51
|
+
|
52
|
+
expect {
|
53
|
+
described_class.new(operand_five, operand_six)
|
54
|
+
}.not_to raise_error
|
55
|
+
|
56
|
+
expect {
|
57
|
+
described_class.new(operand_five, six)
|
58
|
+
}.not_to raise_error
|
59
|
+
end
|
35
60
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/intercept'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe 'Dentaku::AST::Function::Intercept' do
|
6
|
+
it 'returns the correct intercept for given x and y arrays' do
|
7
|
+
x_values = [6, 13, 15, 10, 11, 10]
|
8
|
+
y_values = [-1, 8, 8, 13, 3, 15]
|
9
|
+
result = Dentaku('INTERCEPT(ys, xs)', xs: x_values, ys: y_values)
|
10
|
+
expect(result).to be_within(0.001).of(9.437)
|
11
|
+
end
|
12
|
+
|
13
|
+
context 'checking errors' do
|
14
|
+
it 'raises an error if arguments are not arrays' do
|
15
|
+
expect { Dentaku!("INTERCEPT(1, 2)") }.to raise_error(Dentaku::ArgumentError)
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises an error if the arrays are not of equal length' do
|
19
|
+
x_values = [1, 2, 3]
|
20
|
+
y_values = [2, 3, 5, 4]
|
21
|
+
expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'raises an error if any of the arrays is empty' do
|
25
|
+
x_values = []
|
26
|
+
y_values = [2, 3, 5, 4]
|
27
|
+
expect { Dentaku!("INTERCEPT(y, x)", x: x_values, y: y_values) }.to raise_error(Dentaku::ArgumentError)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
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([])
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/functions/reduce'
|
3
|
+
require 'dentaku'
|
4
|
+
|
5
|
+
describe Dentaku::AST::Reduce do
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
7
|
+
|
8
|
+
it 'performs REDUCE operation with initial value' do
|
9
|
+
result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val), "hello")', vals: ["wo", "rl", "d"])
|
10
|
+
expect(result).to eq("helloworld")
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'performs REDUCE operation without initial value' do
|
14
|
+
result = Dentaku('REDUCE(vals, memo, val, CONCAT(memo, val))', vals: ["wo", "rl", "d"])
|
15
|
+
expect(result).to eq("world")
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'raises argument error if a string is passed as identifier' do
|
19
|
+
expect { calculator.evaluate!('REDUCE({1, 2, 3}, memo, "val", memo + val)') }.to raise_error(Dentaku::ParseError)
|
20
|
+
expect { calculator.evaluate!('REDUCE({1, 2, 3}, "memo", val, memo + val)') }.to raise_error(Dentaku::ParseError)
|
21
|
+
end
|
22
|
+
end
|
@@ -107,6 +107,22 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
107
107
|
end
|
108
108
|
|
109
109
|
describe "#solve" do
|
110
|
+
it 'resolves capitalized keys when they are declared out of order' do
|
111
|
+
expressions = {
|
112
|
+
FIRST: "SECOND * 2",
|
113
|
+
SECOND: "THIRD * 2",
|
114
|
+
THIRD: 2,
|
115
|
+
}
|
116
|
+
|
117
|
+
result = described_class.new(expressions, calculator).solve
|
118
|
+
|
119
|
+
expect(result).to eq(
|
120
|
+
FIRST: 8,
|
121
|
+
SECOND: 4,
|
122
|
+
THIRD: 2
|
123
|
+
)
|
124
|
+
end
|
125
|
+
|
110
126
|
it "returns :undefined when variables are unbound" do
|
111
127
|
expressions = {more_apples: "apples + 1"}
|
112
128
|
expect(described_class.new(expressions, calculator).solve)
|
@@ -125,6 +141,12 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
125
141
|
.to eq(more_apples: :foo)
|
126
142
|
end
|
127
143
|
|
144
|
+
it "allows passing in ast as expression" do
|
145
|
+
expressions = {more_apples: calculator.ast("1/0")}
|
146
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
147
|
+
.to eq(more_apples: :foo)
|
148
|
+
end
|
149
|
+
|
128
150
|
it 'stores the recipient variable on the exception when there is a div/0 error' do
|
129
151
|
expressions = {more_apples: "1/0"}
|
130
152
|
exception = nil
|
@@ -197,5 +219,16 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
197
219
|
expect(results).to eq('key' => [3, :undefined])
|
198
220
|
expect { solver.solve! }.to raise_error(Dentaku::UnboundVariableError)
|
199
221
|
end
|
222
|
+
|
223
|
+
it do
|
224
|
+
calculator.store(val: nil)
|
225
|
+
expressions = {
|
226
|
+
a: 'IF(5 / 0 > 0, 100, 1000)',
|
227
|
+
b: 'IF(val = 0, 0, IF(val > 0, 0, 0))'
|
228
|
+
}
|
229
|
+
solver = described_class.new(expressions, calculator)
|
230
|
+
results = solver.solve
|
231
|
+
expect(results).to eq(a: :undefined, b: :undefined)
|
232
|
+
end
|
200
233
|
end
|
201
234
|
end
|
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
|
@@ -159,6 +167,12 @@ describe Dentaku::Calculator do
|
|
159
167
|
expect(calculator.evaluate!('area', length: 5, width: 5)).to eq(25)
|
160
168
|
end
|
161
169
|
|
170
|
+
it 'stores dates' do
|
171
|
+
calculator.store("d1", Date.parse("2024/01/02"))
|
172
|
+
calculator.store("d2", Date.parse("2024/01/06"))
|
173
|
+
expect(calculator.solve(diff: "d1 - d2")).to eq(diff: -4)
|
174
|
+
end
|
175
|
+
|
162
176
|
it 'stores nested hashes' do
|
163
177
|
calculator.store(a: {basket: {of: 'apples'}}, b: 2)
|
164
178
|
expect(calculator.evaluate!('a.basket.of')).to eq('apples')
|
@@ -166,6 +180,13 @@ describe Dentaku::Calculator do
|
|
166
180
|
expect(calculator.evaluate!('b')).to eq(2)
|
167
181
|
end
|
168
182
|
|
183
|
+
it 'stores nested hashes with quotes' do
|
184
|
+
calculator.store(a: {basket: {of: 'apples'}}, b: 2)
|
185
|
+
expect(calculator.evaluate!('`a.basket.of`')).to eq('apples')
|
186
|
+
expect(calculator.evaluate!('`a.basket`')).to eq(of: 'apples')
|
187
|
+
expect(calculator.evaluate!('`b`')).to eq(2)
|
188
|
+
end
|
189
|
+
|
169
190
|
it 'stores arrays' do
|
170
191
|
calculator.store(a: [1, 2, 3])
|
171
192
|
expect(calculator.evaluate!('a[0]')).to eq(1)
|
@@ -180,6 +201,10 @@ describe Dentaku::Calculator do
|
|
180
201
|
end
|
181
202
|
|
182
203
|
describe 'dependencies' do
|
204
|
+
it 'respects quoted identifiers in dependencies' do
|
205
|
+
expect(calculator.dependencies("`bob the builder` + `dole the digger` / 3")).to eq(['bob the builder', 'dole the digger'])
|
206
|
+
end
|
207
|
+
|
183
208
|
it "finds dependencies in a generic statement" do
|
184
209
|
expect(calculator.dependencies("bob + dole / 3")).to eq(['bob', 'dole'])
|
185
210
|
end
|
@@ -188,6 +213,10 @@ describe Dentaku::Calculator do
|
|
188
213
|
expect(calculator.dependencies("a + b", a: 1)).to eq(['b'])
|
189
214
|
end
|
190
215
|
|
216
|
+
it "ignores dependencies passed in context for quoted identifiers" do
|
217
|
+
expect(calculator.dependencies("`a-c` + b", "a-c": 1)).to eq(['b'])
|
218
|
+
end
|
219
|
+
|
191
220
|
it "finds dependencies in formula arguments" do
|
192
221
|
allow(Dentaku).to receive(:cache_ast?) { true }
|
193
222
|
|
@@ -211,10 +240,11 @@ describe Dentaku::Calculator do
|
|
211
240
|
describe 'solve!' do
|
212
241
|
it "evaluates properly with variables, even if some in memory" do
|
213
242
|
expect(with_memory.solve!(
|
243
|
+
"monthly fruit budget": "weekly_fruit_budget * 4",
|
214
244
|
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
215
245
|
weekly_apple_budget: "apples * 7",
|
216
246
|
pear: "1"
|
217
|
-
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
247
|
+
)).to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25, "monthly fruit budget": 100)
|
218
248
|
end
|
219
249
|
|
220
250
|
it "prefers variables over values in memory if they have no dependencies" do
|
@@ -412,6 +442,13 @@ describe Dentaku::Calculator do
|
|
412
442
|
expect(calculator.evaluate('fo1o * 2', fo1o: 4)).to eq(8)
|
413
443
|
end
|
414
444
|
|
445
|
+
it 'accepts special characters in quoted identifiers' do
|
446
|
+
expect(calculator.evaluate('`foo1 bar` * 2', "foo1 bar": 2)).to eq(4)
|
447
|
+
expect(calculator.evaluate('`foo1-bar` * 2', 'foo1-bar' => 4)).to eq(8)
|
448
|
+
expect(calculator.evaluate('`1foo (bar)` * 2', '1foo (bar)' => 2)).to eq(4)
|
449
|
+
expect(calculator.evaluate('`fo1o *bar*` * 2', 'fo1o *bar*': 4)).to eq(8)
|
450
|
+
end
|
451
|
+
|
415
452
|
it 'compares string literals with string variables' do
|
416
453
|
expect(calculator.evaluate('fruit = "apple"', fruit: 'apple')).to be_truthy
|
417
454
|
expect(calculator.evaluate('fruit = "apple"', fruit: 'pear')).to be_falsey
|
@@ -471,16 +508,31 @@ describe Dentaku::Calculator do
|
|
471
508
|
|
472
509
|
it 'from string variable' do
|
473
510
|
value = '2023-01-01'
|
511
|
+
value2 = '2022-12-31'
|
474
512
|
|
475
|
-
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to
|
476
|
-
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to
|
513
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
|
514
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
|
515
|
+
expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
|
477
516
|
end
|
478
517
|
|
479
518
|
it 'from date object' do
|
480
519
|
value = Date.parse('2023-01-01').to_date
|
520
|
+
value2 = Date.parse('2022-12-31').to_date
|
481
521
|
|
482
|
-
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to
|
483
|
-
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to
|
522
|
+
expect(calculator.evaluate!('value + duration(1, month)', { value: value }).to_date).to eq(Date.parse('2023-02-01'))
|
523
|
+
expect(calculator.evaluate!('value - duration(1, month)', { value: value }).to_date).to eq(Date.parse('2022-12-01'))
|
524
|
+
expect(calculator.evaluate!('value - value2', { value: value, value2: value2 })).to eq(1)
|
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(12_182_399.0)
|
535
|
+
expect(calculator.evaluate!('value - 7200', { value: value })).to eq(Time.local(2023, 7, 13, 8, 42, 11))
|
484
536
|
end
|
485
537
|
end
|
486
538
|
|
@@ -522,6 +574,13 @@ describe Dentaku::Calculator do
|
|
522
574
|
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
|
523
575
|
end
|
524
576
|
|
577
|
+
it 'calculates intercept correctly' do
|
578
|
+
x_values = [1, 2, 3, 4, 5]
|
579
|
+
y_values = [2, 3, 5, 4, 6]
|
580
|
+
result = calculator.evaluate('INTERCEPT(x_values, y_values)', x_values: x_values, y_values: y_values)
|
581
|
+
expect(result).to be_within(0.001).of(1.3)
|
582
|
+
end
|
583
|
+
|
525
584
|
describe "any" do
|
526
585
|
it "enumerates values and returns true if any evaluation is truthy" do
|
527
586
|
expect(calculator.evaluate!('any(xs, x, x > 3)', xs: [1, 2, 3, 4])).to be_truthy
|
@@ -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
|
data/spec/parser_spec.rb
CHANGED
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
|
|