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 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