hayadentaku 3.5.7
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 +7 -0
- data/.github/workflows/rspec.yml +26 -0
- data/.github/workflows/rubocop.yml +14 -0
- data/.gitignore +14 -0
- data/.pryrc +2 -0
- data/.rubocop.yml +114 -0
- data/.travis.yml +10 -0
- data/CHANGELOG.md +328 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +352 -0
- data/Rakefile +31 -0
- data/hayadentaku.gemspec +35 -0
- data/lib/dentaku/ast/access.rb +44 -0
- data/lib/dentaku/ast/arithmetic.rb +292 -0
- data/lib/dentaku/ast/array.rb +38 -0
- data/lib/dentaku/ast/bitwise.rb +42 -0
- data/lib/dentaku/ast/case/case_conditional.rb +38 -0
- data/lib/dentaku/ast/case/case_else.rb +35 -0
- data/lib/dentaku/ast/case/case_switch_variable.rb +35 -0
- data/lib/dentaku/ast/case/case_then.rb +35 -0
- data/lib/dentaku/ast/case/case_when.rb +39 -0
- data/lib/dentaku/ast/case.rb +93 -0
- data/lib/dentaku/ast/combinators.rb +50 -0
- data/lib/dentaku/ast/comparators.rb +88 -0
- data/lib/dentaku/ast/datetime.rb +8 -0
- data/lib/dentaku/ast/function.rb +56 -0
- data/lib/dentaku/ast/function_registry.rb +107 -0
- data/lib/dentaku/ast/functions/abs.rb +5 -0
- data/lib/dentaku/ast/functions/all.rb +19 -0
- data/lib/dentaku/ast/functions/and.rb +25 -0
- data/lib/dentaku/ast/functions/any.rb +19 -0
- data/lib/dentaku/ast/functions/avg.rb +13 -0
- data/lib/dentaku/ast/functions/count.rb +26 -0
- data/lib/dentaku/ast/functions/duration.rb +51 -0
- data/lib/dentaku/ast/functions/enum.rb +54 -0
- data/lib/dentaku/ast/functions/filter.rb +21 -0
- data/lib/dentaku/ast/functions/if.rb +47 -0
- data/lib/dentaku/ast/functions/intercept.rb +33 -0
- data/lib/dentaku/ast/functions/map.rb +19 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/mul.rb +12 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/or.rb +25 -0
- data/lib/dentaku/ast/functions/pluck.rb +34 -0
- data/lib/dentaku/ast/functions/reduce.rb +60 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +8 -0
- data/lib/dentaku/ast/functions/roundup.rb +8 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +57 -0
- data/lib/dentaku/ast/functions/string_functions.rb +212 -0
- data/lib/dentaku/ast/functions/sum.rb +12 -0
- data/lib/dentaku/ast/functions/switch.rb +8 -0
- data/lib/dentaku/ast/functions/xor.rb +44 -0
- data/lib/dentaku/ast/grouping.rb +23 -0
- data/lib/dentaku/ast/identifier.rb +52 -0
- data/lib/dentaku/ast/literal.rb +30 -0
- data/lib/dentaku/ast/logical.rb +8 -0
- data/lib/dentaku/ast/negation.rb +54 -0
- data/lib/dentaku/ast/nil.rb +13 -0
- data/lib/dentaku/ast/node.rb +29 -0
- data/lib/dentaku/ast/numeric.rb +8 -0
- data/lib/dentaku/ast/operation.rb +44 -0
- data/lib/dentaku/ast/string.rb +15 -0
- data/lib/dentaku/ast.rb +42 -0
- data/lib/dentaku/bulk_expression_solver.rb +158 -0
- data/lib/dentaku/calculator.rb +192 -0
- data/lib/dentaku/date_arithmetic.rb +60 -0
- data/lib/dentaku/dependency_resolver.rb +29 -0
- data/lib/dentaku/exceptions.rb +116 -0
- data/lib/dentaku/flat_hash.rb +161 -0
- data/lib/dentaku/parser.rb +318 -0
- data/lib/dentaku/print_visitor.rb +112 -0
- data/lib/dentaku/string_casing.rb +7 -0
- data/lib/dentaku/token.rb +48 -0
- data/lib/dentaku/token_matcher.rb +138 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +240 -0
- data/lib/dentaku/tokenizer.rb +127 -0
- data/lib/dentaku/version.rb +3 -0
- data/lib/dentaku/visitor/infix.rb +86 -0
- data/lib/dentaku.rb +69 -0
- data/spec/ast/abs_spec.rb +26 -0
- data/spec/ast/addition_spec.rb +67 -0
- data/spec/ast/all_spec.rb +38 -0
- data/spec/ast/and_function_spec.rb +35 -0
- data/spec/ast/and_spec.rb +32 -0
- data/spec/ast/any_spec.rb +36 -0
- data/spec/ast/arithmetic_spec.rb +147 -0
- data/spec/ast/avg_spec.rb +42 -0
- data/spec/ast/case_spec.rb +84 -0
- data/spec/ast/comparator_spec.rb +87 -0
- data/spec/ast/count_spec.rb +40 -0
- data/spec/ast/division_spec.rb +64 -0
- data/spec/ast/filter_spec.rb +25 -0
- data/spec/ast/function_spec.rb +69 -0
- data/spec/ast/intercept_spec.rb +30 -0
- data/spec/ast/map_spec.rb +40 -0
- data/spec/ast/max_spec.rb +33 -0
- data/spec/ast/min_spec.rb +33 -0
- data/spec/ast/mul_spec.rb +43 -0
- data/spec/ast/negation_spec.rb +48 -0
- data/spec/ast/node_spec.rb +43 -0
- data/spec/ast/numeric_spec.rb +16 -0
- data/spec/ast/or_spec.rb +35 -0
- data/spec/ast/pluck_spec.rb +49 -0
- data/spec/ast/reduce_spec.rb +22 -0
- data/spec/ast/round_spec.rb +35 -0
- data/spec/ast/rounddown_spec.rb +35 -0
- data/spec/ast/roundup_spec.rb +35 -0
- data/spec/ast/string_functions_spec.rb +217 -0
- data/spec/ast/sum_spec.rb +43 -0
- data/spec/ast/switch_spec.rb +30 -0
- data/spec/ast/xor_spec.rb +35 -0
- data/spec/benchmark.rb +70 -0
- data/spec/bulk_expression_solver_spec.rb +241 -0
- data/spec/calculator_spec.rb +1003 -0
- data/spec/dentaku_spec.rb +52 -0
- data/spec/dependency_resolver_spec.rb +18 -0
- data/spec/exceptions_spec.rb +9 -0
- data/spec/external_function_spec.rb +177 -0
- data/spec/parser_spec.rb +183 -0
- data/spec/print_visitor_spec.rb +77 -0
- data/spec/spec_helper.rb +69 -0
- data/spec/token_matcher_spec.rb +134 -0
- data/spec/token_scanner_spec.rb +49 -0
- data/spec/token_spec.rb +16 -0
- data/spec/tokenizer_spec.rb +375 -0
- data/spec/visitor/infix_spec.rb +52 -0
- data/spec/visitor_spec.rb +139 -0
- metadata +353 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/string_functions'
|
|
3
|
+
|
|
4
|
+
describe Dentaku::AST::StringFunctions::Left do
|
|
5
|
+
let(:string) { identifier('string') }
|
|
6
|
+
let(:length) { identifier('length') }
|
|
7
|
+
|
|
8
|
+
subject { described_class.new(string, length) }
|
|
9
|
+
|
|
10
|
+
it 'returns the left N characters of the string' do
|
|
11
|
+
expect(subject.value('string' => 'ABCDEFG', 'length' => 4)).to eq 'ABCD'
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
it 'works correctly with literals' do
|
|
15
|
+
left = literal('ABCD')
|
|
16
|
+
len = literal(2)
|
|
17
|
+
fn = described_class.new(left, len)
|
|
18
|
+
expect(fn.value).to eq 'AB'
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'handles an empty string correctly' do
|
|
22
|
+
expect(subject.value('string' => '', 'length' => 4)).to eq ''
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
it 'handles size greater than input string length correctly' do
|
|
26
|
+
expect(subject.value('string' => 'abcdefg', 'length' => 40)).to eq 'abcdefg'
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it 'accepts strings as length if they can be parsed to a number' do
|
|
30
|
+
expect(subject.value('string' => 'ABCDEFG', 'length' => '4')).to eq 'ABCD'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'has the proper type' do
|
|
34
|
+
expect(subject.type).to eq(:string)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'raises an error if given invalid length' do
|
|
38
|
+
expect {
|
|
39
|
+
subject.value('string' => 'abcdefg', 'length' => -2)
|
|
40
|
+
}.to raise_error(Dentaku::ArgumentError, /LEFT\(\) requires length to be positive/)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'raises an error when given a junk length' do
|
|
44
|
+
expect {
|
|
45
|
+
subject.value('string' => 'abcdefg', 'length' => 'junk')
|
|
46
|
+
}.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
describe Dentaku::AST::StringFunctions::Right do
|
|
51
|
+
it 'returns the right N characters of the string' do
|
|
52
|
+
subject = described_class.new(literal('ABCDEFG'), literal(4))
|
|
53
|
+
expect(subject.value).to eq 'DEFG'
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it 'handles an empty string correctly' do
|
|
57
|
+
subject = described_class.new(literal(''), literal(4))
|
|
58
|
+
expect(subject.value).to eq ''
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
it 'handles size greater than input string length correctly' do
|
|
62
|
+
subject = described_class.new(literal('abcdefg'), literal(40))
|
|
63
|
+
expect(subject.value).to eq 'abcdefg'
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it 'accepts strings as length if they can be parsed to a number' do
|
|
67
|
+
subject = described_class.new(literal('ABCDEFG'), literal('4'))
|
|
68
|
+
expect(subject.value).to eq 'DEFG'
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it 'has the proper type' do
|
|
72
|
+
expect(subject.type).to eq(:string)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
it 'raises an error when given a junk length' do
|
|
76
|
+
subject = described_class.new(literal('abcdefg'), literal('junk'))
|
|
77
|
+
expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe Dentaku::AST::StringFunctions::Mid do
|
|
82
|
+
it 'returns a substring from the middle of the string' do
|
|
83
|
+
subject = described_class.new(literal('ABCDEFG'), literal(4), literal(2))
|
|
84
|
+
expect(subject.value).to eq 'DE'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'handles an empty string correctly' do
|
|
88
|
+
subject = described_class.new(literal(''), literal(4), literal(2))
|
|
89
|
+
expect(subject.value).to eq ''
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
it 'handles offset greater than input string length correctly' do
|
|
93
|
+
subject = described_class.new(literal('abcdefg'), literal(40), literal(4))
|
|
94
|
+
expect(subject.value).to eq ''
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it 'handles size greater than input string length correctly' do
|
|
98
|
+
subject = described_class.new(literal('abcdefg'), literal(4), literal(40))
|
|
99
|
+
expect(subject.value).to eq 'defg'
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
it 'accepts strings as offset and length if they can be parsed to a number' do
|
|
103
|
+
subject = described_class.new(literal('ABCDEFG'), literal('4'), literal('2'))
|
|
104
|
+
expect(subject.value).to eq 'DE'
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
it 'has the proper type' do
|
|
108
|
+
expect(subject.type).to eq(:string)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'raises an error when given a junk offset' do
|
|
112
|
+
subject = described_class.new(literal('abcdefg'), literal('junk offset'), literal(2))
|
|
113
|
+
expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk offset' is not coercible to numeric")
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'raises an error when given a junk length' do
|
|
117
|
+
subject = described_class.new(literal('abcdefg'), literal(4), literal('junk'))
|
|
118
|
+
expect { subject.value }.to raise_error(Dentaku::ArgumentError, "'junk' is not coercible to numeric")
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
describe Dentaku::AST::StringFunctions::Len do
|
|
123
|
+
it 'returns the length of a string' do
|
|
124
|
+
subject = described_class.new(literal('ABCDEFG'))
|
|
125
|
+
expect(subject.value).to eq 7
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
it 'handles an empty string correctly' do
|
|
129
|
+
subject = described_class.new(literal(''))
|
|
130
|
+
expect(subject.value).to eq 0
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
it 'has the proper type' do
|
|
134
|
+
expect(subject.type).to eq(:numeric)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
describe Dentaku::AST::StringFunctions::Find do
|
|
139
|
+
it 'returns the position of a substring within a string' do
|
|
140
|
+
subject = described_class.new(literal('DE'), literal('ABCDEFG'))
|
|
141
|
+
expect(subject.value).to eq 4
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it 'handles an empty substring correctly' do
|
|
145
|
+
subject = described_class.new(literal(''), literal('ABCDEFG'))
|
|
146
|
+
expect(subject.value).to eq 1
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'handles an empty string correctly' do
|
|
150
|
+
subject = described_class.new(literal('DE'), literal(''))
|
|
151
|
+
expect(subject.value).to be_nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it 'has the proper type' do
|
|
155
|
+
expect(subject.type).to eq(:numeric)
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
describe Dentaku::AST::StringFunctions::Substitute do
|
|
160
|
+
it 'replaces a substring within a string' do
|
|
161
|
+
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal('xy'))
|
|
162
|
+
expect(subject.value).to eq 'ABCxyFG'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
it 'handles an empty search string correctly' do
|
|
166
|
+
subject = described_class.new(literal('ABCDEFG'), literal(''), literal('xy'))
|
|
167
|
+
expect(subject.value).to eq 'xyABCDEFG'
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
it 'handles an empty replacement string correctly' do
|
|
171
|
+
subject = described_class.new(literal('ABCDEFG'), literal('DE'), literal(''))
|
|
172
|
+
expect(subject.value).to eq 'ABCFG'
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'has the proper type' do
|
|
176
|
+
expect(subject.type).to eq(:string)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
describe Dentaku::AST::StringFunctions::Concat do
|
|
181
|
+
it 'concatenates two strings' do
|
|
182
|
+
subject = described_class.new(literal('ABC'), literal('DEF'))
|
|
183
|
+
expect(subject.value).to eq 'ABCDEF'
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
it 'concatenates a string onto an empty string' do
|
|
187
|
+
subject = described_class.new(literal(''), literal('ABC'))
|
|
188
|
+
expect(subject.value).to eq 'ABC'
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
it 'concatenates an empty string onto a string' do
|
|
192
|
+
subject = described_class.new(literal('ABC'), literal(''))
|
|
193
|
+
expect(subject.value).to eq 'ABC'
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
it 'concatenates two empty strings' do
|
|
197
|
+
subject = described_class.new(literal(''), literal(''))
|
|
198
|
+
expect(subject.value).to eq ''
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
it 'has the proper type' do
|
|
202
|
+
expect(subject.type).to eq(:string)
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
describe Dentaku::AST::StringFunctions::Contains do
|
|
207
|
+
it 'checks for substrings' do
|
|
208
|
+
subject = described_class.new(literal('app'), literal('apple'))
|
|
209
|
+
expect(subject.value).to be_truthy
|
|
210
|
+
subject = described_class.new(literal('app'), literal('orange'))
|
|
211
|
+
expect(subject.value).to be_falsy
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
it 'has the proper type' do
|
|
215
|
+
expect(subject.type).to eq(:logical)
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/sum'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Sum' do
|
|
6
|
+
it 'returns the sum of an array of Numeric values' do
|
|
7
|
+
result = Dentaku('SUM(1, x, 1.8)', x: 2.3)
|
|
8
|
+
expect(result).to eq(5.1)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns the sum of a single entry array of a Numeric value' do
|
|
12
|
+
result = Dentaku('SUM(x)', x: 2.3)
|
|
13
|
+
expect(result).to eq(2.3)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the sum even if a String is passed' do
|
|
17
|
+
result = Dentaku('SUM(1, x, 1.8)', x: '2.3')
|
|
18
|
+
expect(result).to eq(5.1)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the sum even if an array is passed' do
|
|
22
|
+
result = Dentaku('SUM(1, x, 2.3)', x: [4, 5])
|
|
23
|
+
expect(result).to eq(12.3)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'returns the sum of nested sums' do
|
|
27
|
+
result = Dentaku('SUM(1, x, SUM(4, 5))', x: '2.3')
|
|
28
|
+
expect(result).to eq(12.3)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'checking errors' do
|
|
32
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
33
|
+
|
|
34
|
+
it 'raises an error if no arguments are passed' do
|
|
35
|
+
expect { calculator.evaluate!('SUM()') }.to raise_error(Dentaku::ArgumentError)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'does not raise an error if an empty array is passed' do
|
|
39
|
+
result = calculator.evaluate!('SUM(x)', x: [])
|
|
40
|
+
expect(result).to eq(0)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku/ast/functions/switch'
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Function::Switch' do
|
|
6
|
+
it 'returns the match if present in argumtents' do
|
|
7
|
+
result = Dentaku('SWITCH(1, 1, "one", 2, "two")')
|
|
8
|
+
expect(result).to eq('one')
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
it 'returns nil if no match was found' do
|
|
12
|
+
result = Dentaku('SWITCH(3, 1, "one", 2, "two")')
|
|
13
|
+
expect(result).to eq(nil)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it 'returns the default value if present and no match was found' do
|
|
17
|
+
result = Dentaku('SWITCH(3, 1, "one", 2, "two", "no match")')
|
|
18
|
+
expect(result).to eq('no match')
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
it 'returns the first match if multiple matches exist' do
|
|
22
|
+
result = Dentaku('SWITCH(1, 1, "one", 2, "two", 1, "three")')
|
|
23
|
+
expect(result).to eq('one')
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
it 'does not return a match where a value matches the search value' do
|
|
27
|
+
result = Dentaku('SWITCH(1, "one", 1, 2, "two", 3)')
|
|
28
|
+
expect(result).to eq(3)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
3
|
+
require 'dentaku/ast/functions/or'
|
|
4
|
+
|
|
5
|
+
describe 'Dentaku::AST::Xor' do
|
|
6
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
7
|
+
|
|
8
|
+
it 'returns false if all of the arguments are false' do
|
|
9
|
+
result = Dentaku('XOR(false, false)')
|
|
10
|
+
expect(result).to eq(false)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
it 'returns true if only one of the arguments is true' do
|
|
14
|
+
result = Dentaku('XOR(false, true)')
|
|
15
|
+
expect(result).to eq(true)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
it 'returns false if more than one of the arguments is true' do
|
|
19
|
+
result = Dentaku('XOR(false, true, true)')
|
|
20
|
+
expect(result).to eq(false)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'supports nested expressions' do
|
|
24
|
+
result = Dentaku('XOR(y = 1, x = 1)', x: 1, y: 2)
|
|
25
|
+
expect(result).to eq(true)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it 'raises an error if no arguments are passed' do
|
|
29
|
+
expect { calculator.evaluate!('XOR()') }.to raise_error(Dentaku::ParseError)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'raises an error if a non logical argument is passed' do
|
|
33
|
+
expect { calculator.evaluate!('XOR("r")') }.to raise_error(Dentaku::ArgumentError)
|
|
34
|
+
end
|
|
35
|
+
end
|
data/spec/benchmark.rb
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'dentaku'
|
|
4
|
+
require 'allocation_stats'
|
|
5
|
+
require 'benchmark'
|
|
6
|
+
|
|
7
|
+
puts "Dentaku version #{Dentaku::VERSION}"
|
|
8
|
+
puts "Ruby version #{RUBY_VERSION}"
|
|
9
|
+
|
|
10
|
+
with_duplicate_variables = [
|
|
11
|
+
"R1+R2+R3+R4+R5+R6",
|
|
12
|
+
{"R1" => 100000, "R2" => 0, "R3" => 200000, "R4" => 0, "R5" => 500000, "R6" => 0, "r1" => 100000, "r2" => 0, "r3" => 200000, "r4" => 0, "r5" => 500000, "r6" => 0}
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
without_duplicate_variables = [
|
|
16
|
+
"R1+R2+R3+R4+R5+R6",
|
|
17
|
+
{"R1" => 100000, "R2" => 0, "R3" => 200000, "R4" => 0, "R5" => 500000, "R6" => 0}
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
def test(args, custom_function: true)
|
|
21
|
+
calls = [ args ] * 100
|
|
22
|
+
|
|
23
|
+
10.times do |i|
|
|
24
|
+
|
|
25
|
+
stats = nil
|
|
26
|
+
bm = Benchmark.measure do
|
|
27
|
+
stats = AllocationStats.trace do
|
|
28
|
+
|
|
29
|
+
calls.each do |formula, bound|
|
|
30
|
+
|
|
31
|
+
calculator = Dentaku::Calculator.new
|
|
32
|
+
|
|
33
|
+
if custom_function
|
|
34
|
+
calculator.add_function(
|
|
35
|
+
:sum,
|
|
36
|
+
:numeric,
|
|
37
|
+
->(numbers) { numbers.inject(:+) }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
calculator.evaluate(formula, bound)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
puts " run #{i}: #{bm.total}"
|
|
47
|
+
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
case ARGV[0]
|
|
52
|
+
when '1'
|
|
53
|
+
puts "with duplicate (downcased) variables, with a custom function:"
|
|
54
|
+
test(with_duplicate_variables, custom_function: true)
|
|
55
|
+
|
|
56
|
+
when '2'
|
|
57
|
+
puts "with duplicate (downcased) variables, without a custom function:"
|
|
58
|
+
test(with_duplicate_variables, custom_function: false)
|
|
59
|
+
|
|
60
|
+
when '3'
|
|
61
|
+
puts "without duplicate (downcased) variables, with a custom function:"
|
|
62
|
+
test(without_duplicate_variables, custom_function: true)
|
|
63
|
+
|
|
64
|
+
when '4'
|
|
65
|
+
puts "with duplicate (downcased) variables, without a custom function:"
|
|
66
|
+
test(without_duplicate_variables, custom_function: false)
|
|
67
|
+
|
|
68
|
+
else
|
|
69
|
+
puts "select a run option (1-4)"
|
|
70
|
+
end
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
require 'dentaku'
|
|
3
|
+
require 'dentaku/bulk_expression_solver'
|
|
4
|
+
require 'dentaku/calculator'
|
|
5
|
+
require 'dentaku/exceptions'
|
|
6
|
+
|
|
7
|
+
RSpec.describe Dentaku::BulkExpressionSolver do
|
|
8
|
+
let(:calculator) { Dentaku::Calculator.new }
|
|
9
|
+
|
|
10
|
+
describe "#solve!" do
|
|
11
|
+
it "evaluates properly with variables, even if some in memory" do
|
|
12
|
+
expressions = {
|
|
13
|
+
weekly_fruit_budget: "weekly_apple_budget + pear * 4",
|
|
14
|
+
weekly_apple_budget: "apples * 7",
|
|
15
|
+
pear: "1"
|
|
16
|
+
}
|
|
17
|
+
solver = described_class.new(expressions, calculator.store(apples: 3))
|
|
18
|
+
expect(solver.solve!)
|
|
19
|
+
.to eq(pear: 1, weekly_apple_budget: 21, weekly_fruit_budget: 25)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "lets you know if a variable is unbound" do
|
|
23
|
+
expressions = {more_apples: "apples + 1"}
|
|
24
|
+
expect {
|
|
25
|
+
described_class.new(expressions, calculator).solve!
|
|
26
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
it "properly handles access on an unbound variable" do
|
|
30
|
+
expressions = {more_apples: "apples[0]"}
|
|
31
|
+
expect {
|
|
32
|
+
described_class.new(expressions, calculator).solve!
|
|
33
|
+
}.to raise_error(Dentaku::UnboundVariableError)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "lets you know if the result is a div/0 error when dividing" do
|
|
37
|
+
expressions = {more_apples: "1/0"}
|
|
38
|
+
expect {
|
|
39
|
+
described_class.new(expressions, calculator).solve!
|
|
40
|
+
}.to raise_error(Dentaku::ZeroDivisionError)
|
|
41
|
+
end
|
|
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
|
+
|
|
50
|
+
it "does not require keys to be parseable" do
|
|
51
|
+
expressions = { "the value of x, incremented" => "x + 1" }
|
|
52
|
+
solver = described_class.new(expressions, calculator.store("x" => 3))
|
|
53
|
+
expect(solver.solve!).to eq("the value of x, incremented" => 4)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
it "allows self-referential formulas" do
|
|
57
|
+
expressions = { x: "x + 1" }
|
|
58
|
+
solver = described_class.new(expressions, calculator.store(x: 1))
|
|
59
|
+
expect(solver.solve!).to eq(x: 2)
|
|
60
|
+
|
|
61
|
+
expressions = { x: "y + 3", y: "x * 2" }
|
|
62
|
+
solver = described_class.new(expressions, calculator.store(x: 5, y: 3))
|
|
63
|
+
expect(solver.solve!).to eq(x: 6, y: 12) # x = 6 by the time y is calculated
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
it "does not execute functions unnecessarily" do
|
|
67
|
+
calls = 0
|
|
68
|
+
external = ->() { calls += 1 }
|
|
69
|
+
hash = {test: 'EXTERNAL()'}
|
|
70
|
+
calculator = Dentaku::Calculator.new
|
|
71
|
+
calculator.add_function(:external, :numeric, external)
|
|
72
|
+
calculator.solve(hash)
|
|
73
|
+
expect(calls).to eq(1)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "evaluates expressions in hashes and arrays, and expands the results" do
|
|
77
|
+
calculator.store(
|
|
78
|
+
fruit_quantities: {
|
|
79
|
+
apple: 5,
|
|
80
|
+
pear: 9
|
|
81
|
+
},
|
|
82
|
+
fruit_prices: {
|
|
83
|
+
apple: 1.66,
|
|
84
|
+
pear: 2.50
|
|
85
|
+
}
|
|
86
|
+
)
|
|
87
|
+
expressions = {
|
|
88
|
+
weekly_budget: {
|
|
89
|
+
fruit: "weekly_budget.apples + weekly_budget.pears",
|
|
90
|
+
apples: "fruit_quantities.apple * discounted_fruit_prices.apple",
|
|
91
|
+
pears: "fruit_quantities.pear * discounted_fruit_prices.pear",
|
|
92
|
+
},
|
|
93
|
+
discounted_fruit_prices: {
|
|
94
|
+
apple: "round(fruit_prices.apple * discounts[0], 2)",
|
|
95
|
+
pear: "round(fruit_prices.pear * discounts[1], 2)"
|
|
96
|
+
},
|
|
97
|
+
discounts: ["0.4 * 2", "0.3 * 2"],
|
|
98
|
+
}
|
|
99
|
+
solver = described_class.new(expressions, calculator)
|
|
100
|
+
|
|
101
|
+
expect(solver.solve!).to eq(
|
|
102
|
+
weekly_budget: {
|
|
103
|
+
fruit: 20.15,
|
|
104
|
+
apples: 6.65,
|
|
105
|
+
pears: 13.50
|
|
106
|
+
},
|
|
107
|
+
discounted_fruit_prices: {
|
|
108
|
+
apple: 1.33,
|
|
109
|
+
pear: 1.50
|
|
110
|
+
},
|
|
111
|
+
discounts: [0.8, 0.6]
|
|
112
|
+
)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
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
|
+
|
|
133
|
+
it "returns :undefined when variables are unbound" do
|
|
134
|
+
expressions = {more_apples: "apples + 1"}
|
|
135
|
+
expect(described_class.new(expressions, calculator).solve)
|
|
136
|
+
.to eq(more_apples: :undefined)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
it "allows passing in a custom value to an error handler when a variable is unbound" do
|
|
140
|
+
expressions = {more_apples: "apples + 1"}
|
|
141
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
|
142
|
+
.to eq(more_apples: :foo)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
it "allows passing in a custom value to an error handler when there is a div/0 error" do
|
|
146
|
+
expressions = {more_apples: "1/0"}
|
|
147
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
|
148
|
+
.to eq(more_apples: :foo)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
it "allows passing in ast as expression" do
|
|
152
|
+
expressions = {more_apples: calculator.ast("1/0")}
|
|
153
|
+
expect(described_class.new(expressions, calculator).solve { :foo })
|
|
154
|
+
.to eq(more_apples: :foo)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
it 'stores the recipient variable on the exception when there is a div/0 error' do
|
|
158
|
+
expressions = {more_apples: "1/0"}
|
|
159
|
+
exception = nil
|
|
160
|
+
described_class.new(expressions, calculator).solve do |ex|
|
|
161
|
+
exception = ex
|
|
162
|
+
end
|
|
163
|
+
expect(exception.recipient_variable).to eq('more_apples')
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
it 'stores the recipient variable on the exception when there is an unbound variable' do
|
|
167
|
+
expressions = {more_apples: "apples + 1"}
|
|
168
|
+
exception = nil
|
|
169
|
+
described_class.new(expressions, calculator).solve do |ex|
|
|
170
|
+
exception = ex
|
|
171
|
+
end
|
|
172
|
+
expect(exception.recipient_variable).to eq('more_apples')
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
it 'stores the recipient variable on the exception when there is an ArgumentError' do
|
|
176
|
+
expressions = {apples: "NULL", more_apples: "1 + apples"}
|
|
177
|
+
exception = nil
|
|
178
|
+
described_class.new(expressions, calculator).solve do |ex|
|
|
179
|
+
exception = ex
|
|
180
|
+
end
|
|
181
|
+
expect(exception.recipient_variable).to eq('more_apples')
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'safely handles argument errors' do
|
|
185
|
+
expressions = {i: "a / 5 + d", a: "m * 12", d: "a + b"}
|
|
186
|
+
result = described_class.new(expressions, calculator.store(m: 3)).solve
|
|
187
|
+
expect(result).to eq(
|
|
188
|
+
i: :undefined,
|
|
189
|
+
d: :undefined,
|
|
190
|
+
a: 36,
|
|
191
|
+
)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
it 'supports nested hashes of expressions using dot notation' do
|
|
195
|
+
expressions = {
|
|
196
|
+
a: "25",
|
|
197
|
+
b: {
|
|
198
|
+
c: "a / 5",
|
|
199
|
+
d: [3, 4, 5]
|
|
200
|
+
},
|
|
201
|
+
e: ["b.c + b.d[1]"],
|
|
202
|
+
f: "e[0] + 1"
|
|
203
|
+
}
|
|
204
|
+
results = described_class.new(expressions, calculator).solve
|
|
205
|
+
expect(results[:f]).to eq(10)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
it 'uses stored values for expressions when they are known' do
|
|
209
|
+
calculator.store(Force: 50, Mass: 25)
|
|
210
|
+
expressions = {
|
|
211
|
+
Force: "Mass * Acceleration",
|
|
212
|
+
Mass: "Force / Acceleration",
|
|
213
|
+
Acceleration: "Force / Mass",
|
|
214
|
+
}
|
|
215
|
+
solver = described_class.new(expressions, calculator)
|
|
216
|
+
results = solver.solve
|
|
217
|
+
expect(results).to eq(Force: 50, Mass: 25, Acceleration: 2)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
it 'solves all array expressions for which context exists, returning :undefined for the rest' do
|
|
221
|
+
calculator.store(first: 1, equation: 3)
|
|
222
|
+
system = {'key' => ['first * equation', 'second * equation'] }
|
|
223
|
+
solver = described_class.new(system, calculator)
|
|
224
|
+
expect(solver.dependencies).to eq('key' => ['second'])
|
|
225
|
+
results = solver.solve
|
|
226
|
+
expect(results).to eq('key' => [3, :undefined])
|
|
227
|
+
expect { solver.solve! }.to raise_error(Dentaku::UnboundVariableError)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
it do
|
|
231
|
+
calculator.store(val: nil)
|
|
232
|
+
expressions = {
|
|
233
|
+
a: 'IF(5 / 0 > 0, 100, 1000)',
|
|
234
|
+
b: 'IF(val = 0, 0, IF(val > 0, 0, 0))'
|
|
235
|
+
}
|
|
236
|
+
solver = described_class.new(expressions, calculator)
|
|
237
|
+
results = solver.solve
|
|
238
|
+
expect(results).to eq(a: :undefined, b: :undefined)
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|