├── .gitignore ├── .rspec ├── .ruby-version ├── .travis.yml ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── lib ├── round_robin_tournament.rb └── round_robin_tournament │ └── version.rb ├── round_robin_tournament.gemspec └── spec ├── round_robin_tournament_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | *.gem -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.3 4 | - 2.0.0 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in round_robin_tournament.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Sebastien Saunier 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/ssaunier/round_robin_tournament.svg?branch=master)](https://travis-ci.org/ssaunier/round_robin_tournament) 2 | [![Gem Version](https://badge.fury.io/rb/round_robin_tournament.svg)](http://rubygems.org/gems/round_robin_tournament) 3 | 4 | # Round Robin Tournament 5 | 6 | This little ruby gem implements the [Round Robin Tournament](http://en.wikipedia.org/wiki/Round-robin_tournament#Scheduling_algorithm) scheduling. It is useful when you want a competition 7 | "in which each contestant meets all other contestants in turn", or if you have a classroom 8 | of students and want them to work in pairs, but with a different partner every day. 9 | 10 | ## Upgrade 11 | 12 | If you upgrade from version `0.0.1` to `0.1.2`, the tournament rotation has been reversed, which brings **stability** on first day pairs for a growing number for participants. 13 | 14 | ## Installation 15 | 16 | Add this line to your application's Gemfile: 17 | 18 | ```ruby 19 | gem 'round_robin_tournament' 20 | ``` 21 | 22 | And then execute: 23 | 24 | $ bundle 25 | 26 | ## Usage 27 | 28 | Call the method `RoundRobinTournament.schedule` with one argument, 29 | an `Array` of size `n`. It will return an array of `n - 1` elements, 30 | the days, and each element is an array of two elements, the game competitors. 31 | 32 | If `n` is off, then the first element of each array element will be 33 | an array of two element, the last one being `nil`. 34 | 35 | ## Example (`n` is even) 36 | 37 | ```ruby 38 | require "round_robin_tournament" 39 | 40 | # Compute all the possible teams for each day in the classroom 41 | students = %w(John Paul Ringo George) 42 | teams = RoundRobinTournament.schedule(students) 43 | 44 | # Print for each day, each team 45 | teams.each_with_index do |day, index| 46 | day_teams = day.map { |team| "(#{team.first}, #{team.last})" }.join(", ") 47 | puts "Day #{index + 1}: #{day_teams}" 48 | end 49 | 50 | # Day 1: (Paul, George), (Ringo, John) 51 | # Day 2: (Ringo, George), (John, Paul) 52 | # Day 3: (John, George), (Paul, Ringo) 53 | ``` 54 | 55 | ## Example (`n` is odd) 56 | 57 | ```ruby 58 | require "round_robin_tournament" 59 | 60 | # Compute all the possible teams for each day in the classroom 61 | students = %w(John Paul Ringo George OtherGuy) 62 | teams = RoundRobinTournament.schedule(students) 63 | 64 | # Print for each day, each team 65 | teams.each_with_index do |day, index| 66 | day_teams = day.map { |team| "(#{team.first}, #{team.last})" }.join(", ") 67 | puts "Day #{index + 1}: #{day_teams}" 68 | end 69 | 70 | # Day 1: (Paul, ), (Ringo, John), (George, OtherGuy) 71 | # Day 2: (Ringo, ), (George, Paul), (OtherGuy, John) 72 | # Day 3: (George, ), (OtherGuy, Ringo), (John, Paul) 73 | # Day 4: (OtherGuy, ), (John, George), (Paul, Ringo) 74 | # Day 5: (John, ), (Paul, OtherGuy), (Ringo, George) 75 | ``` 76 | 77 | ## Credits 78 | 79 | I'd like to thank [Pierre-Olivier Goffard](http://pierre-olivier.goffard.me/), 80 | Joel Cohen and Florent Rovetta from the [*Institut de Mathématiques de Marseille*](http://iml.univ-mrs.fr/) who helped a lot! 81 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task default: :spec 7 | -------------------------------------------------------------------------------- /lib/round_robin_tournament.rb: -------------------------------------------------------------------------------- 1 | require 'round_robin_tournament/version' 2 | 3 | module RoundRobinTournament 4 | 5 | def self.schedule(array) 6 | array.push(nil) if array.size.odd? 7 | n = array.size 8 | 1.step(n / 2, 1).each do |i| 9 | array.insert(n - i, array.delete_at(i)) 10 | end 11 | pivot = array.pop 12 | games = (n - 1).times.map do 13 | day = [[array.first, pivot]] + (1...(n / 2)).map { |j| [array[j], array[n - 1 - j]] } 14 | array.rotate! 15 | day 16 | end 17 | array.push pivot unless pivot.nil? 18 | games 19 | end 20 | 21 | end 22 | -------------------------------------------------------------------------------- /lib/round_robin_tournament/version.rb: -------------------------------------------------------------------------------- 1 | module RoundRobinTournament 2 | VERSION = '0.1.2' 3 | end 4 | -------------------------------------------------------------------------------- /round_robin_tournament.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'round_robin_tournament/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'round_robin_tournament' 8 | spec.version = RoundRobinTournament::VERSION 9 | spec.authors = ['Sebastien Saunier'] 10 | spec.email = ['seb@saunier.me'] 11 | spec.summary = 'Round Robin Tournament schedule for competitions or classroom teams' 12 | spec.description = spec.summary 13 | spec.homepage = 'https://github.com/ssaunier/round_robin_tournament' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '~> 2.4' 22 | spec.add_development_dependency 'rake', '~> 12.3' 23 | spec.add_development_dependency 'rspec' 24 | end 25 | -------------------------------------------------------------------------------- /spec/round_robin_tournament_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe RoundRobinTournament do 4 | it 'has a version number' do 5 | expect(RoundRobinTournament::VERSION).not_to be nil 6 | end 7 | 8 | describe '#schedule' do 9 | (2..20).to_a.each do |n| 10 | it "is correct for #{n} numbers" do 11 | schedule = RoundRobinTournament.schedule((1..n).to_a) 12 | expect { check_schedule!(schedule) }.not_to raise_error 13 | end 14 | end 15 | 16 | it 'works with array of any stuff' do 17 | Student = Struct.new(:name) 18 | students = %w(John Paul Ringo George).map { |beatle| Student.new beatle } 19 | 20 | teams = RoundRobinTournament.schedule(students) 21 | expect { check_schedule!(teams) }.not_to raise_error 22 | end 23 | 24 | (2..20).to_a.each do |n| 25 | it "is stable growing the number of participants from #{n} to #{n + 1}" do 26 | day_one = RoundRobinTournament.schedule(("a".."z").to_a.take(n)).first 27 | participants_day_two = ("a".."z").to_a.take(n + 1) 28 | participants_day_two_last = participants_day_two.last 29 | day_two = RoundRobinTournament.schedule(participants_day_two).first 30 | 31 | if n.even? 32 | day_two = day_two.reject { |pair| pair.compact.size != 2 } 33 | else 34 | day_one = day_one.map { |pair| pair.map { |participant| participant.nil? ? participants_day_two_last : participant } } 35 | end 36 | expect(day_two).to eq(day_one) 37 | end 38 | end 39 | end 40 | 41 | def check_schedule!(schedule) 42 | schedule.each_with_index do |games, day| 43 | games.each_with_index do |game, game_id| 44 | schedule.each_with_index do |other_games, other_day| 45 | next if day == other_day 46 | other_games.each_with_index do |other_game, other_game_id| 47 | fail if game_id != other_game_id && same_game?(game, other_game) 48 | end 49 | end 50 | end 51 | end 52 | end 53 | 54 | def same_game?(game, other_game) 55 | (game.first == other_game.first && game.last == other_game.last) || 56 | (game.last == other_game.first && game.first == other_game.last) 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | require 'round_robin_tournament' 3 | --------------------------------------------------------------------------------