matchmaker 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 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
+