matchmaker 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile ADDED
@@ -0,0 +1,48 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "matchmaker"
8
+ gem.summary = %Q{Ruby Pattern Matching}
9
+ gem.description = %Q{A pattern matching library}
10
+ gem.email = "hayeah@gmail.com"
11
+ gem.homepage = "http://github.com/hayeah/case"
12
+ gem.authors = ["Howard Yeh"]
13
+ gem.add_development_dependency "rspec"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ rescue LoadError
17
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require 'spec/rake/spectask'
21
+ Spec::Rake::SpecTask.new(:spec) do |spec|
22
+ spec.libs << 'lib' << 'spec'
23
+ spec.spec_files = FileList['spec/**/*_spec.rb']
24
+ end
25
+
26
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.pattern = 'spec/**/*_spec.rb'
29
+ spec.rcov = true
30
+ end
31
+
32
+ task :spec => :check_dependencies
33
+
34
+ task :default => :spec
35
+
36
+ require 'rake/rdoctask'
37
+ Rake::RDocTask.new do |rdoc|
38
+ if File.exist?('VERSION')
39
+ version = File.read('VERSION')
40
+ else
41
+ version = ""
42
+ end
43
+
44
+ rdoc.rdoc_dir = 'rdoc'
45
+ rdoc.title = "case #{version}"
46
+ rdoc.rdoc_files.include('README*')
47
+ rdoc.rdoc_files.include('lib/**/*.rb')
48
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.0.1
data/lib/matchmaker.rb ADDED
@@ -0,0 +1,479 @@
1
+ require 'set'
2
+ require 'pp'
3
+ require 'stringio'
4
+
5
+ class Case
6
+ class CaseError < StandardError
7
+ end
8
+
9
+ class NoMatch < CaseError
10
+ def initialize(stack,pattern_stack,msg=nil)
11
+ @stack = stack
12
+ @pattern_stack = pattern_stack
13
+ @msg = msg
14
+ end
15
+
16
+ def inspect
17
+ self.to_s
18
+ end
19
+
20
+ def to_s
21
+ io = StringIO.new
22
+ io.puts "#<#{self.class} #{@msg}"
23
+ trace = @stack.zip(@pattern_stack).map { |obj,pat|
24
+ if pat.label
25
+ io.print pat.label
26
+ io.print ": "
27
+ PP.pp(obj,io)
28
+
29
+ else
30
+ PP.pp(obj,io)
31
+ end
32
+ }
33
+ io.string
34
+ end
35
+ end
36
+
37
+ class CaseFail < CaseError
38
+ def initialize(errors)
39
+ @errors = errors
40
+ end
41
+
42
+ def to_s
43
+ io = StringIO.new
44
+ @errors.each { |e|
45
+ io.puts e
46
+ io.puts ""
47
+ }
48
+ io.string
49
+ end
50
+ end
51
+
52
+ class NoClauses < CaseError
53
+ end
54
+
55
+ class UnboundVariable < CaseError
56
+ end
57
+
58
+ class Pattern
59
+ attr_reader :matcher, :guard, :variable, :label
60
+ def initialize(matcher,guard,variable,label=nil)
61
+ @matcher = matcher # Proc || Pattern
62
+ @guard = guard # Proc || nil
63
+ @variable = variable.to_s.downcase.to_sym if !variable.nil?
64
+ @label = label
65
+ end
66
+
67
+ def match(context)
68
+ case @matcher
69
+ when Pattern
70
+ result = @matcher.match(context)
71
+ when Proc
72
+ if @matcher.arity == 2
73
+ result = @matcher.call(context.current,context)
74
+ else
75
+ result = @matcher.call(context.current)
76
+ end
77
+
78
+ end
79
+ context.fail unless result
80
+ if @guard
81
+ result = @guard.call(context.current)
82
+ context.fail("guard failed") unless result
83
+ end
84
+ context.bind(@variable,context.current) if @variable
85
+ true
86
+ end
87
+
88
+ def when(&block)
89
+ @guard = block
90
+ self
91
+ end
92
+
93
+ def bind(var)
94
+
95
+ end
96
+ end
97
+
98
+ # matches the tail of an array
99
+ class StarPattern # [*pattern]
100
+ attr_reader :pattern, :variable, :guard
101
+ def initialize(pattern,guard,variable)
102
+ @pattern = pattern
103
+ @guard = guard
104
+ @variable = variable
105
+ end
106
+ end
107
+
108
+ class MatchContext
109
+ def initialize(pattern,object)
110
+ # stack of references we are destructuring,
111
+ # we start off with the object itself.
112
+ @stack = [object]
113
+ @pattern_stack = [pattern]
114
+ @bindings = {}
115
+ end
116
+
117
+ def current
118
+ @stack.last
119
+ end
120
+
121
+ def bind(var,val)
122
+ if @bindings.has_key?(var)
123
+ self.fail unless @bindings[var] == val
124
+ else
125
+ @bindings[var] = val
126
+ end
127
+ end
128
+
129
+ def fail(msg=nil)
130
+ raise NoMatch.new(@stack,@pattern_stack,msg) # what object is failing patter match
131
+ end
132
+
133
+ unless defined?(BasicObject)
134
+ # for ruby 1.8
135
+ class BasicObject #:nodoc:
136
+ instance_methods.each { |m| undef_method m unless m =~ /^__|instance_eval|object_id/ }
137
+ end
138
+ end
139
+
140
+ class CallContext < BasicObject
141
+ def initialize(bindings)
142
+ @bindings = bindings
143
+ end
144
+
145
+ def method_missing(var,*args)
146
+ ::Kernel.raise ::Case::UnboundVariable.new unless @bindings.has_key?(var)
147
+ @bindings[var]
148
+ end
149
+ end
150
+
151
+ IS_RUBY_19 = (RUBY_VERSION > "1.9")
152
+ def call(&block)
153
+ return block.call if @bindings.empty?
154
+ context = CallContext.new(@bindings)
155
+
156
+ #http://coderrr.wordpress.com/2009/06/02/fixing-constant-lookup-in-dsls-in-ruby-1-9/
157
+ #http://coderrr.wordpress.com/2009/05/18/dynamically-adding-a-constant-nesting-in-ruby-1-9/
158
+ if IS_RUBY_19
159
+ # what a fail
160
+ l = lambda { context.instance_eval(&block) }
161
+ modules = block.binding.eval "Module.nesting"
162
+ modules.reverse.inject(l) {|l, k| lambda { k.class_eval(&l) } }.call
163
+ else
164
+ context.instance_eval &block
165
+ end
166
+ end
167
+
168
+ def nest(object,pattern)
169
+ @stack.push(object)
170
+ @pattern_stack.push(pattern)
171
+ if pattern
172
+ pattern.match(self)
173
+ else
174
+ yield
175
+ end
176
+ @stack.pop
177
+ @pattern_stack.pop
178
+ end
179
+ end
180
+
181
+ class Clause
182
+ def initialize(pattern,action)
183
+ raise "badarg" unless Pattern === pattern
184
+ @pattern = pattern
185
+ @action = action
186
+ end
187
+
188
+ def match(object)
189
+ context = MatchContext.new(@pattern,object)
190
+ @pattern.match(context)
191
+ @action.nil? ? true : context.call(&@action)
192
+ end
193
+ end
194
+
195
+ def self.pattern(&block)
196
+ pat = nil
197
+ Case.new {
198
+ pat = is(self.instance_eval(&block))
199
+ of(pat) # dummy clause to prevent raising error
200
+ }
201
+ pat
202
+ end
203
+
204
+ def initialize(&block)
205
+ @clauses = []
206
+ self.instance_eval(&block)
207
+ raise NoClauses if @clauses.empty?
208
+ self
209
+ end
210
+
211
+ def of(o,&action)
212
+ case o
213
+ when StarPattern
214
+ raise "badarg: star pattern only allowed in structural patterns."
215
+ when Pattern
216
+ pattern = o
217
+ else
218
+ pattern = is(o)
219
+ end
220
+ @clauses << Clause.new(pattern,action)
221
+ end
222
+
223
+ # this can be used to coerce literal values into pattern
224
+ def is(o,var=nil,&guard)
225
+ case o
226
+ when Pattern
227
+ #bind(o,var,&guard)
228
+ o # return a is
229
+ when Regexp
230
+ regexp = o
231
+ string(regexp,var,&guard)
232
+ when Array
233
+ array(o,var)
234
+ when Range
235
+ integer(o,var,&guard)
236
+ when Hash
237
+ hash(o,var,&guard)
238
+ when Class
239
+ a(o,var,&guard)
240
+ else
241
+ literal(o,var,&guard)
242
+ end
243
+ end
244
+
245
+ def literal(val,var=nil,&guard)
246
+ matcher = lambda { |obj|
247
+ obj == val
248
+ }
249
+ Pattern.new(matcher,guard,var,"Literal(#{val})")
250
+ end
251
+
252
+ def a(klass,var=nil,&guard)
253
+ # TODO should assert var to be symbol
254
+ matcher = lambda { |o|
255
+ o.is_a?(klass)
256
+ }
257
+ Pattern.new(matcher,guard,var,"Class(#{klass})")
258
+ end
259
+
260
+ def integer(o=nil,var=nil,&guard)
261
+ case o
262
+ when Integer
263
+ literal(o,var,&guard)
264
+ when Range
265
+ range = o
266
+ matcher_lambda = lambda { |o|
267
+ range.include?(o)
268
+ }
269
+ Pattern.new(matcher_lambda,guard,var,"Integer(#{range})")
270
+ when Array
271
+ set = Set.new(o)
272
+ matcher_lambda = lambda { |o|
273
+ set.include?(o)
274
+ }
275
+ Pattern.new(matcher_lambda,guard,var,"Integer(#{o.join(",")})")
276
+ when nil
277
+ a(Integer,var,&guard)
278
+ else
279
+ raise "badarg"
280
+ end
281
+ end
282
+
283
+ def symbol(o=nil,var=nil,&guard)
284
+ case o
285
+ when Symbol
286
+ literal(o.to_sym,var,&guard)
287
+ when Regexp
288
+ re = o
289
+ matcher_lambda = lambda { |o|
290
+ o.is_a?(Symbol) && o.to_s =~ re
291
+ }
292
+ Pattern.new(matcher_lambda,guard,var,"Symbol")
293
+ when nil
294
+ a(Symbol,var,&guard)
295
+ else
296
+ raise "badarg"
297
+ end
298
+ end
299
+
300
+ def string(o=nil,var=nil,&guard)
301
+ case o
302
+ when String
303
+ literal(o.to_s,var,&guard)
304
+ when Regexp
305
+ re = o
306
+ matcher_lambda = lambda { |o|
307
+ o.is_a?(String) && o.to_s =~ re
308
+ }
309
+ Pattern.new(matcher_lambda,guard,var,"String")
310
+ when nil
311
+ a(String,var,&guard)
312
+ else
313
+ raise "badarg"
314
+ end
315
+ end
316
+
317
+ def bind(o,var,&guard)
318
+ Pattern.new(is(o),guard,var)
319
+ end
320
+
321
+ def one_of(patterns,var=nil,&guard)
322
+ patterns = patterns.map { |o|
323
+ case o
324
+ when Pattern, StarPattern
325
+ o
326
+ else
327
+ is(o) # coerce into pattern
328
+ end
329
+ }
330
+ matcher = lambda { |o,context|
331
+ r = false
332
+ patterns.each { |pat|
333
+ begin
334
+ context.nest(o,pat)
335
+ r = true
336
+ break
337
+ rescue NoMatch
338
+ next
339
+ end
340
+ }
341
+ return r
342
+ }
343
+ Pattern.new(matcher,guard,var,"OneOf")
344
+ end
345
+
346
+ def array(os,var=nil,&guard)
347
+ # build structrual pattern
348
+ patterns = os.map { |o|
349
+ case o
350
+ when Pattern, StarPattern
351
+ o
352
+ else
353
+ is(o) # coerce into pattern
354
+ end
355
+ }
356
+
357
+ #allow star pattern only for the last position
358
+ star_pattern = nil
359
+ # this works with 1.8.6
360
+ patterns.each_with_index { |pattern,i|
361
+ # allows star pattern only at the end of the pattern
362
+ if pattern.is_a?(StarPattern)
363
+ unless i == patterns.length - 1
364
+ raise "badarg: star pattern only allowed at the end of the array."
365
+ end
366
+ star_pattern = pattern
367
+ end
368
+ }
369
+ if star_pattern
370
+ patterns = patterns[0..-2]
371
+ end
372
+
373
+ matcher = lambda { |o,context|
374
+ return false unless o.is_a?(Array)
375
+ if star_pattern
376
+ # this is an array pattern with star pattern to match tial
377
+ context.fail("not enough elements") if patterns.length > o.length + 1
378
+ else
379
+ # no star pattern
380
+ context.fail("not enough elements") if patterns.length != o.length
381
+ end
382
+ # match mandatory elements
383
+ patterns.each_with_index { |pattern,i|
384
+ context.nest(o[i],pattern)
385
+ }
386
+ # match tail
387
+ if star_pattern
388
+ tail = o[patterns.size..-1]
389
+ tail.each do |tail_element|
390
+ context.nest(tail_element,star_pattern.pattern)
391
+ end
392
+ return false if star_pattern.guard && star_pattern.guard.call(tail) == false
393
+ context.bind(star_pattern.variable,tail) if star_pattern.variable
394
+ end
395
+ true
396
+ }
397
+ Pattern.new(matcher,guard,var,"Array")
398
+ end
399
+
400
+ # ["key"] => optional key
401
+ ## /regexp/ => across all key that matches
402
+ # literal => required key
403
+ # bleh..
404
+ def hash(hash=nil,var=nil,&guard)
405
+ # coerce literals into patterns
406
+ patterns = hash.to_a.map! { |(k,v)|
407
+ [k,(v.is_a?(Pattern) ? v : is(v))]
408
+ }
409
+ matcher = lambda { |h,context|
410
+ context.fail unless h.is_a?(Hash)
411
+ patterns.each { |(k,value_pattern)|
412
+ case k
413
+ when Array
414
+ # optional key
415
+ k = k.first
416
+ # try matching iff the value is non-nil
417
+ if value=h[k]
418
+ context.nest(value,value_pattern)
419
+ end
420
+ # regexp match is a bit silly...
421
+ # when Regexp
422
+ # # pattern applies to all keys that matches regexp
423
+ # re = k
424
+ # hash.keys.each do |k|
425
+ # if k =~ re
426
+ # context.nest(h[k],value_pattern)
427
+ # end
428
+ # end
429
+
430
+ else
431
+ # required key
432
+ context.fail("no required key: #{k}") unless h.has_key?(k)
433
+ context.nest(h[k],value_pattern)
434
+ end
435
+ }
436
+ true
437
+ }
438
+ Pattern.new(matcher,guard,var,"Hash")
439
+ end
440
+
441
+ def _(var=nil,&guard)
442
+ matcher = lambda { |o| true }
443
+ Pattern.new(matcher, guard, var)
444
+ end
445
+
446
+ def _!(pattern,var=nil,&guard)
447
+ pattern = is(pattern) unless Pattern === pattern
448
+ StarPattern.new(pattern,guard,var)
449
+ end
450
+
451
+ def match(o)
452
+ errors = []
453
+ @clauses.each { |c|
454
+ begin
455
+ return c.match(o)
456
+ rescue NoMatch
457
+ errors << $!
458
+ end
459
+ }
460
+ raise CaseFail.new(errors)
461
+ end
462
+
463
+ def to_s
464
+ "#<#{self.class}>"
465
+ end
466
+
467
+ def inspect
468
+ self.to_s
469
+ end
470
+ end
471
+
472
+ module MatchMaker
473
+ Case = ::Case
474
+ end
475
+
476
+ def Case(obj,&block)
477
+ Case.new(&block).match(obj)
478
+ end
479
+