patm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (9) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +1 -0
  3. data/Gemfile +2 -0
  4. data/LICENSE +21 -0
  5. data/README.md +85 -0
  6. data/lib/patm.rb +229 -0
  7. data/patm.gemspec +21 -0
  8. data/spec/patm_spec.rb +143 -0
  9. metadata +79 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 1adf094454c70d6f4147b4c44e134a0204274e6c
4
+ data.tar.gz: 93e09c5195a23d9dfd2ac2e317841c4a6cb80afc
5
+ SHA512:
6
+ metadata.gz: 80ada62d20ae8926cf97f27c120e70896e3f0d916e38058221afa60967b5a6701cb2ca8d1dd35c8807d104f616bddc62bd032f64e27857d67c2821e5a7aae2a9
7
+ data.tar.gz: 824f33ef73d6561932e620ca7e657c74ad2767301b32ede0c47677fc4a50010277a603e419b17967273d86320288e9a08788c47595ed3179f332c6d9cddeb09c
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2014 todesking
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,85 @@
1
+ # PATM: PATtern Matcher for Ruby
2
+
3
+ ## Usage
4
+
5
+ ```ruby
6
+ require 'patm'
7
+ ```
8
+
9
+ ```ruby
10
+ # With case(simple but slow)
11
+ def match(obj)
12
+ p = Patm
13
+ _xs = Patm::ARRAY_REST
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
21
+
22
+ match([1, 2, 3])
23
+ # => [2, 3]
24
+
25
+ match([:x, :y, :z])
26
+ # => [:z, :y]
27
+
28
+ match([])
29
+ # => nil
30
+ ```
31
+
32
+ ```ruby
33
+ # With pre-built Rule
34
+ rule = Patm::Rule.new do|r|
35
+ p = Patm
36
+ _xs = Patm::ARRAY_REST
37
+ r.on [:x, p._1, p._2] do|m|
38
+ [m._2, m._1]
39
+ end
40
+ r.on [1, _xs&p._1] do|m|
41
+ m._1
42
+ end
43
+ end
44
+
45
+ rule.apply([1,2,3])
46
+ # => [2, 3]
47
+
48
+ rule.apply([:x, :y, :z])
49
+ # => [:z, :y]
50
+
51
+ rule.apply([])
52
+ # => nil
53
+ ```
54
+
55
+ ```ruby
56
+ # With cached rules
57
+ class A
58
+ def initialize
59
+ @rules = Patm::RuleCache.new
60
+ end
61
+
62
+ def match1(obj)
63
+ @rules.match(:match1, obj) do|r|
64
+ p = Patm
65
+ r.on [:x, p._1, p._2] do|m|
66
+ [m._1, m._2]
67
+ end
68
+ end
69
+ end
70
+
71
+ def match2(obj)
72
+ @rules.match(:match2, obj) do|r|
73
+ # ...
74
+ end
75
+ end
76
+ end
77
+ ```
78
+
79
+
80
+ ## Changes
81
+
82
+ ### 0.0.1
83
+
84
+ - Initial release
85
+
@@ -0,0 +1,229 @@
1
+ module Patm
2
+ class Pattern
3
+ def execute(match, obj); true; end
4
+
5
+ def rest?
6
+ false
7
+ end
8
+
9
+ def self.build_from(plain)
10
+ case plain
11
+ when Pattern
12
+ plain
13
+ when Array
14
+ if plain.last.is_a?(Pattern) && plain.last.rest?
15
+ Arr.new(plain[0..-2].map{|p| build_from(p) }, plain.last)
16
+ else
17
+ Arr.new(plain.map{|p| build_from(p) })
18
+ end
19
+ else
20
+ Obj.new(plain)
21
+ end
22
+ end
23
+
24
+ def &(rhs)
25
+ And.new([self, rhs])
26
+ end
27
+
28
+ class Arr < self
29
+ def initialize(head, rest = nil, tail = [])
30
+ @head = head
31
+ @rest = rest
32
+ @tail = tail
33
+ end
34
+
35
+ def execute(mmatch, obj)
36
+ size_min = @head.size + @tail.size
37
+ return false unless obj.is_a?(Array)
38
+ return false unless @rest ? (obj.size >= size_min) : (obj.size == size_min)
39
+ @head.zip(obj[0..(@head.size - 1)]).all? {|pat, o|
40
+ pat.execute(mmatch, o)
41
+ } &&
42
+ @tail.zip(obj[(-@tail.size)..-1]).all? {|pat, o|
43
+ pat.execute(mmatch, o)
44
+ } &&
45
+ (!@rest || @rest.execute(mmatch, obj[@head.size..-(@tail.size+1)]))
46
+ end
47
+
48
+ def inspect; (@head + [@rest] + @tail).inspect; end
49
+ end
50
+
51
+ class ArrRest < self
52
+ def execute(mmatch, obj)
53
+ true
54
+ end
55
+ def rest?
56
+ true
57
+ end
58
+ def inspect; "..."; end
59
+ end
60
+
61
+ class Obj < self
62
+ def initialize(obj)
63
+ @obj = obj
64
+ end
65
+
66
+ def execute(mmatch, obj)
67
+ @obj === obj
68
+ end
69
+
70
+ def inspect
71
+ "OBJ(#{@obj.inspect})"
72
+ end
73
+ end
74
+
75
+ class Any < self
76
+ def execute(match, obj); true; end
77
+ def inspect; 'ANY'; end
78
+ end
79
+
80
+ class Group < self
81
+ def initialize(index)
82
+ @index = index
83
+ end
84
+ attr_reader :index
85
+ def execute(mmatch, obj)
86
+ mmatch[@index] = obj
87
+ true
88
+ end
89
+ def inspect; "GROUP(#{@index})"; end
90
+ end
91
+
92
+ class Or < self
93
+ def initialize(pats)
94
+ @pats = pats
95
+ end
96
+ def execute(mmatch, obj)
97
+ @pats.any? do|pat|
98
+ pat.execute(mmatch, obj)
99
+ end
100
+ end
101
+ def rest?
102
+ @pats.any?(&rest?)
103
+ end
104
+ def inspect
105
+ "OR(#{@pats.map(&:inspect).join(',')})"
106
+ end
107
+ end
108
+
109
+ class And <self
110
+ def initialize(pats)
111
+ @pats = pats
112
+ end
113
+ def execute(mmatch, obj)
114
+ @pats.all? do|pat|
115
+ pat.execute(mmatch, obj)
116
+ end
117
+ end
118
+ def rest?
119
+ @pats.any?(&:rest?)
120
+ end
121
+ def inspect
122
+ "AND(#{@pats.map(&:inspect).join(',')})"
123
+ end
124
+ end
125
+ end
126
+
127
+ ANY = Pattern::Any.new
128
+ GROUP = 100.times.map{|i| Pattern::Group.new(i) }
129
+ ARRAY_REST = Pattern::ArrRest.new
130
+
131
+ class Rule
132
+ def initialize(&block)
133
+ # { Pattern => Proc }
134
+ @rules = []
135
+ block[self]
136
+ end
137
+
138
+ def on(pat, &block)
139
+ @rules << [Pattern.build_from(pat), block]
140
+ end
141
+
142
+ def else(&block)
143
+ @rules << [ANY, lambda {|m,o| block[o] }]
144
+ end
145
+
146
+ def apply(obj)
147
+ match = Match.new
148
+ @rules.each do|(pat, block)|
149
+ if pat.execute(match, obj)
150
+ return block.call(match, obj)
151
+ end
152
+ end
153
+ nil
154
+ end
155
+ end
156
+
157
+ class RuleCache
158
+ def initialize
159
+ @rules = {}
160
+ end
161
+ def match(rule_name, obj, &rule)
162
+ (@rules[rule_name] ||= ::Patm::Rule.new(&rule)).apply(obj)
163
+ end
164
+ end
165
+
166
+ class Match
167
+ def initialize
168
+ @group = {}
169
+ end
170
+
171
+ def [](i)
172
+ @group[i]
173
+ end
174
+
175
+ def []=(i, val)
176
+ @group[i] = val
177
+ end
178
+
179
+ Patm::GROUP.each do|g|
180
+ define_method "_#{g.index}" do
181
+ self[g.index]
182
+ end
183
+ end
184
+ end
185
+
186
+ class CaseBinder
187
+ def initialize(pat)
188
+ @pat = pat
189
+ @match = Match.new
190
+ end
191
+
192
+ def ===(obj)
193
+ @pat.execute(@match, obj)
194
+ end
195
+
196
+ def [](i); @match[i]; end
197
+ Patm::GROUP.each do|g|
198
+ define_method "_#{g.index}" do
199
+ @match[g.index]
200
+ end
201
+ end
202
+ end
203
+
204
+ def self.match(plain_pat)
205
+ CaseBinder.new Pattern.build_from(plain_pat)
206
+ end
207
+
208
+ def self.match_array(head, rest_spat = nil, tail = [])
209
+ Match.new(
210
+ Pattern::Arr.new(
211
+ head.map{|e| Pattern.build_from(e)},
212
+ rest_spat,
213
+ tail.map{|e| Pattern.build_from(e)}
214
+ )
215
+ )
216
+ end
217
+
218
+ def self.or(*pats)
219
+ Pattern::Or.new(pats.map{|p| Pattern.build_from(p) })
220
+ end
221
+
222
+ class <<self
223
+ GROUP.each do|g|
224
+ define_method "_#{g.index}" do
225
+ g
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,21 @@
1
+ Gem::Specification.new do |s|
2
+ s.platform = Gem::Platform::RUBY
3
+ s.name = 'patm'
4
+ s.version = '0.0.1'
5
+ s.summary = 'PATtern Matching library'
6
+ s.description = 'Pattern matching library for plain data structure'
7
+ s.required_ruby_version = '>= 1.9.0'
8
+ s.license = 'MIT'
9
+
10
+ s.author = 'todesking'
11
+ s.email = 'discommucative@gmail.com'
12
+ s.homepage = 'https://github.com/todesking/patm'
13
+
14
+ s.files = `git ls-files`.split($\)
15
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ s.test_files = s.files.grep(%r{^(test|spec|features)/})
17
+ s.require_paths = ["lib"]
18
+
19
+ s.add_development_dependency('rspec', '~>2.14')
20
+ s.add_development_dependency('pry', '~>0.9')
21
+ end
@@ -0,0 +1,143 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'patm.rb')
2
+ require 'pry'
3
+
4
+ module PatmHelper
5
+ extend RSpec::Matchers::DSL
6
+
7
+ matcher :these_matches do|*matches|
8
+ match do|actual|
9
+ matches.all?{|m| m.matches?(actual) }
10
+ end
11
+ end
12
+
13
+ matcher :match_to do|expected|
14
+ match do|actual|
15
+ exec(actual, expected)
16
+ end
17
+
18
+ def exec(actual, expected)
19
+ @match = Patm::Match.new
20
+ actual.execute(@match, expected)
21
+ end
22
+
23
+ def match; @match; end
24
+
25
+ def and_capture(g1, g2 = nil, g3 = nil)
26
+ these_matches(
27
+ self, _capture(self, g1, g2, g3)
28
+ )
29
+ end
30
+ end
31
+
32
+ matcher :_capture do|m, g1, g2, g3|
33
+ match do|_|
34
+ [m.match[1], m.match[2], m.match[3]] == [g1, g2, g3]
35
+ end
36
+ end
37
+
38
+ end
39
+
40
+ describe "Usage:" do
41
+ it 'with case expression' do
42
+ p = Patm
43
+ case [1, 2, 3]
44
+ when m = p.match([1, p._1, p._2])
45
+ [m._1, m._2]
46
+ else
47
+ []
48
+ end
49
+ .should == [2, 3]
50
+ end
51
+
52
+ it 'with predefined Rule' do
53
+ p = Patm
54
+ r = p::Rule.new do|r|
55
+ r.on [1, p._1, p._2] do|m|
56
+ [m._1, m._2]
57
+ end
58
+ r.else {|obj| [] }
59
+ end
60
+ r.apply([1, 2, 3]).should == [2, 3]
61
+ end
62
+
63
+ it 'with RuleCache' do
64
+ p = Patm
65
+ rs = p::RuleCache.new
66
+
67
+ rs.match(:pattern_1, [1, 2, 3]) do|r|
68
+ r.on [1, p._1, p._2] do|m|
69
+ [m._1, m._2]
70
+ end
71
+ r.else {|obj| [] }
72
+ end
73
+ .should == [2, 3]
74
+
75
+ rs.match(:pattern_1, [1, 3, 5]) {|r| fail "should not reach here" }.should == [3, 5]
76
+ end
77
+ end
78
+
79
+ describe Patm::Pattern do
80
+ include PatmHelper
81
+ def self.pattern(plain, &b)
82
+ context "pattern '#{plain.inspect}'" do
83
+ subject { Patm::Pattern.build_from(plain) }
84
+ instance_eval(&b)
85
+ end
86
+ end
87
+
88
+ pattern 1 do
89
+ it { should match_to(1) }
90
+ it { should_not match_to(2) }
91
+ end
92
+
93
+ pattern [] do
94
+ it { should match_to [] }
95
+ it { should_not match_to {} }
96
+ it { should_not match_to [1] }
97
+ end
98
+
99
+ pattern [1,2] do
100
+ it { should match_to [1,2] }
101
+ it { should_not match_to [1] }
102
+ it { should_not match_to [1, -1] }
103
+ it { should_not match_to [1,2,3] }
104
+ end
105
+
106
+ pattern Patm::ANY do
107
+ it { should match_to 1 }
108
+ it { should match_to ["foo", "bar"] }
109
+ end
110
+
111
+ pattern [1, Patm::ANY, 3] do
112
+ it { should match_to [1, 2, 3] }
113
+ it { should match_to [1, 0, 3] }
114
+ it { should_not match_to [1, 0, 4] }
115
+ end
116
+
117
+ pattern Patm.or(1, 2) do
118
+ it { should match_to 1 }
119
+ it { should match_to 2 }
120
+ it { should_not match_to 3 }
121
+ end
122
+
123
+ pattern Patm._1 do
124
+ it { should match_to(1).and_capture(1) }
125
+ it { should match_to('x').and_capture('x') }
126
+ end
127
+
128
+ pattern [0, Patm._1, Patm._2] do
129
+ it { should match_to([0, 1, 2]).and_capture(1, 2) }
130
+ it { should_not match_to(['x', 1, 2]).and_capture(1, 2) }
131
+ end
132
+
133
+ pattern [0, 1, Patm::ARRAY_REST] do
134
+ it { should_not match_to([0]) }
135
+ it { should match_to([0, 1]) }
136
+ it { should match_to([0, 1, 2, 3]) }
137
+ end
138
+
139
+ pattern [0, 1, Patm::ARRAY_REST & Patm._1] do
140
+ it { should match_to([0, 1]).and_capture([]) }
141
+ it { should match_to([0, 1, 2, 3]).and_capture([2, 3]) }
142
+ end
143
+ end
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: patm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - todesking
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-04-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '2.14'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '2.14'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '0.9'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '0.9'
41
+ description: Pattern matching library for plain data structure
42
+ email: discommucative@gmail.com
43
+ executables: []
44
+ extensions: []
45
+ extra_rdoc_files: []
46
+ files:
47
+ - .gitignore
48
+ - Gemfile
49
+ - LICENSE
50
+ - README.md
51
+ - lib/patm.rb
52
+ - patm.gemspec
53
+ - spec/patm_spec.rb
54
+ homepage: https://github.com/todesking/patm
55
+ licenses:
56
+ - MIT
57
+ metadata: {}
58
+ post_install_message:
59
+ rdoc_options: []
60
+ require_paths:
61
+ - lib
62
+ required_ruby_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - '>='
65
+ - !ruby/object:Gem::Version
66
+ version: 1.9.0
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ requirements: []
73
+ rubyforge_project:
74
+ rubygems_version: 2.2.2
75
+ signing_key:
76
+ specification_version: 4
77
+ summary: PATtern Matching library
78
+ test_files:
79
+ - spec/patm_spec.rb