matchy_matchy 0.1.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 +7 -0
- data/.circleci/config.yml +61 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.rubocop.yml +656 -0
- data/.rubocop_todo.yml +1 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +48 -0
- data/LICENSE.txt +21 -0
- data/README.md +118 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/matchy_matchy.rb +19 -0
- data/lib/matchy_matchy/candidate.rb +66 -0
- data/lib/matchy_matchy/match.rb +77 -0
- data/lib/matchy_matchy/match_list.rb +58 -0
- data/lib/matchy_matchy/match_results.rb +53 -0
- data/lib/matchy_matchy/matchbook.rb +92 -0
- data/lib/matchy_matchy/matchmaker.rb +90 -0
- data/lib/matchy_matchy/target.rb +17 -0
- data/lib/matchy_matchy/version.rb +3 -0
- data/matchy_matchy.gemspec +29 -0
- metadata +151 -0
@@ -0,0 +1,58 @@
|
|
1
|
+
module MatchyMatchy
|
2
|
+
# A sorted list of matches with a strict capacity.
|
3
|
+
class MatchList
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
# Initializes the list.
|
7
|
+
#
|
8
|
+
# @param capacity [Integer] The maximum number of matches this list can hold
|
9
|
+
def initialize(capacity = 1)
|
10
|
+
@matches = []
|
11
|
+
@capacity = capacity
|
12
|
+
end
|
13
|
+
|
14
|
+
# Pushes a match into the list.
|
15
|
+
# The list is re-sorted and any matches that don’t fit are rejected.
|
16
|
+
#
|
17
|
+
# @param match [MatchyMatchy::Match]
|
18
|
+
# @return [MatchyMatchy::MatchList] Self
|
19
|
+
def <<(match)
|
20
|
+
if include?(match)
|
21
|
+
match.reject!
|
22
|
+
else
|
23
|
+
@matches << match
|
24
|
+
@matches.sort!
|
25
|
+
@matches.pop.reject! if @matches.size > @capacity
|
26
|
+
end
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns true if the list contains the given match.
|
31
|
+
#
|
32
|
+
# @param match [MatchyMatchy::Match]
|
33
|
+
# @return [Boolean] True if the list contains this match already.
|
34
|
+
def include?(match)
|
35
|
+
any? { |m| m.eql?(match) }
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns an enumerator for the matches in the list.
|
39
|
+
# If a block is given, iterates through the matches in order, yielding them
|
40
|
+
# to the block.
|
41
|
+
#
|
42
|
+
# @yield [MatchyMatchy::Match]
|
43
|
+
# @return Enumerator
|
44
|
+
def each(&block)
|
45
|
+
return enum_for(:each) unless block_given?
|
46
|
+
to_a.each(&block)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns an array of the matches in the list.
|
50
|
+
#
|
51
|
+
# @return [Array]
|
52
|
+
def to_a
|
53
|
+
@matches.dup.freeze
|
54
|
+
end
|
55
|
+
|
56
|
+
alias to_ary to_a
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module MatchyMatchy
|
2
|
+
# An object representing the results of the stable match algorithm.
|
3
|
+
class MatchResults
|
4
|
+
# A list of match targets.
|
5
|
+
attr_reader :targets
|
6
|
+
|
7
|
+
# A list of match candidates.
|
8
|
+
attr_reader :candidates
|
9
|
+
|
10
|
+
# Initializes the match results with an empty list of matches.
|
11
|
+
#
|
12
|
+
# @param targets [Array<MatchyMatchy::Target>] Array of all possible targets
|
13
|
+
# @param candidates [Array<MatchyMatchy::Candidate>
|
14
|
+
# Array of all possible candidates
|
15
|
+
def initialize(targets:, candidates:)
|
16
|
+
@targets = targets
|
17
|
+
@candidates = candidates
|
18
|
+
@matches = targets.map { |t| [t, MatchList.new(t.capacity)] }.to_h
|
19
|
+
end
|
20
|
+
|
21
|
+
# Adds a match to the results.
|
22
|
+
#
|
23
|
+
# @param match [MatchyMatchy::Match] A match to add
|
24
|
+
# @return [MatchyMatchy::MatchResults] Self
|
25
|
+
def <<(match)
|
26
|
+
@matches[match.target] << match
|
27
|
+
self
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns a hash where the keys are the targets in the match, and the
|
31
|
+
# values are an ordered list of candidates for each target (if any).
|
32
|
+
# Targets are included even if no candidates could be matched there.
|
33
|
+
#
|
34
|
+
# @return [Hash<MatchyMatchy::Target, Array<MatchyMatchy::Candidate>>]
|
35
|
+
def by_target
|
36
|
+
targets.
|
37
|
+
map { |t| [t.object, @matches[t].map { |m| m.candidate.object }] }.
|
38
|
+
to_h.
|
39
|
+
freeze
|
40
|
+
end
|
41
|
+
|
42
|
+
# Returns a hash where the keys are the candidates in the match, and the
|
43
|
+
# values are the targets selected for each candidate (or nil, if the
|
44
|
+
# algorithm was unable to place the candidate).
|
45
|
+
#
|
46
|
+
# @return [Hash<MatchyMatchy::Target, Array<MatchyMatchy::Candidate>>]
|
47
|
+
def by_candidate
|
48
|
+
placements =
|
49
|
+
by_target.to_a.flat_map { |t, cs| cs.map { |c| [c, t] } }.to_h
|
50
|
+
candidates.map { |c| [c.object, placements[c.object]] }.to_h.freeze
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
module MatchyMatchy
|
2
|
+
# Small class to maintain a cache of candidates and targets, for use by the
|
3
|
+
# +MatchMaker+. Ensures the following conditions:
|
4
|
+
#
|
5
|
+
# * Candidates and targets are kept separate, even if the objects they wrap
|
6
|
+
# are identical
|
7
|
+
# * Exactly one +Candidate+ or +Target+ instance is created for each object
|
8
|
+
# considered by the +MatchMaker+
|
9
|
+
# * Objects are not double-wrapped
|
10
|
+
class Matchbook
|
11
|
+
# Initializes the Matchbook with an empty cache.
|
12
|
+
def initialize
|
13
|
+
@targets = {}
|
14
|
+
@candidates = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Builds a list of targets for the +MatchMaker+.
|
18
|
+
# The parameter is a hash of unwrapped target objects to their preferred
|
19
|
+
# candidates and maximum capacity (a 2-tuple expressed as an array):
|
20
|
+
#
|
21
|
+
# @example With explicit capacities
|
22
|
+
# matchbook.build_targets(
|
23
|
+
# 'Gryffindor' => [['Hermione', 'Ron', 'Harry'], 2],
|
24
|
+
# 'Ravenclaw' => [['Hermione'], 1],
|
25
|
+
# 'Hufflepuff' => [['Neville', 'Hermione'], 2],
|
26
|
+
# 'Slytherin' => [['Harry', 'Hermione', 'Ron', 'Neville'], 4]
|
27
|
+
# )
|
28
|
+
#
|
29
|
+
# The capacity may also be omitted, in which case it defaults to 1:
|
30
|
+
#
|
31
|
+
# @example With explicit capacities
|
32
|
+
# matchbook.build_targets(
|
33
|
+
# 'Gryffindor' => ['Hermione', 'Ron', 'Harry'],
|
34
|
+
# 'Ravenclaw' => ['Hermione'],
|
35
|
+
# 'Hufflepuff' => ['Neville', 'Hermione'],
|
36
|
+
# 'Slytherin' => ['Harry', 'Hermione', 'Ron', 'Neville'],
|
37
|
+
# )
|
38
|
+
#
|
39
|
+
# @param targets [Hash<Object, Array<Object>>]
|
40
|
+
# Hash of target objects to a list of their preferred candidates
|
41
|
+
def build_targets(targets)
|
42
|
+
targets.to_a.map do |object, row|
|
43
|
+
preferences, capacity = parse_target_row(row)
|
44
|
+
target(object).tap do |t|
|
45
|
+
t.capacity = capacity
|
46
|
+
t.prefer(*preferences.map { |c| candidate(c) })
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Builds a list of candidates for the +MatchMaker+.
|
52
|
+
# The parameter is a hash of unwrapped canditate objects to their preferred
|
53
|
+
# targets.
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# matchbook.build_candidates(
|
57
|
+
# 'Harry' => ['Gryffindor', 'Slytherin'],
|
58
|
+
# 'Hermione' => ['Ravenclaw', 'Gryffindor'],
|
59
|
+
# 'Ron' => ['Gryffindor'],
|
60
|
+
# 'Neville' => ['Hufflepuff', 'Gryffindor', 'Ravenclaw', 'Slytherin']
|
61
|
+
# )
|
62
|
+
#
|
63
|
+
# @param candidates [Hash<Object, Array<Object>>]
|
64
|
+
# Hash of candidate objects to a list of their preferred targets
|
65
|
+
def build_candidates(candidates)
|
66
|
+
candidates.to_a.map do |object, preferences|
|
67
|
+
candidate(object).tap do |c|
|
68
|
+
c.prefer(*preferences.map { |t| target(t) })
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
def target(object)
|
76
|
+
return object if object.is_a?(Target)
|
77
|
+
@targets[object] ||= Target.new(object)
|
78
|
+
end
|
79
|
+
|
80
|
+
def candidate(object)
|
81
|
+
return object if object.is_a?(Candidate)
|
82
|
+
@candidates[object] ||= Candidate.new(object)
|
83
|
+
end
|
84
|
+
|
85
|
+
def parse_target_row(row)
|
86
|
+
return row if row.size == 2 &&
|
87
|
+
row.first.respond_to?(:map) &&
|
88
|
+
row.last.respond_to?(:zero?)
|
89
|
+
[row, 1]
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module MatchyMatchy
|
2
|
+
# Implements the Stable Match algorithm ovver a given set of candidates and
|
3
|
+
# targets.
|
4
|
+
class MatchMaker
|
5
|
+
# Initializes the MatchMaker.
|
6
|
+
# You must specify a list of +targets+ (objects “offering” places) and
|
7
|
+
# +candidates+ (objects “seeking” places). These can be any type of object:
|
8
|
+
# they are wrapped internally, but you should never have to deal with that.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# match_maker = MatchyMatchy::MatchMaker.new(
|
12
|
+
# targets: {
|
13
|
+
# 'Gryffindor' => [['Hermione', 'Ron', 'Harry'], 2],
|
14
|
+
# 'Ravenclaw' => [['Hermione'], 1],
|
15
|
+
# 'Hufflepuff' => [['Neville', 'Hermione'], 2],
|
16
|
+
# 'Slytherin' => [['Harry', 'Hermione', 'Ron', 'Neville'], 4]
|
17
|
+
# },
|
18
|
+
# candidates: {
|
19
|
+
# 'Harry' => ['Gryffindor', 'Slytherin'],
|
20
|
+
# 'Hermione' => ['Ravenclaw', 'Gryffindor'],
|
21
|
+
# 'Ron' => ['Gryffindor'],
|
22
|
+
# 'Neville' => ['Hufflepuff', 'Gryffindor', 'Ravenclaw', 'Slytherin']
|
23
|
+
# }
|
24
|
+
# )
|
25
|
+
#
|
26
|
+
# The +target+ and +candidate+ parameters look similar, but the values in
|
27
|
+
# the +target+ hash have an extra number, which is the maximum capacity of
|
28
|
+
# that target (the total number of candidates it can accept). The capacity
|
29
|
+
# may also be omitted, in which case it defaults to 1; that is:
|
30
|
+
#
|
31
|
+
# {
|
32
|
+
# 'Gryffindor' => ['Hermione', 'Ron', 'Harry'],
|
33
|
+
# 'Ravenclaw' => ['Hermione'],
|
34
|
+
# 'Hufflepuff' => ['Neville', 'Hermione'],
|
35
|
+
# 'Slytherin' => ['Harry', 'Hermione', 'Ron', 'Neville'],
|
36
|
+
# }
|
37
|
+
#
|
38
|
+
# ...is equivalent to:
|
39
|
+
#
|
40
|
+
# {
|
41
|
+
# 'Gryffindor' => [['Hermione', 'Ron', 'Harry'], 1],
|
42
|
+
# 'Ravenclaw' => [['Hermione'], 1],
|
43
|
+
# 'Hufflepuff' => [['Neville', 'Hermione'], 1],
|
44
|
+
# 'Slytherin' => [['Harry', 'Hermione', 'Ron', 'Neville'], 1]
|
45
|
+
# }
|
46
|
+
def initialize(targets:, candidates:)
|
47
|
+
@matchbook = Matchbook.new
|
48
|
+
@targets = matchbook.build_targets(targets)
|
49
|
+
@candidates = matchbook.build_candidates(candidates)
|
50
|
+
@matches = MatchResults.new(targets: @targets, candidates: @candidates)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Kicks off the match.
|
54
|
+
# Iterates through the candidates in order, proposing each to their
|
55
|
+
# first-choice target. If this match is rejected, the candidate is proposed
|
56
|
+
# to their next choice of candidate (if any).
|
57
|
+
#
|
58
|
+
# The running time of the algorithm is proportional to the number of
|
59
|
+
# candidates and targets in the system, but is guaranteed to finish and
|
60
|
+
# yield the same result for the same input.
|
61
|
+
#
|
62
|
+
# @return [MatchyMatchy::MatchResults] A +MatchResults+ object representing
|
63
|
+
# the outcome of the algorithm.
|
64
|
+
def perform
|
65
|
+
candidates.each { |candidate| propose(candidate) }
|
66
|
+
@matches
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
attr_reader :matchbook, :targets, :candidates
|
72
|
+
|
73
|
+
def propose(candidate, index = 0)
|
74
|
+
return unless index < candidate.preferences.size
|
75
|
+
|
76
|
+
proposed = match(candidate, index)
|
77
|
+
if proposed.mutual?
|
78
|
+
@matches << proposed
|
79
|
+
else
|
80
|
+
proposed.reject!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def match(candidate, index)
|
85
|
+
Match.
|
86
|
+
new(candidate: candidate, index: index).
|
87
|
+
on(:reject) { propose(candidate, index + 1) }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module MatchyMatchy
|
2
|
+
# Represents a target in the Stable Match algorithm.
|
3
|
+
# A target is largely the same as a candidate, with the addition of the
|
4
|
+
# concept of “capacity”: how many candidates the target is willing to accept.
|
5
|
+
class Target < Candidate
|
6
|
+
attr_accessor :capacity
|
7
|
+
|
8
|
+
# Initializes the target with an object to wrap, and a capacity.
|
9
|
+
#
|
10
|
+
# @param object [Object] The object represented by this target
|
11
|
+
# @param capacity [Integer] The target’s maximum capacity
|
12
|
+
def initialize(object, capacity: 1)
|
13
|
+
super(object)
|
14
|
+
@capacity = capacity
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'matchy_matchy/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'matchy_matchy'
|
7
|
+
spec.version = MatchyMatchy::VERSION
|
8
|
+
spec.authors = ['Matt Powell']
|
9
|
+
spec.email = ['fauxparse@gmail.com']
|
10
|
+
|
11
|
+
spec.summary = 'A cute Stable Match implementation'
|
12
|
+
spec.homepage = 'https://github.com/fauxparse/matchy_matchy'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
16
|
+
f.match(%r{^(test|spec|features)/})
|
17
|
+
end
|
18
|
+
spec.bindir = 'exe'
|
19
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ['lib']
|
21
|
+
|
22
|
+
spec.add_dependency 'cry', '~> 0.1'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.16.a'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
27
|
+
spec.add_development_dependency 'rspec_junit_formatter', '~> 0.4.1'
|
28
|
+
spec.add_development_dependency 'simplecov'
|
29
|
+
end
|
metadata
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: matchy_matchy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Matt Powell
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-09-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: cry
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.1'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.1'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.16.a
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.16.a
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '3.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec_junit_formatter
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 0.4.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.4.1
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: simplecov
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description:
|
98
|
+
email:
|
99
|
+
- fauxparse@gmail.com
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- ".circleci/config.yml"
|
105
|
+
- ".gitignore"
|
106
|
+
- ".rspec"
|
107
|
+
- ".rubocop.yml"
|
108
|
+
- ".rubocop_todo.yml"
|
109
|
+
- CODE_OF_CONDUCT.md
|
110
|
+
- Gemfile
|
111
|
+
- Gemfile.lock
|
112
|
+
- LICENSE.txt
|
113
|
+
- README.md
|
114
|
+
- Rakefile
|
115
|
+
- bin/console
|
116
|
+
- bin/setup
|
117
|
+
- lib/matchy_matchy.rb
|
118
|
+
- lib/matchy_matchy/candidate.rb
|
119
|
+
- lib/matchy_matchy/match.rb
|
120
|
+
- lib/matchy_matchy/match_list.rb
|
121
|
+
- lib/matchy_matchy/match_results.rb
|
122
|
+
- lib/matchy_matchy/matchbook.rb
|
123
|
+
- lib/matchy_matchy/matchmaker.rb
|
124
|
+
- lib/matchy_matchy/target.rb
|
125
|
+
- lib/matchy_matchy/version.rb
|
126
|
+
- matchy_matchy.gemspec
|
127
|
+
homepage: https://github.com/fauxparse/matchy_matchy
|
128
|
+
licenses:
|
129
|
+
- MIT
|
130
|
+
metadata: {}
|
131
|
+
post_install_message:
|
132
|
+
rdoc_options: []
|
133
|
+
require_paths:
|
134
|
+
- lib
|
135
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
requirements: []
|
146
|
+
rubyforge_project:
|
147
|
+
rubygems_version: 2.5.2.3
|
148
|
+
signing_key:
|
149
|
+
specification_version: 4
|
150
|
+
summary: A cute Stable Match implementation
|
151
|
+
test_files: []
|