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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5dd3288b6e13441c5e71ac914a527a9c4d97daa
4
- data.tar.gz: 88c305d4010f094f2f59615fd830ea583274d9ca
3
+ metadata.gz: cc6a362189b735fe68596e1b94e28787d7290ac8
4
+ data.tar.gz: cec7635943905eaa4cb8cd888649f5c34a0c1871
5
5
  SHA512:
6
- metadata.gz: 588690a66352277de04db87a048047c54d47bf8bd5ce92f3b1d841345a5cf721c903631a283d8912346ccead66242c13ae2f86e3ad186a1ab9b943e05988d095
7
- data.tar.gz: 38b5f255f8e5bac861729589a71b7d7b6deec651bfc0fde17bfb07e96c2c710c758f58835dd9071bf695bb6dda45c532e6a25fc895e908ea10066a7450ac8123
6
+ metadata.gz: d82af702ad0a2b009680e7a3292388174915408f7d8e67c5b5cb0d545bd24544edd40a66a0611268bbb5614920c2de386784f8c7132c420802a1577d30b67a43
7
+ data.tar.gz: 417cd814e7f46973c312568e8dcd9dd4bbe9fe1dfa27abcb8ac178c58e78850c477d99f9a76abd285abd8141d1e647dceb52b40edcb54681743d9af95cde9684
@@ -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 figure out dependencies (using `TSort`) for you.
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)[0, @length.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).length
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
- haystack = @haystack.value(context)
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
- replacement = @replacement.value(context)
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
- @left.value(context) + @right.value(context)
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
@@ -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 Dentaku.cache_ast?
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
@@ -13,6 +13,9 @@ module Dentaku
13
13
  class ParseError < StandardError
14
14
  end
15
15
 
16
+ class TokenizerError < StandardError
17
+ end
18
+
16
19
  class ArgumentError < ::ArgumentError
17
20
  end
18
21
 
@@ -13,13 +13,13 @@ module Dentaku
13
13
  input = strip_comments(string.to_s.dup)
14
14
 
15
15
  until input.empty?
16
- raise "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
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
- raise "too many opening parentheses" if @nesting > 0
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
- raise "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
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
- raise "too many closing parentheses" if @nesting < 0
38
+ fail TokenizerError, "too many closing parentheses" if @nesting < 0
39
39
 
40
40
  @tokens << token unless token.is?(:whitespace)
41
41
  end
@@ -1,3 +1,3 @@
1
1
  module Dentaku
2
- VERSION = "2.0.8"
2
+ VERSION = "2.0.9"
3
3
  end
@@ -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(
@@ -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)
@@ -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(RuntimeError, /too many opening parentheses/)
146
- expect { tokenizer.tokenize(')') }.to raise_error(RuntimeError, /too many closing parentheses/)
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.8
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-05-10 00:00:00.000000000 Z
11
+ date: 2016-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake