dentaku 2.0.8 → 2.0.9
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +8 -3
- data/lib/dentaku/ast/arithmetic.rb +21 -0
- data/lib/dentaku/ast/functions/string_functions.rb +15 -8
- data/lib/dentaku/calculator.rb +26 -2
- data/lib/dentaku/exceptions.rb +3 -0
- data/lib/dentaku/tokenizer.rb +4 -4
- data/lib/dentaku/version.rb +1 -1
- data/spec/calculator_spec.rb +61 -0
- data/spec/parser_spec.rb +8 -0
- data/spec/tokenizer_spec.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc6a362189b735fe68596e1b94e28787d7290ac8
|
4
|
+
data.tar.gz: cec7635943905eaa4cb8cd888649f5c34a0c1871
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d82af702ad0a2b009680e7a3292388174915408f7d8e67c5b5cb0d545bd24544edd40a66a0611268bbb5614920c2de386784f8c7132c420802a1577d30b67a43
|
7
|
+
data.tar.gz: 417cd814e7f46973c312568e8dcd9dd4bbe9fe1dfa27abcb8ac178c58e78850c477d99f9a76abd285abd8141d1e647dceb52b40edcb54681743d9af95cde9684
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
# Change Log
|
2
2
|
|
3
|
+
## [v2.0.9] 2016-09-19
|
4
|
+
- namespace tokenization errors
|
5
|
+
- automatically coerce arguments to string functions as strings
|
6
|
+
- selectively disable or clear AST cache
|
7
|
+
|
3
8
|
## [v2.0.8] 2016-05-10
|
4
9
|
- numeric input validations
|
5
10
|
- fail with gem-specific error for invalid arithmetic operands
|
@@ -106,6 +111,7 @@
|
|
106
111
|
## [v0.1.0] 2012-01-20
|
107
112
|
- initial release
|
108
113
|
|
114
|
+
[v2.0.9]: https://github.com/rubysolo/dentaku/compare/v2.0.8...v2.0.9
|
109
115
|
[v2.0.8]: https://github.com/rubysolo/dentaku/compare/v2.0.7...v2.0.8
|
110
116
|
[v2.0.7]: https://github.com/rubysolo/dentaku/compare/v2.0.6...v2.0.7
|
111
117
|
[v2.0.6]: https://github.com/rubysolo/dentaku/compare/v2.0.5...v2.0.6
|
data/README.md
CHANGED
@@ -169,11 +169,11 @@ calc.dependencies("annual_income / 5")
|
|
169
169
|
#=> [:annual_income]
|
170
170
|
```
|
171
171
|
|
172
|
-
#### Calculator.solve!
|
172
|
+
#### Calculator.solve! / Calculator.solve
|
173
173
|
Have Dentaku figure out the order in which your formulas need to be evaluated.
|
174
174
|
|
175
175
|
Pass in a hash of `{eventual_variable_name: "expression"}` to `solve!` and
|
176
|
-
have Dentaku
|
176
|
+
have Dentaku resolve dependencies (using `TSort`) for you.
|
177
177
|
|
178
178
|
Raises `TSort::Cyclic` when a valid expression order cannot be found.
|
179
179
|
|
@@ -194,6 +194,11 @@ calc.solve!(
|
|
194
194
|
#=> raises TSort::Cyclic
|
195
195
|
```
|
196
196
|
|
197
|
+
`solve!` will also raise an exception if any of the formulas in the set cannot
|
198
|
+
be evaluated (e.g. raise `ZeroDivisionError`). The non-bang `solve` method will
|
199
|
+
find as many solutions as possible and return the symbol `:undefined` for the
|
200
|
+
problem formulas.
|
201
|
+
|
197
202
|
INLINE COMMENTS
|
198
203
|
---------------------------------
|
199
204
|
|
@@ -272,7 +277,7 @@ LICENSE
|
|
272
277
|
|
273
278
|
(The MIT License)
|
274
279
|
|
275
|
-
Copyright © 2012 Solomon White
|
280
|
+
Copyright © 2012-2016 Solomon White
|
276
281
|
|
277
282
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
278
283
|
this software and associated documentation files (the ‘Software’), to deal in
|
@@ -86,6 +86,27 @@ module Dentaku
|
|
86
86
|
end
|
87
87
|
|
88
88
|
class Modulo < Arithmetic
|
89
|
+
def initialize(left, right)
|
90
|
+
@left = left
|
91
|
+
@right = right
|
92
|
+
|
93
|
+
unless (valid_node?(left) || left.nil?) && valid_node?(right)
|
94
|
+
fail ParseError, "#{ self.class } requires numeric operands"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def percent?
|
99
|
+
left.nil?
|
100
|
+
end
|
101
|
+
|
102
|
+
def value(context={})
|
103
|
+
if percent?
|
104
|
+
cast(right.value(context)) * 0.01
|
105
|
+
else
|
106
|
+
super
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
89
110
|
def operator
|
90
111
|
:%
|
91
112
|
end
|
@@ -10,7 +10,9 @@ module Dentaku
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def value(context={})
|
13
|
-
@string.value(context)
|
13
|
+
string = @string.value(context).to_s
|
14
|
+
length = @length.value(context)
|
15
|
+
string[0, length]
|
14
16
|
end
|
15
17
|
end
|
16
18
|
|
@@ -21,7 +23,7 @@ module Dentaku
|
|
21
23
|
end
|
22
24
|
|
23
25
|
def value(context={})
|
24
|
-
string = @string.value(context)
|
26
|
+
string = @string.value(context).to_s
|
25
27
|
length = @length.value(context)
|
26
28
|
string[length * -1, length] || string
|
27
29
|
end
|
@@ -35,7 +37,7 @@ module Dentaku
|
|
35
37
|
end
|
36
38
|
|
37
39
|
def value(context={})
|
38
|
-
string = @string.value(context)
|
40
|
+
string = @string.value(context).to_s
|
39
41
|
offset = @offset.value(context)
|
40
42
|
length = @length.value(context)
|
41
43
|
string[offset - 1, length].to_s
|
@@ -48,7 +50,8 @@ module Dentaku
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def value(context={})
|
51
|
-
@string.value(context).
|
53
|
+
string = @string.value(context).to_s
|
54
|
+
string.length
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
@@ -60,7 +63,8 @@ module Dentaku
|
|
60
63
|
|
61
64
|
def value(context={})
|
62
65
|
needle = @needle.value(context)
|
63
|
-
|
66
|
+
needle = needle.to_s unless needle.is_a?(Regexp)
|
67
|
+
haystack = @haystack.value(context).to_s
|
64
68
|
pos = haystack.index(needle)
|
65
69
|
pos && pos + 1
|
66
70
|
end
|
@@ -74,9 +78,10 @@ module Dentaku
|
|
74
78
|
end
|
75
79
|
|
76
80
|
def value(context={})
|
77
|
-
original = @original.value(context)
|
81
|
+
original = @original.value(context).to_s
|
78
82
|
search = @search.value(context)
|
79
|
-
|
83
|
+
search = search.to_s unless search.is_a?(Regexp)
|
84
|
+
replacement = @replacement.value(context).to_s
|
80
85
|
original.sub(search, replacement)
|
81
86
|
end
|
82
87
|
end
|
@@ -88,7 +93,9 @@ module Dentaku
|
|
88
93
|
end
|
89
94
|
|
90
95
|
def value(context={})
|
91
|
-
|
96
|
+
left = @left.value(context).to_s
|
97
|
+
right = @right.value(context).to_s
|
98
|
+
left + right
|
92
99
|
end
|
93
100
|
end
|
94
101
|
end
|
data/lib/dentaku/calculator.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'dentaku'
|
2
1
|
require 'dentaku/bulk_expression_solver'
|
3
2
|
require 'dentaku/exceptions'
|
4
3
|
require 'dentaku/token'
|
@@ -13,6 +12,7 @@ module Dentaku
|
|
13
12
|
clear
|
14
13
|
@tokenizer = Tokenizer.new
|
15
14
|
@ast_cache = {}
|
15
|
+
@disable_ast_cache = false
|
16
16
|
end
|
17
17
|
|
18
18
|
def add_function(name, type, body)
|
@@ -25,6 +25,13 @@ module Dentaku
|
|
25
25
|
self
|
26
26
|
end
|
27
27
|
|
28
|
+
def disable_cache
|
29
|
+
@disable_ast_cache = true
|
30
|
+
yield(self) if block_given?
|
31
|
+
ensure
|
32
|
+
@disable_ast_cache = false
|
33
|
+
end
|
34
|
+
|
28
35
|
def evaluate(expression, data={})
|
29
36
|
evaluate!(expression, data)
|
30
37
|
rescue UnboundVariableError, ArgumentError
|
@@ -54,11 +61,24 @@ module Dentaku
|
|
54
61
|
def ast(expression)
|
55
62
|
@ast_cache.fetch(expression) {
|
56
63
|
Parser.new(tokenizer.tokenize(expression)).parse.tap do |node|
|
57
|
-
@ast_cache[expression] = node if
|
64
|
+
@ast_cache[expression] = node if cache_ast?
|
58
65
|
end
|
59
66
|
}
|
60
67
|
end
|
61
68
|
|
69
|
+
def clear_cache(pattern=:all)
|
70
|
+
case pattern
|
71
|
+
when :all
|
72
|
+
@ast_cache = {}
|
73
|
+
when String
|
74
|
+
@ast_cache.delete(pattern)
|
75
|
+
when Regexp
|
76
|
+
@ast_cache.delete_if { |k,_| k =~ pattern }
|
77
|
+
else
|
78
|
+
fail Dentaku::ArgumentError
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
62
82
|
def store(key_or_hash, value=nil)
|
63
83
|
restore = Hash[memory]
|
64
84
|
|
@@ -96,5 +116,9 @@ module Dentaku
|
|
96
116
|
def empty?
|
97
117
|
memory.empty?
|
98
118
|
end
|
119
|
+
|
120
|
+
def cache_ast?
|
121
|
+
Dentaku.cache_ast? && !@disable_ast_cache
|
122
|
+
end
|
99
123
|
end
|
100
124
|
end
|
data/lib/dentaku/exceptions.rb
CHANGED
data/lib/dentaku/tokenizer.rb
CHANGED
@@ -13,13 +13,13 @@ module Dentaku
|
|
13
13
|
input = strip_comments(string.to_s.dup)
|
14
14
|
|
15
15
|
until input.empty?
|
16
|
-
|
16
|
+
fail TokenizerError, "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
|
17
17
|
scanned, input = scan(input, scanner)
|
18
18
|
scanned
|
19
19
|
end
|
20
20
|
end
|
21
21
|
|
22
|
-
|
22
|
+
fail TokenizerError, "too many opening parentheses" if @nesting > 0
|
23
23
|
|
24
24
|
@tokens
|
25
25
|
end
|
@@ -31,11 +31,11 @@ module Dentaku
|
|
31
31
|
def scan(string, scanner)
|
32
32
|
if tokens = scanner.scan(string, last_token)
|
33
33
|
tokens.each do |token|
|
34
|
-
|
34
|
+
fail TokenizerError, "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
|
35
35
|
|
36
36
|
@nesting += 1 if LPAREN == token
|
37
37
|
@nesting -= 1 if RPAREN == token
|
38
|
-
|
38
|
+
fail TokenizerError, "too many closing parentheses" if @nesting < 0
|
39
39
|
|
40
40
|
@tokens << token unless token.is?(:whitespace)
|
41
41
|
end
|
data/lib/dentaku/version.rb
CHANGED
data/spec/calculator_spec.rb
CHANGED
@@ -142,6 +142,20 @@ describe Dentaku::Calculator do
|
|
142
142
|
more_peaches: 2
|
143
143
|
)
|
144
144
|
end
|
145
|
+
|
146
|
+
it "solves remainder of expressions when one cannot be evaluated" do
|
147
|
+
result = calculator.solve(
|
148
|
+
conditional: "IF(d != 0, ratio, 0)",
|
149
|
+
ratio: "10/d",
|
150
|
+
d: 0,
|
151
|
+
)
|
152
|
+
|
153
|
+
expect(result).to eq(
|
154
|
+
conditional: 0,
|
155
|
+
ratio: :undefined,
|
156
|
+
d: 0,
|
157
|
+
)
|
158
|
+
end
|
145
159
|
end
|
146
160
|
|
147
161
|
it 'evaluates a statement with no variables' do
|
@@ -379,6 +393,53 @@ describe Dentaku::Calculator do
|
|
379
393
|
end
|
380
394
|
end
|
381
395
|
|
396
|
+
describe 'disable_cache' do
|
397
|
+
before do
|
398
|
+
allow(Dentaku).to receive(:cache_ast?) { true }
|
399
|
+
end
|
400
|
+
|
401
|
+
it 'disables the AST cache' do
|
402
|
+
expect(calculator.disable_cache{ |c| c.cache_ast? }).to be false
|
403
|
+
end
|
404
|
+
|
405
|
+
it 'calculates normally' do
|
406
|
+
expect(calculator.disable_cache{ |c| c.evaluate("2 + 2") }).to eq(4)
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
describe 'clear_cache' do
|
411
|
+
before do
|
412
|
+
allow(Dentaku).to receive(:cache_ast?) { true }
|
413
|
+
|
414
|
+
calculator.ast("1+1")
|
415
|
+
calculator.ast("pineapples * 5")
|
416
|
+
calculator.ast("pi * radius ^ 2")
|
417
|
+
|
418
|
+
def calculator.ast_cache
|
419
|
+
@ast_cache
|
420
|
+
end
|
421
|
+
end
|
422
|
+
|
423
|
+
it 'clears all items from cache' do
|
424
|
+
expect(calculator.ast_cache.length).to eq 3
|
425
|
+
calculator.clear_cache
|
426
|
+
expect(calculator.ast_cache.keys).to be_empty
|
427
|
+
end
|
428
|
+
|
429
|
+
it 'clears one item from cache' do
|
430
|
+
calculator.clear_cache("1+1")
|
431
|
+
expect(calculator.ast_cache.keys.sort).to eq([
|
432
|
+
'pi * radius ^ 2',
|
433
|
+
'pineapples * 5',
|
434
|
+
])
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'clears items matching regex from cache' do
|
438
|
+
calculator.clear_cache(/^pi/)
|
439
|
+
expect(calculator.ast_cache.keys.sort).to eq(['1+1'])
|
440
|
+
end
|
441
|
+
end
|
442
|
+
|
382
443
|
describe 'string functions' do
|
383
444
|
it 'concatenates two strings' do
|
384
445
|
expect(
|
data/spec/parser_spec.rb
CHANGED
@@ -27,6 +27,14 @@ describe Dentaku::Parser do
|
|
27
27
|
expect(node.value).to eq false
|
28
28
|
end
|
29
29
|
|
30
|
+
it 'calculates unary percentage' do
|
31
|
+
five = Dentaku::Token.new(:numeric, 5)
|
32
|
+
mod = Dentaku::Token.new(:operator, :mod)
|
33
|
+
|
34
|
+
node = described_class.new([five, mod]).parse
|
35
|
+
expect(node.value).to eq 0.05
|
36
|
+
end
|
37
|
+
|
30
38
|
it 'performs multiple operations in one stream' do
|
31
39
|
five = Dentaku::Token.new(:numeric, 5)
|
32
40
|
plus = Dentaku::Token.new(:operator, :add)
|
data/spec/tokenizer_spec.rb
CHANGED
@@ -142,8 +142,8 @@ describe Dentaku::Tokenizer do
|
|
142
142
|
end
|
143
143
|
|
144
144
|
it 'detects unbalanced parentheses' do
|
145
|
-
expect { tokenizer.tokenize('(5+3') }.to raise_error(
|
146
|
-
expect { tokenizer.tokenize(')') }.to raise_error(
|
145
|
+
expect { tokenizer.tokenize('(5+3') }.to raise_error(Dentaku::TokenizerError, /too many opening parentheses/)
|
146
|
+
expect { tokenizer.tokenize(')') }.to raise_error(Dentaku::TokenizerError, /too many closing parentheses/)
|
147
147
|
end
|
148
148
|
|
149
149
|
it 'recognizes identifiers that share initial substrings with combinators' do
|
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: 2.0.
|
4
|
+
version: 2.0.9
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Solomon White
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-19 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|