dentaku 1.2.6 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +52 -57
- data/Rakefile +1 -1
- data/lib/dentaku.rb +8 -0
- data/lib/dentaku/ast.rb +22 -0
- data/lib/dentaku/ast/addition.rb +15 -0
- data/lib/dentaku/ast/combinators.rb +15 -0
- data/lib/dentaku/ast/comparators.rb +47 -0
- data/lib/dentaku/ast/division.rb +15 -0
- data/lib/dentaku/ast/exponentiation.rb +15 -0
- data/lib/dentaku/ast/function.rb +54 -0
- data/lib/dentaku/ast/functions/if.rb +26 -0
- data/lib/dentaku/ast/functions/max.rb +5 -0
- data/lib/dentaku/ast/functions/min.rb +5 -0
- data/lib/dentaku/ast/functions/not.rb +5 -0
- data/lib/dentaku/ast/functions/round.rb +5 -0
- data/lib/dentaku/ast/functions/rounddown.rb +5 -0
- data/lib/dentaku/ast/functions/roundup.rb +5 -0
- data/lib/dentaku/ast/functions/ruby_math.rb +8 -0
- data/lib/dentaku/ast/grouping.rb +13 -0
- data/lib/dentaku/ast/identifier.rb +29 -0
- data/lib/dentaku/ast/multiplication.rb +15 -0
- data/lib/dentaku/ast/negation.rb +25 -0
- data/lib/dentaku/ast/nil.rb +9 -0
- data/lib/dentaku/ast/node.rb +13 -0
- data/lib/dentaku/ast/numeric.rb +17 -0
- data/lib/dentaku/ast/operation.rb +20 -0
- data/lib/dentaku/ast/string.rb +17 -0
- data/lib/dentaku/ast/subtraction.rb +15 -0
- data/lib/dentaku/bulk_expression_solver.rb +6 -11
- data/lib/dentaku/calculator.rb +26 -20
- data/lib/dentaku/parser.rb +131 -0
- data/lib/dentaku/token.rb +4 -0
- data/lib/dentaku/token_matchers.rb +29 -0
- data/lib/dentaku/token_scanner.rb +18 -3
- data/lib/dentaku/tokenizer.rb +10 -2
- data/lib/dentaku/version.rb +1 -1
- data/spec/ast/function_spec.rb +19 -0
- data/spec/ast/node_spec.rb +37 -0
- data/spec/bulk_expression_solver_spec.rb +12 -5
- data/spec/calculator_spec.rb +14 -1
- data/spec/external_function_spec.rb +12 -28
- data/spec/parser_spec.rb +88 -0
- data/spec/spec_helper.rb +2 -1
- data/spec/token_scanner_spec.rb +4 -3
- data/spec/tokenizer_spec.rb +32 -6
- metadata +36 -16
- data/lib/dentaku/binary_operation.rb +0 -35
- data/lib/dentaku/evaluator.rb +0 -166
- data/lib/dentaku/expression.rb +0 -56
- data/lib/dentaku/external_function.rb +0 -10
- data/lib/dentaku/rule_set.rb +0 -153
- data/spec/binary_operation_spec.rb +0 -45
- data/spec/evaluator_spec.rb +0 -145
- data/spec/expression_spec.rb +0 -25
- data/spec/rule_set_spec.rb +0 -43
data/lib/dentaku/token.rb
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Dentaku
|
2
|
+
module TokenMatchers
|
3
|
+
def self.token_matchers(*symbols)
|
4
|
+
symbols.map { |s| matcher(s) }
|
5
|
+
end
|
6
|
+
|
7
|
+
def self.function_token_matchers(function_name, *symbols)
|
8
|
+
token_matchers(:fopen, *symbols, :close).unshift(
|
9
|
+
TokenMatcher.send(function_name)
|
10
|
+
)
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.matcher(symbol)
|
14
|
+
@matchers ||= [
|
15
|
+
:numeric, :string, :addsub, :subtract, :muldiv, :pow, :mod,
|
16
|
+
:comparator, :comp_gt, :comp_lt, :fopen, :open, :close, :comma,
|
17
|
+
:non_close_plus, :non_group, :non_group_star, :arguments,
|
18
|
+
:logical, :combinator, :if, :round, :roundup, :rounddown, :not,
|
19
|
+
:anchored_minus, :math_neg_pow, :math_neg_mul
|
20
|
+
].each_with_object({}) do |name, matchers|
|
21
|
+
matchers[name] = TokenMatcher.send(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
@matchers.fetch(symbol) do
|
25
|
+
raise "Unknown token symbol #{ symbol }"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -1,15 +1,17 @@
|
|
1
|
+
require 'bigdecimal'
|
1
2
|
require 'dentaku/token'
|
2
3
|
|
3
4
|
module Dentaku
|
4
5
|
class TokenScanner
|
5
|
-
def initialize(category, regexp, converter=nil)
|
6
|
+
def initialize(category, regexp, converter=nil, condition=nil)
|
6
7
|
@category = category
|
7
8
|
@regexp = %r{\A(#{ regexp })}i
|
8
9
|
@converter = converter
|
10
|
+
@condition = condition || ->(*) { true }
|
9
11
|
end
|
10
12
|
|
11
|
-
def scan(string)
|
12
|
-
if m = @regexp.match(string)
|
13
|
+
def scan(string, last_token=nil)
|
14
|
+
if (m = @regexp.match(string)) && @condition.call(last_token)
|
13
15
|
value = raw = m.to_s
|
14
16
|
value = @converter.call(raw) if @converter
|
15
17
|
|
@@ -28,6 +30,7 @@ module Dentaku
|
|
28
30
|
numeric,
|
29
31
|
double_quoted_string,
|
30
32
|
single_quoted_string,
|
33
|
+
negate,
|
31
34
|
operator,
|
32
35
|
grouping,
|
33
36
|
comparator,
|
@@ -53,6 +56,18 @@ module Dentaku
|
|
53
56
|
new(:string, "'[^']*'", lambda { |raw| raw.gsub(/^'|'$/, '') })
|
54
57
|
end
|
55
58
|
|
59
|
+
def negate
|
60
|
+
new(:operator, '-', lambda { |raw| :negate }, lambda { |last_token|
|
61
|
+
last_token.nil? ||
|
62
|
+
last_token.is?(:operator) ||
|
63
|
+
last_token.is?(:comparator) ||
|
64
|
+
last_token.is?(:combinator) ||
|
65
|
+
last_token.value == :open ||
|
66
|
+
last_token.value == :fopen ||
|
67
|
+
last_token.value == :comma
|
68
|
+
})
|
69
|
+
end
|
70
|
+
|
56
71
|
def operator
|
57
72
|
names = { pow: '^', add: '+', subtract: '-', multiply: '*', divide: '/', mod: '%' }.invert
|
58
73
|
new(:operator, '\^|\+|-|\*|\/|%', lambda { |raw| names[raw] })
|
data/lib/dentaku/tokenizer.rb
CHANGED
@@ -10,7 +10,7 @@ module Dentaku
|
|
10
10
|
def tokenize(string)
|
11
11
|
@nesting = 0
|
12
12
|
@tokens = []
|
13
|
-
input = string.to_s.dup
|
13
|
+
input = strip_comments(string.to_s.dup)
|
14
14
|
|
15
15
|
until input.empty?
|
16
16
|
raise "parse error at: '#{ input }'" unless TokenScanner.scanners.any? do |scanner|
|
@@ -24,8 +24,12 @@ module Dentaku
|
|
24
24
|
@tokens
|
25
25
|
end
|
26
26
|
|
27
|
+
def last_token
|
28
|
+
@tokens.last
|
29
|
+
end
|
30
|
+
|
27
31
|
def scan(string, scanner)
|
28
|
-
if tokens = scanner.scan(string)
|
32
|
+
if tokens = scanner.scan(string, last_token)
|
29
33
|
tokens.each do |token|
|
30
34
|
raise "unexpected zero-width match (:#{ token.category }) at '#{ string }'" if token.length == 0
|
31
35
|
|
@@ -42,5 +46,9 @@ module Dentaku
|
|
42
46
|
[false, string]
|
43
47
|
end
|
44
48
|
end
|
49
|
+
|
50
|
+
def strip_comments(input)
|
51
|
+
input.gsub(/\/\*[^*]*\*+(?:[^*\/][^*]*\*+)*\//, '')
|
52
|
+
end
|
45
53
|
end
|
46
54
|
end
|
data/lib/dentaku/version.rb
CHANGED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/function'
|
3
|
+
|
4
|
+
describe Dentaku::AST::Function do
|
5
|
+
it 'maintains a function registry' do
|
6
|
+
expect(described_class).to respond_to(:get)
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'raises an exception when trying to access an undefined function' do
|
10
|
+
expect { described_class.get("flarble") }.to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'registers a custom function' do
|
14
|
+
described_class.register("flarble", -> { "flarble" })
|
15
|
+
expect { described_class.get("flarble") }.not_to raise_error
|
16
|
+
function = described_class.get("flarble").new
|
17
|
+
expect(function.value).to eq "flarble"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/ast/node'
|
3
|
+
require 'dentaku/tokenizer'
|
4
|
+
require 'dentaku/parser'
|
5
|
+
|
6
|
+
describe Dentaku::AST::Node do
|
7
|
+
it 'returns list of dependencies' do
|
8
|
+
node = make_node('x + 5')
|
9
|
+
expect(node.dependencies).to eq ['x']
|
10
|
+
|
11
|
+
node = make_node('5 < x')
|
12
|
+
expect(node.dependencies).to eq ['x']
|
13
|
+
|
14
|
+
node = make_node('5 < 7')
|
15
|
+
expect(node.dependencies).to eq []
|
16
|
+
|
17
|
+
node = make_node('(y * 7)')
|
18
|
+
expect(node.dependencies).to eq ['y']
|
19
|
+
|
20
|
+
node = make_node('if(x > 5, y, z)')
|
21
|
+
expect(node.dependencies).to eq ['x', 'y', 'z']
|
22
|
+
|
23
|
+
node = make_node('if(x > 5, y, z)')
|
24
|
+
expect(node.dependencies('x' => 7)).to eq ['y', 'z']
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'returns unique list of dependencies' do
|
28
|
+
node = make_node('x + x')
|
29
|
+
expect(node.dependencies).to eq ['x']
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def make_node(expression)
|
35
|
+
Dentaku::Parser.new(Dentaku::Tokenizer.new.tokenize(expression)).parse
|
36
|
+
end
|
37
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'spec_helper'
|
1
2
|
require 'dentaku/bulk_expression_solver'
|
2
3
|
|
3
4
|
RSpec.describe Dentaku::BulkExpressionSolver do
|
@@ -17,34 +18,40 @@ RSpec.describe Dentaku::BulkExpressionSolver do
|
|
17
18
|
it "lets you know if a variable is unbound" do
|
18
19
|
expressions = {more_apples: "apples + 1"}
|
19
20
|
expect {
|
20
|
-
described_class.new(expressions, {}).solve!
|
21
|
+
described_class.new(expressions, {}).solve!
|
21
22
|
}.to raise_error(Dentaku::UnboundVariableError)
|
22
23
|
end
|
23
24
|
|
24
25
|
it "lets you know if the result is a div/0 error" do
|
25
26
|
expressions = {more_apples: "1/0"}
|
26
27
|
expect {
|
27
|
-
described_class.new(expressions, {}).solve!
|
28
|
+
described_class.new(expressions, {}).solve!
|
28
29
|
}.to raise_error(ZeroDivisionError)
|
29
30
|
end
|
31
|
+
|
32
|
+
it "does not require keys to be parseable" do
|
33
|
+
expressions = { "the value of x, incremented" => "x + 1" }
|
34
|
+
solver = described_class.new(expressions, "x" => 3)
|
35
|
+
expect(solver.solve!).to eq({ "the value of x, incremented" => 4 })
|
36
|
+
end
|
30
37
|
end
|
31
38
|
|
32
39
|
describe "#solve" do
|
33
40
|
it "returns :undefined when variables are unbound" do
|
34
41
|
expressions = {more_apples: "apples + 1"}
|
35
|
-
expect(described_class.new(expressions, {}).solve
|
42
|
+
expect(described_class.new(expressions, {}).solve)
|
36
43
|
.to eq(more_apples: :undefined)
|
37
44
|
end
|
38
45
|
|
39
46
|
it "allows passing in a custom value to an error handler when a variable is unbound" do
|
40
47
|
expressions = {more_apples: "apples + 1"}
|
41
|
-
expect(described_class.new(expressions, {}).solve
|
48
|
+
expect(described_class.new(expressions, {}).solve { :foo })
|
42
49
|
.to eq(more_apples: :foo)
|
43
50
|
end
|
44
51
|
|
45
52
|
it "allows passing in a custom value to an error handler when there is a div/0 error" do
|
46
53
|
expressions = {more_apples: "1/0"}
|
47
|
-
expect(described_class.new(expressions, {}).solve
|
54
|
+
expect(described_class.new(expressions, {}).solve { :foo })
|
48
55
|
.to eq(more_apples: :foo)
|
49
56
|
end
|
50
57
|
end
|
data/spec/calculator_spec.rb
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
require 'spec_helper'
|
1
2
|
require 'dentaku/calculator'
|
2
3
|
|
3
4
|
describe Dentaku::Calculator do
|
@@ -19,7 +20,7 @@ describe Dentaku::Calculator do
|
|
19
20
|
expect(calculator.evaluate('(2 + 3) - 1')).to eq(4)
|
20
21
|
expect(calculator.evaluate('(-2 + 3) - 1')).to eq(0)
|
21
22
|
expect(calculator.evaluate('(-2 - 3) - 1')).to eq(-6)
|
22
|
-
expect(calculator.evaluate('1 + -2 ^ 2')).to eq(-3)
|
23
|
+
expect(calculator.evaluate('1 + -(2 ^ 2)')).to eq(-3)
|
23
24
|
expect(calculator.evaluate('3 + -num', num: 2)).to eq(1)
|
24
25
|
expect(calculator.evaluate('-num + 3', num: 2)).to eq(1)
|
25
26
|
expect(calculator.evaluate('10 ^ 2')).to eq(100)
|
@@ -198,4 +199,16 @@ describe Dentaku::Calculator do
|
|
198
199
|
expect(calculator.evaluate('NOT(some_boolean) AND -1 > 3', some_boolean: true)).to be_falsey
|
199
200
|
end
|
200
201
|
end
|
202
|
+
|
203
|
+
describe 'math functions' do
|
204
|
+
Math.methods(false).each do |method|
|
205
|
+
it method do
|
206
|
+
if Math.method(method).arity == 2
|
207
|
+
expect(calculator.evaluate("#{method}(1,2)")).to eq Math.send(method, 1, 2)
|
208
|
+
else
|
209
|
+
expect(calculator.evaluate("#{method}(1)")).to eq Math.send(method, 1)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
201
214
|
end
|
@@ -8,28 +8,12 @@ describe Dentaku::Calculator do
|
|
8
8
|
let(:with_external_funcs) do
|
9
9
|
c = described_class.new
|
10
10
|
|
11
|
-
|
12
|
-
c.add_function(now)
|
11
|
+
c.add_function(:now, -> { Time.now.to_s })
|
13
12
|
|
14
13
|
fns = [
|
15
|
-
{
|
16
|
-
|
17
|
-
|
18
|
-
signature: [ :numeric, :numeric ],
|
19
|
-
body: ->(mantissa, exponent) { mantissa ** exponent }
|
20
|
-
},
|
21
|
-
{
|
22
|
-
name: :max,
|
23
|
-
type: :numeric,
|
24
|
-
signature: [ :arguments ],
|
25
|
-
body: ->(*args) { args.max }
|
26
|
-
},
|
27
|
-
{
|
28
|
-
name: :min,
|
29
|
-
type: :numeric,
|
30
|
-
signature: [ :arguments ],
|
31
|
-
body: ->(*args) { args.min }
|
32
|
-
}
|
14
|
+
[:pow, ->(mantissa, exponent) { mantissa ** exponent }],
|
15
|
+
[:biggest, ->(*args) { args.max }],
|
16
|
+
[:smallest, ->(*args) { args.min }],
|
33
17
|
]
|
34
18
|
|
35
19
|
c.add_functions(fns)
|
@@ -41,18 +25,18 @@ describe Dentaku::Calculator do
|
|
41
25
|
expect(now).not_to be_empty
|
42
26
|
end
|
43
27
|
|
44
|
-
it 'includes
|
45
|
-
expect(with_external_funcs.evaluate('
|
46
|
-
expect(with_external_funcs.evaluate('
|
47
|
-
expect(with_external_funcs.evaluate('
|
28
|
+
it 'includes POW' do
|
29
|
+
expect(with_external_funcs.evaluate('POW(2,3)')).to eq(8)
|
30
|
+
expect(with_external_funcs.evaluate('POW(3,2)')).to eq(9)
|
31
|
+
expect(with_external_funcs.evaluate('POW(mantissa,exponent)', mantissa: 2, exponent: 4)).to eq(16)
|
48
32
|
end
|
49
33
|
|
50
|
-
it 'includes
|
51
|
-
expect(with_external_funcs.evaluate('
|
34
|
+
it 'includes BIGGEST' do
|
35
|
+
expect(with_external_funcs.evaluate('BIGGEST(8,6,7,5,3,0,9)')).to eq(9)
|
52
36
|
end
|
53
37
|
|
54
|
-
it 'includes
|
55
|
-
expect(with_external_funcs.evaluate('
|
38
|
+
it 'includes SMALLEST' do
|
39
|
+
expect(with_external_funcs.evaluate('SMALLEST(8,6,7,5,3,0,9)')).to eq(0)
|
56
40
|
end
|
57
41
|
end
|
58
42
|
end
|
data/spec/parser_spec.rb
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'dentaku/parser'
|
3
|
+
|
4
|
+
describe Dentaku::Parser do
|
5
|
+
it 'is constructed from a token' do
|
6
|
+
token = Dentaku::Token.new(:numeric, 5)
|
7
|
+
node = described_class.new([token]).parse
|
8
|
+
expect(node.value).to eq 5
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'performs simple addition' do
|
12
|
+
five = Dentaku::Token.new(:numeric, 5)
|
13
|
+
plus = Dentaku::Token.new(:operator, :add)
|
14
|
+
four = Dentaku::Token.new(:numeric, 4)
|
15
|
+
|
16
|
+
node = described_class.new([five, plus, four]).parse
|
17
|
+
expect(node.value).to eq 9
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'compares two numbers' do
|
21
|
+
five = Dentaku::Token.new(:numeric, 5)
|
22
|
+
lt = Dentaku::Token.new(:comparator, :lt)
|
23
|
+
four = Dentaku::Token.new(:numeric, 4)
|
24
|
+
|
25
|
+
node = described_class.new([five, lt, four]).parse
|
26
|
+
expect(node.value).to eq false
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'performs multiple operations in one stream' do
|
30
|
+
five = Dentaku::Token.new(:numeric, 5)
|
31
|
+
plus = Dentaku::Token.new(:operator, :add)
|
32
|
+
four = Dentaku::Token.new(:numeric, 4)
|
33
|
+
times = Dentaku::Token.new(:operator, :multiply)
|
34
|
+
three = Dentaku::Token.new(:numeric, 3)
|
35
|
+
|
36
|
+
node = described_class.new([five, plus, four, times, three]).parse
|
37
|
+
expect(node.value).to eq 17
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'respects order of operations' do
|
41
|
+
five = Dentaku::Token.new(:numeric, 5)
|
42
|
+
times = Dentaku::Token.new(:operator, :multiply)
|
43
|
+
four = Dentaku::Token.new(:numeric, 4)
|
44
|
+
plus = Dentaku::Token.new(:operator, :add)
|
45
|
+
three = Dentaku::Token.new(:numeric, 3)
|
46
|
+
|
47
|
+
node = described_class.new([five, times, four, plus, three]).parse
|
48
|
+
expect(node.value).to eq 23
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'respects grouping by parenthesis' do
|
52
|
+
lpar = Dentaku::Token.new(:grouping, :open)
|
53
|
+
five = Dentaku::Token.new(:numeric, 5)
|
54
|
+
plus = Dentaku::Token.new(:operator, :add)
|
55
|
+
four = Dentaku::Token.new(:numeric, 4)
|
56
|
+
rpar = Dentaku::Token.new(:grouping, :close)
|
57
|
+
times = Dentaku::Token.new(:operator, :multiply)
|
58
|
+
three = Dentaku::Token.new(:numeric, 3)
|
59
|
+
|
60
|
+
node = described_class.new([lpar, five, plus, four, rpar, times, three]).parse
|
61
|
+
expect(node.value).to eq 27
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'evaluates functions' do
|
65
|
+
fn = Dentaku::Token.new(:function, :if)
|
66
|
+
fopen = Dentaku::Token.new(:grouping, :fopen)
|
67
|
+
five = Dentaku::Token.new(:numeric, 5)
|
68
|
+
lt = Dentaku::Token.new(:comparator, :lt)
|
69
|
+
four = Dentaku::Token.new(:numeric, 4)
|
70
|
+
comma = Dentaku::Token.new(:grouping, :comma)
|
71
|
+
three = Dentaku::Token.new(:numeric, 3)
|
72
|
+
two = Dentaku::Token.new(:numeric, 2)
|
73
|
+
rpar = Dentaku::Token.new(:grouping, :close)
|
74
|
+
|
75
|
+
node = described_class.new([fn, fopen, five, lt, four, comma, three, comma, two, rpar]).parse
|
76
|
+
expect(node.value).to eq 2
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'represents formulas with variables' do
|
80
|
+
five = Dentaku::Token.new(:numeric, 5)
|
81
|
+
times = Dentaku::Token.new(:operator, :multiply)
|
82
|
+
x = Dentaku::Token.new(:identifier, :x)
|
83
|
+
|
84
|
+
node = described_class.new([five, times, x]).parse
|
85
|
+
expect { node.value }.to raise_error
|
86
|
+
expect(node.value(x: 3)).to eq 15
|
87
|
+
end
|
88
|
+
end
|
data/spec/spec_helper.rb
CHANGED
data/spec/token_scanner_spec.rb
CHANGED
@@ -2,7 +2,9 @@ require 'dentaku/token_scanner'
|
|
2
2
|
|
3
3
|
describe Dentaku::TokenScanner do
|
4
4
|
let(:whitespace) { described_class.new(:whitespace, '\s') }
|
5
|
-
let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
|
5
|
+
let(:numeric) { described_class.new(:numeric, '(\d+(\.\d+)?|\.\d+)',
|
6
|
+
lambda{|raw| raw =~ /\./ ? BigDecimal.new(raw) : raw.to_i })
|
7
|
+
}
|
6
8
|
|
7
9
|
it 'returns a token for a matching string' do
|
8
10
|
token = whitespace.scan(' ').first
|
@@ -21,7 +23,6 @@ describe Dentaku::TokenScanner do
|
|
21
23
|
end
|
22
24
|
|
23
25
|
it 'returns a list of all configured scanners' do
|
24
|
-
expect(described_class.scanners.length).to eq
|
26
|
+
expect(described_class.scanners.length).to eq 11
|
25
27
|
end
|
26
28
|
end
|
27
|
-
|