patm 2.0.1 → 3.0.0
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 +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
|
+
[](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
|