matches 1.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.
- data/LICENSE +20 -0
- data/README.md +30 -0
- data/README.rdoc +18 -0
- data/Rakefile +63 -0
- data/VERSION +1 -0
- data/features/define_matches.feature +44 -0
- data/features/step_definitions/method_steps.rb +33 -0
- data/features/support/env.rb +3 -0
- data/lib/match_method.rb +48 -0
- data/lib/matches.rb +33 -0
- data/spec/match_method_spec.rb +75 -0
- data/spec/matches_spec.rb +74 -0
- data/spec/spec_helper.rb +9 -0
- metadata +93 -0
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Phil Calvin
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
Matches – easygoing methods
|
2
|
+
===========================
|
3
|
+
|
4
|
+
Matches is an easy DSL for defining regular-expression-based methods in Ruby.
|
5
|
+
|
6
|
+
Start playing with matches:
|
7
|
+
|
8
|
+
require 'matches'
|
9
|
+
|
10
|
+
class Hippo
|
11
|
+
def initialize
|
12
|
+
@verbs = []
|
13
|
+
end
|
14
|
+
|
15
|
+
matches /(\w+)\!/ do |verb|
|
16
|
+
@verbs << verb
|
17
|
+
end
|
18
|
+
|
19
|
+
matches /(\w+)ed\?/ do |verb|
|
20
|
+
@verbs.include?(verb)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
herman = Hippo.new
|
25
|
+
herman.fatten!
|
26
|
+
herman.touch!
|
27
|
+
|
28
|
+
herman.touched?
|
29
|
+
==> true
|
30
|
+
|
data/README.rdoc
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
= matches - easygoing methods
|
2
|
+
|
3
|
+
Matches is an easy DSL for defining regular-expression-based methods in Ruby.
|
4
|
+
|
5
|
+
== Note on Patches/Pull Requests
|
6
|
+
|
7
|
+
* Fork the project.
|
8
|
+
* Make your feature addition or bug fix.
|
9
|
+
* Add tests for it. This is important so I don't break it in a
|
10
|
+
future version unintentionally.
|
11
|
+
* Commit, do not mess with rakefile, version, or history.
|
12
|
+
(if you want to have your own version, that is fine but
|
13
|
+
bump version in a commit by itself I can ignore when I pull)
|
14
|
+
* Send me a pull request. Bonus points for topic branches.
|
15
|
+
|
16
|
+
== Copyright
|
17
|
+
|
18
|
+
Copyright (c) 2009 Phil Calvin. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "matches"
|
8
|
+
gem.summary = %Q{A DSL for defining regular-expression-based methods in Ruby.}
|
9
|
+
gem.description = %Q{Matches allows you to define methods that have regular
|
10
|
+
expressions rather than names, and automatically
|
11
|
+
configires method_missing to handle them.}
|
12
|
+
gem.email = "pncalvin@gmail.com"
|
13
|
+
gem.homepage = "http://github.com/pnc/matches"
|
14
|
+
gem.authors = ["Phil Calvin"]
|
15
|
+
gem.add_development_dependency "rspec", ">= 1.2.9"
|
16
|
+
gem.add_development_dependency "cucumber", ">= 0"
|
17
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
18
|
+
end
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
22
|
+
end
|
23
|
+
|
24
|
+
begin
|
25
|
+
require 'rcov/rcovtask'
|
26
|
+
Rcov::RcovTask.new do |test|
|
27
|
+
test.libs << 'test'
|
28
|
+
test.pattern = 'test/**/test_*.rb'
|
29
|
+
test.verbose = true
|
30
|
+
end
|
31
|
+
rescue LoadError
|
32
|
+
task :rcov do
|
33
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
require 'rake/rdoctask'
|
38
|
+
Rake::RDocTask.new do |rdoc|
|
39
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
40
|
+
|
41
|
+
rdoc.rdoc_dir = 'rdoc'
|
42
|
+
rdoc.title = "matches #{version}"
|
43
|
+
rdoc.rdoc_files.include('README*')
|
44
|
+
rdoc.rdoc_files.include('lib/*.rb')
|
45
|
+
end
|
46
|
+
|
47
|
+
require 'spec/rake/spectask'
|
48
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
49
|
+
spec.spec_opts = ['--options', "\"spec/spec.opts\""]
|
50
|
+
spec.libs << 'lib' << 'spec'
|
51
|
+
spec.spec_files = FileList['spec/*_spec.rb']
|
52
|
+
end
|
53
|
+
|
54
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
55
|
+
|
56
|
+
spec.libs << 'lib' << 'spec'
|
57
|
+
spec.pattern = 'spec/*_spec.rb'
|
58
|
+
spec.rcov = true
|
59
|
+
end
|
60
|
+
|
61
|
+
task :spec => :check_dependencies
|
62
|
+
|
63
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.0.0
|
@@ -0,0 +1,44 @@
|
|
1
|
+
As a fancy-pants programmer
|
2
|
+
I want to define match methods using regular expressions
|
3
|
+
So that I don't have to screw around with method_missing
|
4
|
+
|
5
|
+
Scenario: Define a matcher method
|
6
|
+
Given I have the following Ruby code:
|
7
|
+
"""
|
8
|
+
class Hippo
|
9
|
+
matches /(\w+)\!/ do |verb|
|
10
|
+
puts "I've been #{verb}ed!"
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Hippo.new.touch!
|
15
|
+
"""
|
16
|
+
When I execute the code
|
17
|
+
Then I should see "I've been touched!" in the output
|
18
|
+
|
19
|
+
Scenario: Another meta-method
|
20
|
+
Given I reset the class Hippo
|
21
|
+
Given I have the following Ruby code:
|
22
|
+
"""
|
23
|
+
class Hippo
|
24
|
+
def initialize
|
25
|
+
@verbs = []
|
26
|
+
end
|
27
|
+
|
28
|
+
matches /(\w+)\!/ do |verb|
|
29
|
+
@verbs << verb
|
30
|
+
end
|
31
|
+
|
32
|
+
matches /(\w+)ed\?/ do |verb|
|
33
|
+
@verbs.include?(verb)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
herman = Hippo.new
|
38
|
+
herman.fatten!
|
39
|
+
herman.touch!
|
40
|
+
|
41
|
+
puts herman.touched?
|
42
|
+
"""
|
43
|
+
When I execute the code
|
44
|
+
Then I should see "true" in the output
|
@@ -0,0 +1,33 @@
|
|
1
|
+
Given /^I reset the class (.+)$/ do |klass|
|
2
|
+
eval("#{klass}.reset_meta_methods")
|
3
|
+
end
|
4
|
+
|
5
|
+
Given /^I have the following Ruby code:$/ do |code|
|
6
|
+
@code = code
|
7
|
+
end
|
8
|
+
|
9
|
+
When /^I execute the code$/ do
|
10
|
+
module Kernel
|
11
|
+
def puts(str)
|
12
|
+
OutputStorage.write("#{str}\n")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
eval(@code)
|
17
|
+
end
|
18
|
+
|
19
|
+
Then /^I should see \"(.+)\" in the output$/ do |text|
|
20
|
+
OutputStorage.output.should =~ /#{text}/
|
21
|
+
end
|
22
|
+
|
23
|
+
class OutputStorage
|
24
|
+
@@output = ""
|
25
|
+
|
26
|
+
def self.write(str)
|
27
|
+
@@output += str
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.output
|
31
|
+
@@output
|
32
|
+
end
|
33
|
+
end
|
data/lib/match_method.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
class MatchMethod
|
2
|
+
attr_accessor :matcher, :proc
|
3
|
+
|
4
|
+
# Allows properties to be specified in the constructor.
|
5
|
+
# E.g.,
|
6
|
+
# MetaMethod.new(:matcher => /foo/)
|
7
|
+
def initialize(args)
|
8
|
+
args.each do |key, value|
|
9
|
+
send("#{key}=", value)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
# Returns whether this MetaMethod is capable of matching the given
|
14
|
+
# message.
|
15
|
+
def matches?(message)
|
16
|
+
!!(message.to_s =~ matcher)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Calls the method's proc if the message matches.
|
20
|
+
def match(instance, message, *args)
|
21
|
+
groups = message.to_s.match(matcher)[1..-1]
|
22
|
+
|
23
|
+
# Should curry here: instance.instance_eval( &proc.curry(groups + args) )
|
24
|
+
# Or we could use instance_exec.
|
25
|
+
# But these require 1.9. Darn. One day!
|
26
|
+
|
27
|
+
full = (groups + args).flatten
|
28
|
+
instance.instance_exec(*full, &proc)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# SWEET HACK ZOMG. Mind has been blown.
|
33
|
+
# http://www.ruby-forum.com/topic/54096
|
34
|
+
# Mauricio Fernandez is a Ruby beast.
|
35
|
+
# This provides instance-exec-like functionality in Ruby 1.8.
|
36
|
+
|
37
|
+
class Object
|
38
|
+
def instance_exec(*args, &block)
|
39
|
+
mname = "__instance_exec_#{Thread.current.object_id.abs}"
|
40
|
+
class << self; self end.class_eval{ define_method(mname, &block) }
|
41
|
+
begin
|
42
|
+
ret = send(mname, *args)
|
43
|
+
ensure
|
44
|
+
class << self; self end.class_eval{ undef_method(mname) } rescue nil
|
45
|
+
end
|
46
|
+
ret
|
47
|
+
end
|
48
|
+
end
|
data/lib/matches.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/match_method"
|
2
|
+
|
3
|
+
module MatchDef
|
4
|
+
def matches(regexp, &block)
|
5
|
+
@@match_methods ||= []
|
6
|
+
|
7
|
+
@@match_methods << MatchMethod.new( :matcher => regexp,
|
8
|
+
:proc => block )
|
9
|
+
self.class_eval {
|
10
|
+
unless respond_to?(:match_method_missing)
|
11
|
+
def match_method_missing(message, *args)
|
12
|
+
# Attempt to evaluate this using a MetaMethod
|
13
|
+
result = @@match_methods.find do |mm|
|
14
|
+
if mm.matches?(message)
|
15
|
+
return mm.match(self, message, args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
return result if result
|
19
|
+
return old_method_missing(message, args)
|
20
|
+
end
|
21
|
+
|
22
|
+
alias_method :old_method_missing, :method_missing
|
23
|
+
alias_method :method_missing, :match_method_missing
|
24
|
+
end
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
def reset_meta_methods
|
29
|
+
@@match_methods = []
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
Class.class_eval { include MatchDef }
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe MatchMethod do
|
4
|
+
it "should store its matcher" do
|
5
|
+
mm = MatchMethod.new(:matcher => /foo/)
|
6
|
+
mm.matcher.should == /foo/
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should know if a message matches" do
|
10
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/)
|
11
|
+
mm.matches?('find_by_something').should be_true
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should not match if the message doesn't match" do
|
15
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/)
|
16
|
+
mm.matches?('not').should be_false
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should store a Proc" do
|
20
|
+
real_proc = Proc.new {}
|
21
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/,
|
22
|
+
:proc => real_proc)
|
23
|
+
mm.proc.should == real_proc
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should call its Proc when a message matches" do
|
27
|
+
mock_proc = Proc.new {}
|
28
|
+
object = Object.new
|
29
|
+
object.should_receive(:instance_exec).once.with('something')
|
30
|
+
|
31
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/,
|
32
|
+
:proc => mock_proc)
|
33
|
+
mm.match(object, 'find_by_something')
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should work with no groups" do
|
37
|
+
mock_proc = Proc.new {}
|
38
|
+
object = Object.new
|
39
|
+
object.should_receive(:instance_exec).once.with(no_args())
|
40
|
+
|
41
|
+
mm = MatchMethod.new(:matcher => /find_by_something/,
|
42
|
+
:proc => mock_proc)
|
43
|
+
mm.match(object, :find_by_something)
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should pass each group in as an argument" do
|
47
|
+
mock_proc = Proc.new {}
|
48
|
+
object = Object.new
|
49
|
+
object.should_receive(:instance_exec).once.with('something')
|
50
|
+
|
51
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/,
|
52
|
+
:proc => mock_proc)
|
53
|
+
mm.match(object, :find_by_something)
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should work with arguments, too" do
|
57
|
+
mock_proc = Proc.new {}
|
58
|
+
object = Object.new
|
59
|
+
object.should_receive(:instance_exec).once.with('argument')
|
60
|
+
|
61
|
+
mm = MatchMethod.new(:matcher => /find_by_something/,
|
62
|
+
:proc => mock_proc)
|
63
|
+
mm.match(object, :find_by_something, 'argument')
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should work with both match groups and arguments" do
|
67
|
+
mock_proc = Proc.new {}
|
68
|
+
object = Object.new
|
69
|
+
object.should_receive(:instance_exec).once.with('something', 'argument')
|
70
|
+
|
71
|
+
mm = MatchMethod.new(:matcher => /find_by_(\w+)/,
|
72
|
+
:proc => mock_proc)
|
73
|
+
mm.match(object, :find_by_something, 'argument')
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe MatchDef do
|
4
|
+
before(:each) do
|
5
|
+
Hippo.reset_meta_methods
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should provide a matches method on classes" do
|
9
|
+
lambda {
|
10
|
+
Hippo.class_eval do
|
11
|
+
matches /foo/
|
12
|
+
end
|
13
|
+
}.should_not raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should call the method if it matches" do
|
17
|
+
Hippo.class_eval do
|
18
|
+
matches /bar/ do
|
19
|
+
worked
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
test = Hippo.new
|
24
|
+
test.should_receive(:worked).once
|
25
|
+
test.bar
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should pass in the match groups" do
|
29
|
+
Hippo.class_eval do
|
30
|
+
matches /bar_(\w+)/ do |activity|
|
31
|
+
worked(activity)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
test = Hippo.new
|
36
|
+
test.should_receive(:worked).once.with('fight')
|
37
|
+
test.bar_fight()
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should respect an existing method_missing" do
|
41
|
+
Hippo.class_eval do
|
42
|
+
def method_missing(message, *args)
|
43
|
+
affirm(message)
|
44
|
+
end
|
45
|
+
|
46
|
+
matches /second/ do
|
47
|
+
affirm(:second)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
test = Hippo.new
|
52
|
+
test.should_receive(:affirm).once.with(:first)
|
53
|
+
test.should_receive(:affirm).once.with(:second)
|
54
|
+
test.first()
|
55
|
+
test.second()
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not pollute other classes" do
|
59
|
+
Hippo.class_eval do
|
60
|
+
matches /second/ do
|
61
|
+
throw "Should never be reached"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
test = Rhino.new
|
66
|
+
lambda { test.second() }.should raise_error
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
class Hippo
|
71
|
+
end
|
72
|
+
|
73
|
+
class Rhino
|
74
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: matches
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Phil Calvin
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-10-23 00:00:00 -04:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
type: :development
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 1.2.9
|
24
|
+
version:
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: cucumber
|
27
|
+
type: :development
|
28
|
+
version_requirement:
|
29
|
+
version_requirements: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: "0"
|
34
|
+
version:
|
35
|
+
description: |-
|
36
|
+
Matches allows you to define methods that have regular
|
37
|
+
expressions rather than names, and automatically
|
38
|
+
configires method_missing to handle them.
|
39
|
+
email: pncalvin@gmail.com
|
40
|
+
executables: []
|
41
|
+
|
42
|
+
extensions: []
|
43
|
+
|
44
|
+
extra_rdoc_files:
|
45
|
+
- LICENSE
|
46
|
+
- README.md
|
47
|
+
- README.rdoc
|
48
|
+
files:
|
49
|
+
- LICENSE
|
50
|
+
- README.md
|
51
|
+
- Rakefile
|
52
|
+
- VERSION
|
53
|
+
- features/define_matches.feature
|
54
|
+
- features/step_definitions/method_steps.rb
|
55
|
+
- features/support/env.rb
|
56
|
+
- lib/match_method.rb
|
57
|
+
- lib/matches.rb
|
58
|
+
- spec/match_method_spec.rb
|
59
|
+
- spec/matches_spec.rb
|
60
|
+
- spec/spec_helper.rb
|
61
|
+
- README.rdoc
|
62
|
+
has_rdoc: true
|
63
|
+
homepage: http://github.com/pnc/matches
|
64
|
+
licenses: []
|
65
|
+
|
66
|
+
post_install_message:
|
67
|
+
rdoc_options:
|
68
|
+
- --charset=UTF-8
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: "0"
|
82
|
+
version:
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.3.5
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: A DSL for defining regular-expression-based methods in Ruby.
|
90
|
+
test_files:
|
91
|
+
- spec/match_method_spec.rb
|
92
|
+
- spec/matches_spec.rb
|
93
|
+
- spec/spec_helper.rb
|