dentaku 2.0.8 → 2.0.9
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/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
|