ghostwheel 0.0.1
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.
- data/Rakefile +46 -0
- data/lib/ghost_wheel.rb +28 -0
- data/lib/ghost_wheel/build_parser.rb +11 -0
- data/lib/ghost_wheel/errors.rb +9 -0
- data/lib/ghost_wheel/expression.rb +22 -0
- data/lib/ghost_wheel/expression/alternation.rb +25 -0
- data/lib/ghost_wheel/expression/empty.rb +15 -0
- data/lib/ghost_wheel/expression/end_of_file.rb +19 -0
- data/lib/ghost_wheel/expression/literal.rb +31 -0
- data/lib/ghost_wheel/expression/look_ahead.rb +33 -0
- data/lib/ghost_wheel/expression/optional.rb +26 -0
- data/lib/ghost_wheel/expression/query.rb +32 -0
- data/lib/ghost_wheel/expression/repetition.rb +44 -0
- data/lib/ghost_wheel/expression/rule.rb +24 -0
- data/lib/ghost_wheel/expression/sequence.rb +30 -0
- data/lib/ghost_wheel/expression/transform.rb +30 -0
- data/lib/ghost_wheel/parse_results.rb +9 -0
- data/lib/ghost_wheel/parser.rb +71 -0
- data/lib/ghost_wheel/parser_builder/ghost_wheel.rb +100 -0
- data/lib/ghost_wheel/parser_builder/ruby.rb +175 -0
- data/lib/ghost_wheel/scanner.rb +42 -0
- data/setup.rb +1360 -0
- data/test/dsl/tc_build_parser.rb +29 -0
- data/test/dsl/tc_ghost_wheel_dsl.rb +143 -0
- data/test/dsl/tc_ruby_dsl.rb +227 -0
- data/test/example/tc_json_core.rb +95 -0
- data/test/example/tc_json_ghost_wheel.rb +48 -0
- data/test/example/tc_json_ruby.rb +81 -0
- data/test/helpers/ghost_wheel_namespace.rb +24 -0
- data/test/helpers/json_tests.rb +63 -0
- data/test/helpers/parse_helpers.rb +83 -0
- data/test/parser/tc_alternation_expression.rb +27 -0
- data/test/parser/tc_empty_expression.rb +15 -0
- data/test/parser/tc_end_of_file_expression.rb +27 -0
- data/test/parser/tc_literal_expression.rb +55 -0
- data/test/parser/tc_look_ahead_expression.rb +41 -0
- data/test/parser/tc_memoization.rb +31 -0
- data/test/parser/tc_optional_expression.rb +31 -0
- data/test/parser/tc_parser.rb +78 -0
- data/test/parser/tc_query_expression.rb +40 -0
- data/test/parser/tc_repetition_expression.rb +76 -0
- data/test/parser/tc_rule_expression.rb +32 -0
- data/test/parser/tc_scanning.rb +192 -0
- data/test/parser/tc_sequence_expression.rb +42 -0
- data/test/parser/tc_transform_expression.rb +77 -0
- data/test/ts_all.rb +7 -0
- data/test/ts_dsl.rb +8 -0
- data/test/ts_example.rb +7 -0
- data/test/ts_parser.rb +19 -0
- metadata +94 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
|
7
|
+
|
8
|
+
class TestMemoization < Test::Unit::TestCase
|
9
|
+
include GhostWheelNamespace
|
10
|
+
include ParseHelpers
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@source = Scanner.new("42")
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_memoization
|
17
|
+
calls = 0
|
18
|
+
parser = Transform.new(Literal.new(/\d+/)) { |ast| calls += 1; ast }
|
19
|
+
cache = Hash.new
|
20
|
+
|
21
|
+
assert_equal(0, cache.size)
|
22
|
+
|
23
|
+
parsed = parser.parse(@source, cache)
|
24
|
+
@source.pos = 0
|
25
|
+
cached = parser.parse(@source, cache)
|
26
|
+
|
27
|
+
assert_not_equal(0, cache.size)
|
28
|
+
assert_same(parsed, cached)
|
29
|
+
assert_equal(1, calls)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
|
7
|
+
|
8
|
+
class TestOptionalExpression < Test::Unit::TestCase
|
9
|
+
include GhostWheelNamespace
|
10
|
+
include ParseHelpers
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@source = Scanner.new("data")
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_match
|
17
|
+
assert_parses("data", parse_optional("data"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_failed_match
|
21
|
+
assert_parses_empty(parse_optional("missing"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_equality
|
25
|
+
assert_not_equal( Optional.new(Literal.new("one")),
|
26
|
+
Optional.new(Literal.new("two")) )
|
27
|
+
|
28
|
+
assert_equal( Optional.new(Literal.new("one")),
|
29
|
+
Optional.new(Literal.new("one")) )
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
|
7
|
+
class TestScanning < Test::Unit::TestCase
|
8
|
+
include GhostWheelNamespace
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@parser = Parser.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_read_and_write_rules
|
15
|
+
assert_nil(@parser[:empty])
|
16
|
+
|
17
|
+
@parser[:empty] = Empty.instance
|
18
|
+
@parser[:space] = Literal.new(/\s+/)
|
19
|
+
assert_equal(Empty.instance, @parser[:empty])
|
20
|
+
assert_equal(Literal.new(/\s+/), @parser[:space])
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_no_parsers_for_rules
|
24
|
+
@parser[:empty] = Empty.instance
|
25
|
+
assert( !@parser.respond_to?(:parse_empty),
|
26
|
+
"A parse_empty() method was defined, but not expected." )
|
27
|
+
assert_raise(NoMethodError) { @parser.parse_empty("not_used") }
|
28
|
+
assert( !@parser.respond_to?(:parse),
|
29
|
+
"A parse() method was defined, but not expected." )
|
30
|
+
assert_raise(NoMethodError) { @parser.parse("not_used") }
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_named_parsers
|
34
|
+
add_parsers
|
35
|
+
assert_respond_to(@parser, :parse_empty)
|
36
|
+
assert_raise(GhostWheel::EmptyParseError) do
|
37
|
+
@parser.parse_empty("not_used")
|
38
|
+
end
|
39
|
+
assert_respond_to(@parser, :parse_space)
|
40
|
+
assert_equal(" ", @parser.parse_space(" word"))
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_parse
|
44
|
+
# parse() works with just one rule
|
45
|
+
add_empty_parser
|
46
|
+
assert_respond_to(@parser, :parse)
|
47
|
+
assert_raise(GhostWheel::EmptyParseError) { @parser.parse("not_used") }
|
48
|
+
|
49
|
+
# but not with more than one
|
50
|
+
add_space_parser
|
51
|
+
assert( !@parser.respond_to?(:parse),
|
52
|
+
"A parse() method was defined, but not expected." )
|
53
|
+
assert_raise(NoMethodError) { @parser.parse("not_used") }
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_pass_extra_arg
|
57
|
+
add_empty_parser
|
58
|
+
assert_equal(42, @parser.parse_empty("not_used", 42))
|
59
|
+
assert_equal(42, @parser.parse("not_used", 42))
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def add_empty_parser
|
65
|
+
@parser[:empty] = Empty.instance
|
66
|
+
@parser.build_parser(:empty)
|
67
|
+
end
|
68
|
+
|
69
|
+
def add_space_parser
|
70
|
+
@parser[:space] = Literal.new(/\s+/)
|
71
|
+
@parser.build_parser(:space)
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_parsers
|
75
|
+
add_empty_parser
|
76
|
+
add_space_parser
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
|
7
|
+
|
8
|
+
class TestQueryExpression < Test::Unit::TestCase
|
9
|
+
include GhostWheelNamespace
|
10
|
+
include ParseHelpers
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@parser = Query.new(Literal.new(/\d+/)) { |i| i.to_i % 2 == 0 }
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_query_match
|
17
|
+
assert_parses("12", parse("12"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_failed_query_match
|
21
|
+
assert_doesnt_parse(parse("13"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_yields_empty
|
25
|
+
Query.new(Empty.instance) do |empty|
|
26
|
+
assert_instance_of(EmptyParseResult, empty)
|
27
|
+
end.parse(Scanner.new("not_used"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def test_equality
|
31
|
+
assert_not_equal( Query.new(Literal.new("one")) { },
|
32
|
+
Query.new(Literal.new("two")) { } )
|
33
|
+
assert_not_equal( Query.new(Literal.new("one")) { true },
|
34
|
+
Query.new(Literal.new("one")) { } )
|
35
|
+
|
36
|
+
block = lambda { true } # must be *exact* same block
|
37
|
+
assert_equal( Query.new(Literal.new("one"), &block),
|
38
|
+
Query.new(Literal.new("one"), &block) )
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
|
7
|
+
|
8
|
+
class TestRepetitionExpression < Test::Unit::TestCase
|
9
|
+
include GhostWheelNamespace
|
10
|
+
include ParseHelpers
|
11
|
+
|
12
|
+
def setup
|
13
|
+
@source = Scanner.new("X" * 10 + "YZ")
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_zero_or_more_match
|
17
|
+
assert_parses(["X"] * 10, parse_repetition("X"))
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_failed_zero_or_more_match
|
21
|
+
assert_parses_empty(parse_repetition("Z"))
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_one_or_more_match
|
25
|
+
assert_parses(["X"] * 10, parse_repetition("X", 1))
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_failed_one_or_more_match
|
29
|
+
assert_doesnt_parse(parse_repetition("Z", 1))
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_with_minimum_match
|
33
|
+
assert_parses(["X"] * 10, parse_repetition("X", 5))
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_failed_with_minimum_match
|
37
|
+
assert_doesnt_parse(parse_repetition("Z", 15))
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_with_maximum_match
|
41
|
+
assert_parses(["X"] * 3, parse_repetition("X", 0, 3))
|
42
|
+
assert_parses(["X"] * 7, parse_repetition("X", 0, 20))
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_failed_with_maximum_match
|
46
|
+
assert_parses_empty(parse_repetition("X", 0, 0))
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_with_minimum_and_maximum_match
|
50
|
+
assert_parses(["X"] * 3, parse_repetition("X", 1, 3))
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_failed_with_minimum_and_maximum_match
|
54
|
+
assert_doesnt_parse(parse_repetition("X", 20, 40))
|
55
|
+
end
|
56
|
+
|
57
|
+
def test_exact_count_match
|
58
|
+
assert_parses(["X"] * 3, parse_repetition("X", 3, 3))
|
59
|
+
end
|
60
|
+
|
61
|
+
def test_failed_exact_count_match
|
62
|
+
assert_doesnt_parse(parse_repetition("X", 11, 11))
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_equality
|
66
|
+
assert_not_equal( Repetition.new(Literal.new("one")),
|
67
|
+
Repetition.new(Literal.new("two")) )
|
68
|
+
assert_not_equal( Repetition.new(Literal.new("one"), 1),
|
69
|
+
Repetition.new(Literal.new("one")) )
|
70
|
+
assert_not_equal( Repetition.new(Literal.new("one"), 1, 5),
|
71
|
+
Repetition.new(Literal.new("one"), 1) )
|
72
|
+
|
73
|
+
assert_equal( Repetition.new(Literal.new("one")),
|
74
|
+
Repetition.new(Literal.new("one")) )
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers parse_helpers])
|
7
|
+
|
8
|
+
class TestRuleExpression < Test::Unit::TestCase
|
9
|
+
include GhostWheelNamespace
|
10
|
+
include ParseHelpers
|
11
|
+
|
12
|
+
def test_simple_wrapping
|
13
|
+
assert_parses_empty(Rule.new(Empty.instance).parse(Scanner.new("not_used")))
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_forward_declaration
|
17
|
+
rule = Rule.new
|
18
|
+
assert_raise(EmptyRuleError) { rule.parse(Scanner.new("not_used")) }
|
19
|
+
rule.expression = Empty.instance
|
20
|
+
assert_parses_empty(rule.parse(Scanner.new("not_used")))
|
21
|
+
end
|
22
|
+
|
23
|
+
def test_equality
|
24
|
+
assert_not_equal(Rule.new(Literal.new("one")), Rule.new)
|
25
|
+
assert_not_equal( Rule.new(Literal.new("one")),
|
26
|
+
Rule.new(Literal.new("two")) )
|
27
|
+
|
28
|
+
assert_equal(Rule.new, Rule.new)
|
29
|
+
assert_equal( Rule.new(Literal.new("one")),
|
30
|
+
Rule.new(Literal.new("one")) )
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,192 @@
|
|
1
|
+
#!/usr/bin/env ruby -w
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
|
5
|
+
require File.join(File.dirname(__FILE__), %w[.. helpers ghost_wheel_namespace])
|
6
|
+
|
7
|
+
class TestScanning < Test::Unit::TestCase
|
8
|
+
include GhostWheelNamespace
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@scanner = Scanner.new("three little words")
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_scanner_advances_on_match
|
15
|
+
assert_equal("three little words", @scanner.rest)
|
16
|
+
assert_scan_to("little words", /\S+\s+/)
|
17
|
+
assert_scan_to("words", /\S+\s+/)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_failed_match_does_not_advance
|
21
|
+
assert_equal("three little words", @scanner.rest)
|
22
|
+
assert_doesnt_scan(/fails/)
|
23
|
+
assert_scan_to("little words", /\S+\s+/)
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_successful_transaction
|
27
|
+
assert_equal("three little words", @scanner.rest)
|
28
|
+
|
29
|
+
@scanner.transaction do
|
30
|
+
assert_doesnt_scan(/fails/)
|
31
|
+
assert_scan_to("little words", /\S+\s+/)
|
32
|
+
end
|
33
|
+
|
34
|
+
assert_equal("little words", @scanner.rest)
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_aborted_transaction
|
38
|
+
assert_equal("three little words", @scanner.rest)
|
39
|
+
|
40
|
+
assert_nothing_raised(Exception) do
|
41
|
+
@scanner.transaction do
|
42
|
+
assert_scan_to("little words", /\S+\s+/)
|
43
|
+
|
44
|
+
@scanner.abort
|
45
|
+
|
46
|
+
flunk("Should never reach here.")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
assert_equal("three little words", @scanner.rest)
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_nested_transactions
|
54
|
+
assert_equal("three little words", @scanner.rest)
|
55
|
+
|
56
|
+
# nested aborts...
|
57
|
+
@scanner.transaction do
|
58
|
+
assert_scan_to("little words", /\S+\s+/)
|
59
|
+
|
60
|
+
@scanner.transaction do
|
61
|
+
assert_scan_to("words", /\S+\s+/)
|
62
|
+
|
63
|
+
@scanner.abort
|
64
|
+
|
65
|
+
flunk("Should never reach here.")
|
66
|
+
end
|
67
|
+
|
68
|
+
assert_equal("little words", @scanner.rest)
|
69
|
+
|
70
|
+
@scanner.abort
|
71
|
+
|
72
|
+
flunk("Should never reach here.")
|
73
|
+
end
|
74
|
+
|
75
|
+
assert_equal("three little words", @scanner.rest)
|
76
|
+
|
77
|
+
# nested success...
|
78
|
+
@scanner.transaction do
|
79
|
+
assert_scan_to("little words", /\S+\s+/)
|
80
|
+
|
81
|
+
@scanner.transaction do
|
82
|
+
assert_scan_to("words", /\S+\s+/)
|
83
|
+
end
|
84
|
+
|
85
|
+
assert_equal("words", @scanner.rest)
|
86
|
+
end
|
87
|
+
|
88
|
+
assert_equal("words", @scanner.rest)
|
89
|
+
end
|
90
|
+
|
91
|
+
def test_successful_transactions_auto_commit_changes
|
92
|
+
assert_equal("three little words", @scanner.rest)
|
93
|
+
|
94
|
+
@scanner.transaction do
|
95
|
+
assert_scan_to("little words", /\S+\s+/)
|
96
|
+
|
97
|
+
@scanner.transaction do
|
98
|
+
assert_scan_to("words", /\S+\s+/)
|
99
|
+
end
|
100
|
+
|
101
|
+
assert_equal("words", @scanner.rest)
|
102
|
+
|
103
|
+
@scanner.abort
|
104
|
+
|
105
|
+
flunk("Should never reach here.")
|
106
|
+
end
|
107
|
+
|
108
|
+
assert_equal("three little words", @scanner.rest)
|
109
|
+
end
|
110
|
+
|
111
|
+
def test_exception_aborts_transaction
|
112
|
+
assert_equal("three little words", @scanner.rest)
|
113
|
+
|
114
|
+
@scanner.transaction do
|
115
|
+
assert_scan_to("little words", /\S+\s+/)
|
116
|
+
|
117
|
+
assert_raise(NoMethodError) do
|
118
|
+
@scanner.transaction do
|
119
|
+
assert_scan_to("words", /\S+\s+/)
|
120
|
+
|
121
|
+
Object.new.no_such_method # raise an exception
|
122
|
+
|
123
|
+
flunk("Should never reach here.")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
assert_equal("little words", @scanner.rest)
|
128
|
+
end
|
129
|
+
|
130
|
+
assert_equal("little words", @scanner.rest)
|
131
|
+
end
|
132
|
+
|
133
|
+
def test_stack_underflow_error
|
134
|
+
# abort() without opening a transaction
|
135
|
+
assert_raise(StackUnderflowError) { @scanner.abort }
|
136
|
+
end
|
137
|
+
|
138
|
+
def test_eos_detection
|
139
|
+
assert(!@scanner.eos?, "Detected eos?() with content left.")
|
140
|
+
assert_scan_to(String.new, /.*/m)
|
141
|
+
assert(@scanner.eos?, "Failed to detect eos?().")
|
142
|
+
end
|
143
|
+
|
144
|
+
def test_transaction_return_value
|
145
|
+
assert_equal("success", @scanner.transaction { "success" })
|
146
|
+
assert_equal(
|
147
|
+
"failure",
|
148
|
+
@scanner.transaction { @scanner.abort("failure"); "success" }
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
def test_pos
|
153
|
+
assert_equal("three little words", @scanner.rest)
|
154
|
+
|
155
|
+
saved_pos = nil
|
156
|
+
@scanner.transaction do
|
157
|
+
assert_scan_to("little words", /\S+\s+/)
|
158
|
+
|
159
|
+
@scanner.transaction do
|
160
|
+
assert_scan_to("words", /\S+\s+/)
|
161
|
+
|
162
|
+
saved_pos = @scanner.pos
|
163
|
+
|
164
|
+
@scanner.abort
|
165
|
+
|
166
|
+
flunk("Should never reach here.")
|
167
|
+
end
|
168
|
+
|
169
|
+
assert_equal("little words", @scanner.rest)
|
170
|
+
|
171
|
+
@scanner.abort
|
172
|
+
|
173
|
+
flunk("Should never reach here.")
|
174
|
+
end
|
175
|
+
|
176
|
+
assert_equal("three little words", @scanner.rest)
|
177
|
+
|
178
|
+
@scanner.pos = saved_pos
|
179
|
+
assert_equal("words", @scanner.rest)
|
180
|
+
end
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def assert_scan_to(rest, regexp, message = "Scan failed.")
|
185
|
+
assert_block(message) { @scanner.scan(regexp) and @scanner.rest == rest }
|
186
|
+
end
|
187
|
+
|
188
|
+
def assert_doesnt_scan(regexp, message = "Scan matched unexpectedly.")
|
189
|
+
rest = @scanner.rest
|
190
|
+
assert_block(message) { !@scanner.scan(regexp) and @scanner.rest == rest }
|
191
|
+
end
|
192
|
+
end
|