patm 2.0.1 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +2 -0
- data/.travis.yml +7 -0
- data/README.md +97 -42
- data/benchmark/benchmark.rb +2 -2
- data/benchmark/comparison.rb +209 -0
- data/lib/patm.rb +98 -35
- data/patm.gemspec +4 -1
- data/spec/patm_spec.rb +97 -59
- metadata +46 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7dfd622177fbff6949aeba6e0ea9a37c503ca13e
|
4
|
+
data.tar.gz: 49e7f41b2e40d4fa9724eb607340b6196a41ef0c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7a0dd73809aadf15e43dab4c93cd857f38bf3bdb0666525f4791f117e415d0123df8dd90d0f66880362669efbaad2bfd3c0ecf82a17c2929c620711e6b773655
|
7
|
+
data.tar.gz: 379c2300a12acd9662f5abffa284dc6ffc1854f027d5ca9a8e92e5bafb9a497581054a39998334c965bb21cd4e872035ff7e88471ee00983e4468f47421cc87b
|
data/.gitignore
CHANGED
data/.travis.yml
ADDED
data/README.md
CHANGED
@@ -1,32 +1,15 @@
|
|
1
1
|
# PATM: PATtern Matcher for Ruby
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
```ruby
|
6
|
-
require 'patm'
|
7
|
-
```
|
3
|
+
[![Build Status](https://travis-ci.org/todesking/patm.svg?branch=master)](https://travis-ci.org/todesking/patm)
|
8
4
|
|
9
|
-
|
10
|
-
# With case(simple but slow)
|
11
|
-
def match(obj)
|
12
|
-
p = Patm
|
13
|
-
_xs = Patm._xs
|
14
|
-
case obj
|
15
|
-
when m = Patm.match([:x, p._1, p._2])
|
16
|
-
[m._2, m._1]
|
17
|
-
when m = Patm.match([1, _xs&p._1])
|
18
|
-
m._1
|
19
|
-
end
|
20
|
-
end
|
5
|
+
PATM is extremely faster pattern match library.
|
21
6
|
|
22
|
-
|
23
|
-
# => [2, 3]
|
7
|
+
Features: Match value/classes, Capture, Array/Hash decomposition.
|
24
8
|
|
25
|
-
|
26
|
-
# => [:z, :y]
|
9
|
+
## Usage
|
27
10
|
|
28
|
-
|
29
|
-
|
11
|
+
```ruby
|
12
|
+
require 'patm'
|
30
13
|
```
|
31
14
|
|
32
15
|
```ruby
|
@@ -46,6 +29,9 @@ class A
|
|
46
29
|
_self.match1(m._1)
|
47
30
|
end
|
48
31
|
# ...
|
32
|
+
r.else do
|
33
|
+
nil
|
34
|
+
end
|
49
35
|
end
|
50
36
|
end
|
51
37
|
|
@@ -53,6 +39,29 @@ A.new.match1([:x, 1, 2])
|
|
53
39
|
# => [1, 2]
|
54
40
|
```
|
55
41
|
|
42
|
+
```ruby
|
43
|
+
# With case(simple but slow)
|
44
|
+
def match(obj)
|
45
|
+
p = Patm
|
46
|
+
_xs = Patm._xs
|
47
|
+
case obj
|
48
|
+
when m = Patm.match([:x, p._1, p._2])
|
49
|
+
[m._2, m._1]
|
50
|
+
when m = Patm.match([1, _xs&p._1])
|
51
|
+
m._1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
match([1, 2, 3])
|
56
|
+
# => [2, 3]
|
57
|
+
|
58
|
+
match([:x, :y, :z])
|
59
|
+
# => [:z, :y]
|
60
|
+
|
61
|
+
match([])
|
62
|
+
# => nil
|
63
|
+
```
|
64
|
+
|
56
65
|
```ruby
|
57
66
|
# With pre-built Rule
|
58
67
|
rule = Patm::Rule.new do|r|
|
@@ -76,29 +85,31 @@ rule.apply([])
|
|
76
85
|
# => nil
|
77
86
|
```
|
78
87
|
|
88
|
+
## DSL
|
89
|
+
|
79
90
|
```ruby
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
91
|
+
class PatternMatcher
|
92
|
+
extend Patm::DSL
|
93
|
+
|
94
|
+
define_matcher(:match) do|r| # r is instance of Patm::Rule
|
95
|
+
# r.on( PATTERN ) {|match, _self|
|
96
|
+
# First argument is instance of Patm::Match. Use it to access captured value.
|
97
|
+
# ex. m._1, m._2, ..., m[capture_name]
|
98
|
+
#
|
99
|
+
# Second argument is instance of the class. Use it to access other methods.
|
100
|
+
# ex. _self.other_method
|
101
|
+
# }
|
102
|
+
#
|
103
|
+
# r.else {|value, _self|
|
104
|
+
# First argument is the value. Second is instance of the class.
|
105
|
+
# }
|
84
106
|
end
|
107
|
+
end
|
85
108
|
|
86
|
-
|
87
|
-
@rules.match(:match1, obj) do|r|
|
88
|
-
p = Patm
|
89
|
-
r.on [:x, p._1, p._2] do|m|
|
90
|
-
[m._1, m._2]
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
109
|
+
matcher = PatternMatcher.new
|
94
110
|
|
95
|
-
|
96
|
-
|
97
|
-
# ...
|
98
|
-
end
|
99
|
-
end
|
100
|
-
end
|
101
|
-
```
|
111
|
+
matcher.match(1)
|
112
|
+
```
|
102
113
|
|
103
114
|
## Patterns
|
104
115
|
|
@@ -138,8 +149,52 @@ Captured values are accessible through `Match#_1, _2, ...` and `Match#[capture_n
|
|
138
149
|
`Patm.or(1, 2)` matches 1 or 2.
|
139
150
|
|
140
151
|
|
152
|
+
## Performance
|
153
|
+
|
154
|
+
see [benchmark code](./benchmark/comparison.rb) for details
|
155
|
+
|
156
|
+
Machine: MacBook Air(Late 2010) C2D 1.8GHz, OS X 10.9.2
|
157
|
+
|
158
|
+
```
|
159
|
+
RUBY_VERSION: 2.1.2 p95
|
160
|
+
|
161
|
+
Benchmark: Empty(x10000)
|
162
|
+
user system total real
|
163
|
+
manual 0.010000 0.000000 0.010000 ( 0.012252)
|
164
|
+
patm 0.060000 0.000000 0.060000 ( 0.057050)
|
165
|
+
pattern_match 1.710000 0.010000 1.720000 ( 1.765749)
|
166
|
+
|
167
|
+
Benchmark: SimpleConst(x10000)
|
168
|
+
user system total real
|
169
|
+
manual 0.020000 0.000000 0.020000 ( 0.018274)
|
170
|
+
patm 0.060000 0.000000 0.060000 ( 0.075068)
|
171
|
+
patm_case 0.160000 0.000000 0.160000 ( 0.161002)
|
172
|
+
pattern_match 1.960000 0.020000 1.980000 ( 2.007936)
|
173
|
+
|
174
|
+
Benchmark: ArrayDecomposition(x10000)
|
175
|
+
user system total real
|
176
|
+
manual 0.050000 0.000000 0.050000 ( 0.047948)
|
177
|
+
patm 0.250000 0.000000 0.250000 ( 0.254039)
|
178
|
+
patm_case 1.710000 0.000000 1.710000 ( 1.765656)
|
179
|
+
pattern_match 12.890000 0.060000 12.950000 ( 13.343334)
|
180
|
+
|
181
|
+
Benchmark: VarArray(x10000)
|
182
|
+
user system total real
|
183
|
+
manual 0.050000 0.000000 0.050000 ( 0.052425)
|
184
|
+
patm 0.210000 0.000000 0.210000 ( 0.223190)
|
185
|
+
patm_case 1.440000 0.000000 1.440000 ( 1.587535)
|
186
|
+
pattern_match 10.050000 0.070000 10.120000 ( 10.898683)
|
187
|
+
```
|
188
|
+
|
189
|
+
|
141
190
|
## Changes
|
142
191
|
|
192
|
+
### 3.0.0
|
193
|
+
|
194
|
+
- If given value can't match any pattern and no `else`, `Patm::NoMatchError` raised(Instead of return nil).
|
195
|
+
- RuleCache is now obsoleted. Use DSL.
|
196
|
+
- More optimized compiler
|
197
|
+
|
143
198
|
### 2.0.1
|
144
199
|
|
145
200
|
- Bugfix: About pattern `Patm._1 & Array`.
|
data/benchmark/benchmark.rb
CHANGED
@@ -0,0 +1,209 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
|
3
|
+
def benchmark(klass, n)
|
4
|
+
puts "Benchmark: #{klass}(x#{n})"
|
5
|
+
|
6
|
+
target_methods = klass.instance_methods - Object.instance_methods - [:test_values]
|
7
|
+
validate(klass, target_methods)
|
8
|
+
|
9
|
+
Benchmark.bm('pattern-match'.size) do|b|
|
10
|
+
obj = klass.new
|
11
|
+
test_values = obj.test_values
|
12
|
+
target_methods.each do|method_name|
|
13
|
+
b.report(method_name) do
|
14
|
+
m = obj.method(method_name)
|
15
|
+
n.times { test_values.each {|val| m.call(val) } }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
puts
|
20
|
+
end
|
21
|
+
|
22
|
+
def validate(klass, target_methods)
|
23
|
+
obj = klass.new
|
24
|
+
obj.test_values.each do|val|
|
25
|
+
results = target_methods.map {|name| [name, obj.public_send(name, val)] }
|
26
|
+
unless results.each_cons(2).all?{|(_, a), (_, b)| a == b }
|
27
|
+
raise "ERROR: Result not match. val=#{val.inspect}, #{results.map{|n,r| "#{n}=#{r.inspect}"}.join(', ')}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
load File.join(File.dirname(__FILE__), '../lib/patm.rb')
|
33
|
+
require 'pattern-match'
|
34
|
+
|
35
|
+
class Empty
|
36
|
+
extend Patm::DSL
|
37
|
+
|
38
|
+
def manual(obj)
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
define_matcher :patm do|r|
|
43
|
+
r.else { nil }
|
44
|
+
end
|
45
|
+
|
46
|
+
def pattern_match(obj)
|
47
|
+
match(obj) do
|
48
|
+
with(_) { nil }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_values
|
53
|
+
[1, 2, 3]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class SimpleConst
|
58
|
+
extend Patm::DSL
|
59
|
+
|
60
|
+
def manual(obj)
|
61
|
+
if obj == 1
|
62
|
+
100
|
63
|
+
elsif obj == 2
|
64
|
+
200
|
65
|
+
else
|
66
|
+
300
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
define_matcher :patm do|r|
|
71
|
+
r.on(1) { 100 }
|
72
|
+
r.on(2) { 200 }
|
73
|
+
r.else { 300 }
|
74
|
+
end
|
75
|
+
|
76
|
+
def patm_case(obj)
|
77
|
+
case obj
|
78
|
+
when m = Patm.match(1)
|
79
|
+
100
|
80
|
+
when m = Patm.match(2)
|
81
|
+
200
|
82
|
+
else
|
83
|
+
300
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def pattern_match(obj)
|
88
|
+
match(obj) do
|
89
|
+
with(1) { 100 }
|
90
|
+
with(2) { 200 }
|
91
|
+
with(_) { 300 }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_values
|
96
|
+
[1, 2, 3]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class ArrayDecomposition
|
101
|
+
extend Patm::DSL
|
102
|
+
|
103
|
+
def manual(obj)
|
104
|
+
return 100 unless obj
|
105
|
+
return nil unless obj.is_a?(Array)
|
106
|
+
return nil if obj.size != 3
|
107
|
+
return nil unless obj[0] == 1
|
108
|
+
|
109
|
+
if obj[2] == 2
|
110
|
+
obj[1]
|
111
|
+
else
|
112
|
+
[obj[1], obj[2]]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
define_matcher :patm do|r|
|
117
|
+
_1, _2 = Patm._1, Patm._2
|
118
|
+
r.on([1, _1, 2]) {|m| m._1 }
|
119
|
+
r.on([1, _1, _2]) {|m| [m._1, m._2] }
|
120
|
+
r.on(nil) { 100 }
|
121
|
+
r.else { nil }
|
122
|
+
end
|
123
|
+
|
124
|
+
def patm_case(obj)
|
125
|
+
_1, _2 = Patm._1, Patm._2
|
126
|
+
case obj
|
127
|
+
when m = Patm.match([1, _1, 2])
|
128
|
+
m._1
|
129
|
+
when m = Patm.match([1, _1, _2])
|
130
|
+
[m._1, m._2]
|
131
|
+
when m = Patm.match(nil)
|
132
|
+
100
|
133
|
+
else
|
134
|
+
nil
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def pattern_match(obj)
|
139
|
+
match(obj) do
|
140
|
+
with(_[1, _1, 2]) { _1 }
|
141
|
+
with(_[1, _1, _2]) { [_1, _2] }
|
142
|
+
with(nil) { 100 }
|
143
|
+
with(_) { nil }
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def test_values
|
148
|
+
[
|
149
|
+
[],
|
150
|
+
[1, 9, 2],
|
151
|
+
[1, 9, 3],
|
152
|
+
[1, 9, 1],
|
153
|
+
[1],
|
154
|
+
"foo",
|
155
|
+
nil
|
156
|
+
]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
class VarArray
|
161
|
+
extend Patm::DSL
|
162
|
+
|
163
|
+
def manual(obj)
|
164
|
+
return nil unless obj.is_a?(Array) && obj.size >= 2 && obj[0] == 1 && obj[1] == 2
|
165
|
+
return obj[2..-1]
|
166
|
+
end
|
167
|
+
|
168
|
+
define_matcher :patm do|r|
|
169
|
+
r.on([1, 2, Patm._xs[1]]) {|m| m[1] }
|
170
|
+
r.else { nil }
|
171
|
+
end
|
172
|
+
|
173
|
+
def patm_case(obj)
|
174
|
+
case obj
|
175
|
+
when m = Patm.match([1, 2, Patm._xs[1]])
|
176
|
+
m._1
|
177
|
+
else
|
178
|
+
nil
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def pattern_match(obj)
|
183
|
+
match(obj) do
|
184
|
+
with(_[1, 2, *_1]) { _1 }
|
185
|
+
with(_) { nil }
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def test_values
|
190
|
+
[
|
191
|
+
nil,
|
192
|
+
100,
|
193
|
+
[],
|
194
|
+
[1, 2],
|
195
|
+
[1, 2, 3],
|
196
|
+
[1, 2, 3, 4],
|
197
|
+
[1, 10, 100],
|
198
|
+
]
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
|
203
|
+
puts "RUBY_VERSION: #{RUBY_VERSION} p#{RUBY_PATCHLEVEL}"
|
204
|
+
puts
|
205
|
+
|
206
|
+
benchmark Empty, 10000
|
207
|
+
benchmark SimpleConst, 10000
|
208
|
+
benchmark ArrayDecomposition, 10000
|
209
|
+
benchmark VarArray, 10000
|
data/lib/patm.rb
CHANGED
@@ -1,4 +1,11 @@
|
|
1
1
|
module Patm
|
2
|
+
class NoMatchError < StandardError
|
3
|
+
def initialize(value)
|
4
|
+
super("Pattern not match: value=#{value.inspect}")
|
5
|
+
@value = value
|
6
|
+
end
|
7
|
+
attr_reader :value
|
8
|
+
end
|
2
9
|
class Pattern
|
3
10
|
|
4
11
|
def self.build_from(plain)
|
@@ -30,7 +37,7 @@ module Patm
|
|
30
37
|
|
31
38
|
module Util
|
32
39
|
def self.compile_value(value, free_index)
|
33
|
-
if value.is_a?(Numeric) || value.is_a?(String) || value.is_a?(Symbol)
|
40
|
+
if value.nil? || value.is_a?(Numeric) || value.is_a?(String) || value.is_a?(Symbol)
|
34
41
|
[
|
35
42
|
value.inspect,
|
36
43
|
[],
|
@@ -90,6 +97,7 @@ module Patm
|
|
90
97
|
@desc = desc
|
91
98
|
@context = context
|
92
99
|
singleton_class = class <<self; self; end
|
100
|
+
@src_body = src
|
93
101
|
@src = <<-RUBY
|
94
102
|
def execute(_match, _obj)
|
95
103
|
_ctx = @context
|
@@ -99,10 +107,11 @@ module Patm
|
|
99
107
|
singleton_class.class_eval(@src)
|
100
108
|
end
|
101
109
|
|
110
|
+
attr_reader :src_body
|
102
111
|
attr_reader :src
|
103
112
|
|
104
113
|
def compile_internal(free_index, target_name = "_obj")
|
105
|
-
raise "
|
114
|
+
raise "already compiled"
|
106
115
|
end
|
107
116
|
def inspect; "<compiled>#{@desc}"; end
|
108
117
|
end
|
@@ -227,16 +236,22 @@ module Patm
|
|
227
236
|
|
228
237
|
elm_target_name = "#{target_name}_elm"
|
229
238
|
@head.each_with_index do|h, hi|
|
230
|
-
|
231
|
-
|
232
|
-
|
239
|
+
if h.is_a?(Obj)
|
240
|
+
s, c, i = h.compile_internal(i, "#{target_name}[#{hi}]")
|
241
|
+
srcs << "#{s}" if s
|
242
|
+
ctxs << c
|
243
|
+
else
|
244
|
+
s, c, i = h.compile_internal(i, elm_target_name)
|
245
|
+
srcs << "#{elm_target_name} = #{target_name}[#{hi}]; #{s}" if s
|
246
|
+
ctxs << c
|
247
|
+
end
|
233
248
|
end
|
234
249
|
|
235
250
|
unless @tail.empty?
|
236
|
-
srcs << "
|
251
|
+
srcs << "#{target_name}_t = #{target_name}[(-#{@tail.size})..-1]; true"
|
237
252
|
@tail.each_with_index do|t, ti|
|
238
253
|
s, c, i = t.compile_internal(i, elm_target_name)
|
239
|
-
srcs << "
|
254
|
+
srcs << "#{elm_target_name} = #{target_name}_t[#{ti}]; #{s}" if s
|
240
255
|
ctxs << c
|
241
256
|
end
|
242
257
|
end
|
@@ -244,7 +259,7 @@ module Patm
|
|
244
259
|
if @rest
|
245
260
|
tname = "#{target_name}_r"
|
246
261
|
s, c, i = @rest.compile_internal(i, tname)
|
247
|
-
srcs << "
|
262
|
+
srcs << "#{tname} = #{target_name}[#{@head.size}..-(#{@tail.size+1})];#{s}" if s
|
248
263
|
ctxs << c
|
249
264
|
end
|
250
265
|
|
@@ -434,20 +449,15 @@ module Patm
|
|
434
449
|
end
|
435
450
|
|
436
451
|
class Rule
|
437
|
-
def initialize(
|
438
|
-
|
439
|
-
# { Pattern => Proc }
|
452
|
+
def initialize(&block)
|
453
|
+
# [[Pattern, Proc]...]
|
440
454
|
@rules = []
|
441
|
-
@else =
|
455
|
+
@else = nil
|
442
456
|
block[self]
|
443
457
|
end
|
444
458
|
|
445
459
|
def on(pat, &block)
|
446
|
-
|
447
|
-
@rules << [Pattern.build_from(pat).compile, block]
|
448
|
-
else
|
449
|
-
@rules << [Pattern.build_from(pat), block]
|
450
|
-
end
|
460
|
+
@rules << [Pattern.build_from(pat), block]
|
451
461
|
end
|
452
462
|
|
453
463
|
def else(&block)
|
@@ -461,36 +471,82 @@ module Patm
|
|
461
471
|
return block.call(match, _self)
|
462
472
|
end
|
463
473
|
end
|
464
|
-
@else[obj, _self]
|
474
|
+
@else ? @else[obj, _self] : (raise NoMatchError.new(obj))
|
475
|
+
end
|
476
|
+
|
477
|
+
def inspect
|
478
|
+
"Rule{#{@rules.map(&:first).map(&:inspect).join(', ')}#{@else ? ', _' : ''}}"
|
465
479
|
end
|
466
|
-
end
|
467
480
|
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
481
|
+
def compile_call(block, *args)
|
482
|
+
"call(#{args[0...block.arity].join(', ')})"
|
483
|
+
end
|
484
|
+
|
485
|
+
def compile
|
486
|
+
i = 0
|
487
|
+
ctxs = []
|
488
|
+
srcs = []
|
489
|
+
@rules.each do|pat, block|
|
490
|
+
s, c, i = pat.compile_internal(i, '_obj')
|
491
|
+
ctxs << c
|
492
|
+
ctxs << [block]
|
493
|
+
srcs << "if (#{s || 'true'})\n_ctx[#{i}].#{compile_call(block, "::Patm::Match.new(_match)"," _self")}"
|
494
|
+
i += 1
|
495
|
+
end
|
496
|
+
src = srcs.join("\nels")
|
497
|
+
if @else
|
498
|
+
src << "\nelse\n" unless srcs.empty?
|
499
|
+
src << "_ctx[#{i}].#{compile_call(@else, "_obj"," _self")}"
|
500
|
+
ctxs << [@else]
|
501
|
+
i += 1
|
502
|
+
else
|
503
|
+
src << "\nelse\n" unless srcs.empty?
|
504
|
+
src << "raise ::Patm::NoMatchError.new(_obj)"
|
505
|
+
end
|
506
|
+
src << "\nend" unless srcs.empty?
|
507
|
+
Compiled.new(
|
508
|
+
src,
|
509
|
+
ctxs.flatten(1)
|
510
|
+
)
|
472
511
|
end
|
473
|
-
|
474
|
-
|
512
|
+
|
513
|
+
class Compiled
|
514
|
+
def initialize(src_body, context)
|
515
|
+
@src_body = src_body
|
516
|
+
@context = context
|
517
|
+
@src = <<-RUBY
|
518
|
+
def apply(_obj, _self = nil)
|
519
|
+
_ctx = @context
|
520
|
+
_match = {}
|
521
|
+
#{@src_body}
|
522
|
+
end
|
523
|
+
RUBY
|
524
|
+
|
525
|
+
singleton_class = class <<self; self; end
|
526
|
+
singleton_class.class_eval(@src)
|
527
|
+
end
|
528
|
+
|
529
|
+
attr_reader :src_body
|
530
|
+
attr_reader :context
|
475
531
|
end
|
476
532
|
end
|
477
533
|
|
478
534
|
class Match
|
479
|
-
def initialize
|
480
|
-
@
|
535
|
+
def initialize(data = {})
|
536
|
+
@data = data
|
481
537
|
end
|
482
538
|
|
483
539
|
def [](i)
|
484
|
-
@
|
540
|
+
@data[i]
|
485
541
|
end
|
486
542
|
|
487
543
|
def []=(i, val)
|
488
|
-
@
|
544
|
+
@data[i] = val
|
489
545
|
end
|
490
546
|
|
491
547
|
PREDEF_GROUP_SIZE.times.each do|i|
|
492
548
|
define_method "_#{i}" do
|
493
|
-
|
549
|
+
@data[i]
|
494
550
|
end
|
495
551
|
end
|
496
552
|
end
|
@@ -515,11 +571,18 @@ module Patm
|
|
515
571
|
|
516
572
|
module DSL
|
517
573
|
def define_matcher(name, &rule)
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
574
|
+
rule = Rule.new(&rule).compile
|
575
|
+
ctx = rule.context
|
576
|
+
self.class_variable_set("@@_patm_ctx_#{name}", ctx)
|
577
|
+
src = <<-RUBY
|
578
|
+
def #{name}(_obj)
|
579
|
+
_self = self
|
580
|
+
_ctx = self.#{self.name ? 'class' : 'singleton_class'}.class_variable_get(:@@_patm_ctx_#{name})
|
581
|
+
_match = {}
|
582
|
+
#{rule.src_body}
|
583
|
+
end
|
584
|
+
RUBY
|
585
|
+
class_eval(src)
|
523
586
|
end
|
524
587
|
end
|
525
588
|
end
|
data/patm.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.platform = Gem::Platform::RUBY
|
3
3
|
s.name = 'patm'
|
4
|
-
s.version = '
|
4
|
+
s.version = '3.0.0'
|
5
5
|
s.summary = 'PATtern Matching library'
|
6
6
|
s.description = 'Pattern matching library for plain data structure'
|
7
7
|
s.required_ruby_version = '>= 1.9.0'
|
@@ -16,6 +16,9 @@ Gem::Specification.new do |s|
|
|
16
16
|
s.test_files = s.files.grep(%r{^(test|spec|features)/})
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
+
s.add_development_dependency('simplecov', '~> 0.7.1')
|
20
|
+
s.add_development_dependency('simplecov-vim')
|
19
21
|
s.add_development_dependency('rspec', '~>2.14')
|
20
22
|
s.add_development_dependency('pry', '~>0.9')
|
23
|
+
s.add_development_dependency('pattern-match', '=0.5.1') # for benchmarking
|
21
24
|
end
|
data/spec/patm_spec.rb
CHANGED
@@ -1,43 +1,64 @@
|
|
1
|
+
require 'simplecov'
|
2
|
+
require 'simplecov-vim/formatter'
|
3
|
+
SimpleCov.start do
|
4
|
+
formatter SimpleCov::Formatter::VimFormatter
|
5
|
+
end
|
6
|
+
|
1
7
|
require File.join(File.dirname(__FILE__), '..', 'lib', 'patm.rb')
|
2
8
|
require 'pry'
|
3
9
|
|
4
10
|
module PatmHelper
|
5
|
-
|
11
|
+
module Pattern
|
12
|
+
extend RSpec::Matchers::DSL
|
6
13
|
|
7
|
-
|
8
|
-
|
9
|
-
|
14
|
+
matcher :these_matches do|*matches|
|
15
|
+
match do|actual|
|
16
|
+
matches.all?{|m| m.matches?(actual) }
|
17
|
+
end
|
10
18
|
end
|
11
|
-
end
|
12
19
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
20
|
+
matcher :match_to do|expected|
|
21
|
+
match do|actual|
|
22
|
+
exec(actual, expected)
|
23
|
+
end
|
17
24
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
25
|
+
def exec(actual, expected)
|
26
|
+
@match = Patm::Match.new
|
27
|
+
actual.execute(@match, expected)
|
28
|
+
end
|
29
|
+
|
30
|
+
def match; @match; end
|
22
31
|
|
23
|
-
|
32
|
+
def and_capture(g1, g2 = nil, g3 = nil, g4 = nil)
|
33
|
+
these_matches(
|
34
|
+
self, _capture(self, {1 => g1, 2 => g2, 3 => g3, 4 => g4})
|
35
|
+
)
|
36
|
+
end
|
24
37
|
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
38
|
+
def and_named_capture(capture)
|
39
|
+
these_matches(
|
40
|
+
self, _capture(self, capture)
|
41
|
+
)
|
42
|
+
end
|
29
43
|
end
|
30
44
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
45
|
+
matcher :_capture do|m, capture|
|
46
|
+
match do|_|
|
47
|
+
[m.match[1], m.match[2], m.match[3], m.match[4]] == capture.values_at(1,2,3,4)
|
48
|
+
end
|
35
49
|
end
|
36
50
|
end
|
37
51
|
|
38
|
-
|
39
|
-
|
40
|
-
|
52
|
+
module Rule
|
53
|
+
extend RSpec::Matchers::DSL
|
54
|
+
matcher :converts do|value, result|
|
55
|
+
match do|rule|
|
56
|
+
@matched = rule.apply(value, nil)
|
57
|
+
@matched == result
|
58
|
+
end
|
59
|
+
failure_message_for_should do|rule|
|
60
|
+
"match #{value.inspect} to #{rule.inspect}: expected #{result} but #{@matched.inspect}"
|
61
|
+
end
|
41
62
|
end
|
42
63
|
end
|
43
64
|
|
@@ -57,7 +78,7 @@ describe "Usage:" do
|
|
57
78
|
|
58
79
|
it 'with predefined Rule' do
|
59
80
|
p = Patm
|
60
|
-
r = p::Rule.new
|
81
|
+
r = p::Rule.new do|r|
|
61
82
|
r.on [1, p._1, p._2] do|m|
|
62
83
|
[m._1, m._2]
|
63
84
|
end
|
@@ -68,43 +89,13 @@ describe "Usage:" do
|
|
68
89
|
|
69
90
|
it 'with predefined Rule(compiled)' do
|
70
91
|
p = Patm
|
71
|
-
r = p::Rule.new
|
72
|
-
r.on [1, p._1, p._2] do|m|
|
73
|
-
[m._1, m._2]
|
74
|
-
end
|
75
|
-
r.else {|obj| [] }
|
76
|
-
end
|
77
|
-
r.apply([1, 2, 3]).should == [2, 3]
|
78
|
-
end
|
79
|
-
|
80
|
-
it 'with RuleCache' do
|
81
|
-
p = Patm
|
82
|
-
rs = p::RuleCache.new
|
83
|
-
|
84
|
-
rs.match(:pattern_1, [1, 2, 3]) do|r|
|
85
|
-
r.on [1, p._1, p._2] do|m|
|
86
|
-
[m._1, m._2]
|
87
|
-
end
|
88
|
-
r.else {|obj| [] }
|
89
|
-
end
|
90
|
-
.should == [2, 3]
|
91
|
-
|
92
|
-
rs.match(:pattern_1, [1, 3, 5]) {|r| fail "should not reach here" }.should == [3, 5]
|
93
|
-
end
|
94
|
-
|
95
|
-
it 'with RuleCache(compiled)' do
|
96
|
-
p = Patm
|
97
|
-
rs = p::RuleCache.new(true)
|
98
|
-
|
99
|
-
rs.match(:pattern_1, [1, 2, 3]) do|r|
|
92
|
+
r = p::Rule.new do|r|
|
100
93
|
r.on [1, p._1, p._2] do|m|
|
101
94
|
[m._1, m._2]
|
102
95
|
end
|
103
96
|
r.else {|obj| [] }
|
104
97
|
end
|
105
|
-
|
106
|
-
|
107
|
-
rs.match(:pattern_1, [1, 3, 5]) {|r| fail "should not reach here" }.should == [3, 5]
|
98
|
+
r.compile.apply([1, 2, 3]).should == [2, 3]
|
108
99
|
end
|
109
100
|
|
110
101
|
it 'with DSL' do
|
@@ -139,8 +130,55 @@ describe "Usage:" do
|
|
139
130
|
end
|
140
131
|
end
|
141
132
|
|
133
|
+
describe Patm::Rule do
|
134
|
+
include PatmHelper::Rule
|
135
|
+
def self.rule(name, definition, &block)
|
136
|
+
[[false, "#{name}"], [true, "#{name}(compiled)"]].each do|compile, name|
|
137
|
+
describe name do
|
138
|
+
subject { Patm::Rule.new(&definition).tap{|r| break r.compile if compile } }
|
139
|
+
self.instance_eval(&block)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
rule(:rule1, ->(r){
|
145
|
+
r.on([1, Patm._1, Patm._2]) {|m| [m._1, m._2] }
|
146
|
+
r.else { [] }
|
147
|
+
}) do
|
148
|
+
it { should converts([1, 2, 3], [2, 3]) }
|
149
|
+
it { should converts([1], []) }
|
150
|
+
end
|
151
|
+
|
152
|
+
rule(:rule2, ->(r) {
|
153
|
+
r.on(1) { 100 }
|
154
|
+
}) do
|
155
|
+
it { should converts(1, 100) }
|
156
|
+
it { expect { subject.apply(nil) }.to raise_error(Patm::NoMatchError) }
|
157
|
+
end
|
158
|
+
|
159
|
+
context 'regression' do
|
160
|
+
rule(:reg1, ->(r){
|
161
|
+
_1, _2 = Patm._1, Patm._2
|
162
|
+
r.on([1, _1, 2]) {|m| m._1 }
|
163
|
+
r.on([1, _1, _2]) {|m| [m._1, m._2] }
|
164
|
+
r.on(nil) { 100 }
|
165
|
+
r.else { nil }
|
166
|
+
}) do
|
167
|
+
it { should converts([1, 2, 2], 2) }
|
168
|
+
it { should converts([1, 2, 3], [2, 3]) }
|
169
|
+
it { should converts(nil, 100) }
|
170
|
+
it { should converts([], nil) }
|
171
|
+
end
|
172
|
+
|
173
|
+
rule(:reg2, ->(r) { r.else { nil }}) do
|
174
|
+
it { should converts(1, nil) }
|
175
|
+
it { should converts("hoge", nil) }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
142
180
|
describe Patm::Pattern do
|
143
|
-
include PatmHelper
|
181
|
+
include PatmHelper::Pattern
|
144
182
|
def self.pattern(plain, &b)
|
145
183
|
context "pattern '#{plain.inspect}'" do
|
146
184
|
subject { Patm::Pattern.build_from(plain) }
|
metadata
CHANGED
@@ -1,15 +1,43 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: patm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 3.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- todesking
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-05-
|
11
|
+
date: 2014-05-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: simplecov
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 0.7.1
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 0.7.1
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: simplecov-vim
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
13
41
|
- !ruby/object:Gem::Dependency
|
14
42
|
name: rspec
|
15
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -38,6 +66,20 @@ dependencies:
|
|
38
66
|
- - ~>
|
39
67
|
- !ruby/object:Gem::Version
|
40
68
|
version: '0.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: pattern-match
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.5.1
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 0.5.1
|
41
83
|
description: Pattern matching library for plain data structure
|
42
84
|
email: discommucative@gmail.com
|
43
85
|
executables: []
|
@@ -45,10 +87,12 @@ extensions: []
|
|
45
87
|
extra_rdoc_files: []
|
46
88
|
files:
|
47
89
|
- .gitignore
|
90
|
+
- .travis.yml
|
48
91
|
- Gemfile
|
49
92
|
- LICENSE
|
50
93
|
- README.md
|
51
94
|
- benchmark/benchmark.rb
|
95
|
+
- benchmark/comparison.rb
|
52
96
|
- lib/patm.rb
|
53
97
|
- patm.gemspec
|
54
98
|
- spec/patm_spec.rb
|