Compare commits
1 Commits
d511103596
...
4725ac40b9
Author | SHA1 | Date | |
---|---|---|---|
![]() |
4725ac40b9 |
4
Gemfile
4
Gemfile
@ -50,7 +50,6 @@ group :development, :test do
|
|||||||
gem 'rspec-rails', '~> 6.1.0'
|
gem 'rspec-rails', '~> 6.1.0'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'pry'
|
gem 'pry'
|
||||||
gem "factory_bot_rails"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development do
|
group :development do
|
||||||
@ -66,5 +65,4 @@ end
|
|||||||
|
|
||||||
gem "money"
|
gem "money"
|
||||||
gem 'acts-as-taggable-on'
|
gem 'acts-as-taggable-on'
|
||||||
|
gem "rubytree"
|
||||||
gem "rubytree"
|
|
@ -94,12 +94,8 @@ GEM
|
|||||||
diff-lcs (1.5.1)
|
diff-lcs (1.5.1)
|
||||||
drb (2.2.1)
|
drb (2.2.1)
|
||||||
erubi (1.13.0)
|
erubi (1.13.0)
|
||||||
factory_bot (6.4.6)
|
faker (3.4.2)
|
||||||
activesupport (>= 5.0.0)
|
i18n (>= 1.8.11, < 2)
|
||||||
factory_bot_rails (6.4.3)
|
|
||||||
factory_bot (~> 6.4)
|
|
||||||
railties (>= 5.0.0)
|
|
||||||
faker (3.1.1)
|
|
||||||
globalid (1.2.1)
|
globalid (1.2.1)
|
||||||
activesupport (>= 6.1)
|
activesupport (>= 6.1)
|
||||||
i18n (1.14.5)
|
i18n (1.14.5)
|
||||||
@ -269,7 +265,6 @@ DEPENDENCIES
|
|||||||
acts-as-taggable-on
|
acts-as-taggable-on
|
||||||
bootsnap
|
bootsnap
|
||||||
debug
|
debug
|
||||||
factory_bot_rails
|
|
||||||
faker
|
faker
|
||||||
importmap-rails
|
importmap-rails
|
||||||
jbuilder
|
jbuilder
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
class TablesArrangementsController < ApplicationController
|
|
||||||
def index
|
|
||||||
@tables_arrangements = TablesArrangement.all.order(discomfort: :asc).limit(10)
|
|
||||||
end
|
|
||||||
|
|
||||||
def show
|
|
||||||
@tables_arrangement = TablesArrangement.find(params[:id])
|
|
||||||
@seats = @tables_arrangement.seats.includes(:guest).group_by(&:table_number)
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,2 +0,0 @@
|
|||||||
module TablesArrangementsHelper
|
|
||||||
end
|
|
@ -1,4 +0,0 @@
|
|||||||
class Seat < ApplicationRecord
|
|
||||||
belongs_to :guest
|
|
||||||
belongs_to :table_arrangement
|
|
||||||
end
|
|
@ -1,3 +0,0 @@
|
|||||||
class TablesArrangement < ApplicationRecord
|
|
||||||
has_many :seats
|
|
||||||
end
|
|
@ -1,25 +0,0 @@
|
|||||||
module Tables
|
|
||||||
class DiscomfortCalculator
|
|
||||||
private attr_reader :table
|
|
||||||
def initialize(table)
|
|
||||||
@table = table
|
|
||||||
end
|
|
||||||
|
|
||||||
def calculate
|
|
||||||
cohesion_penalty
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def cohesion_penalty
|
|
||||||
table.map { |guest| guest.affinity_group_list.first }.tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
|
|
||||||
distance = AffinityGroupsHierarchy.instance.distance(a, b)
|
|
||||||
|
|
||||||
next count_a * count_b if distance.nil?
|
|
||||||
next 0 if distance.zero?
|
|
||||||
|
|
||||||
count_a * count_b * Rational(distance, distance + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,65 +0,0 @@
|
|||||||
require_relative '../../extensions/tree_node_extension'
|
|
||||||
|
|
||||||
module Tables
|
|
||||||
class Distribution
|
|
||||||
attr_accessor :tables
|
|
||||||
|
|
||||||
def initialize(min_per_table:, max_per_table:)
|
|
||||||
@min_per_table = min_per_table
|
|
||||||
@max_per_table = max_per_table
|
|
||||||
@tables = []
|
|
||||||
end
|
|
||||||
|
|
||||||
def random_distribution(people)
|
|
||||||
@tables = []
|
|
||||||
|
|
||||||
@tables << people.slice!(0..rand(@min_per_table..@max_per_table)) while people.any?
|
|
||||||
end
|
|
||||||
|
|
||||||
def discomfort
|
|
||||||
@tables.map do |table|
|
|
||||||
local_discomfort(table)
|
|
||||||
end.sum
|
|
||||||
end
|
|
||||||
|
|
||||||
def inspect
|
|
||||||
"#{@tables.count} tables, discomfort: #{discomfort}"
|
|
||||||
end
|
|
||||||
|
|
||||||
def pretty_print
|
|
||||||
@tables.map.with_index do |table, i|
|
|
||||||
"Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:full_name).join(', ')}"
|
|
||||||
end.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def deep_dup
|
|
||||||
self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution|
|
|
||||||
new_distribution.tables = @tables.map(&:dup)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
def save!
|
|
||||||
ActiveRecord::Base.transaction do
|
|
||||||
arrangement = TablesArrangement.create!
|
|
||||||
|
|
||||||
records_to_store = []
|
|
||||||
|
|
||||||
tables.each_with_index do |table, table_number|
|
|
||||||
table.each do |person|
|
|
||||||
records_to_store << { guest_id: person.id, tables_arrangement_id: arrangement.id, table_number: }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
Seat.insert_all!(records_to_store)
|
|
||||||
|
|
||||||
arrangement.update!(discomfort:)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def local_discomfort(table)
|
|
||||||
DiscomfortCalculator.new(table).calculate
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,28 +0,0 @@
|
|||||||
module Tables
|
|
||||||
class Swap
|
|
||||||
private attr_reader :initial_solution
|
|
||||||
def initialize(initial_solution)
|
|
||||||
@initial_solution = initial_solution
|
|
||||||
end
|
|
||||||
|
|
||||||
def each
|
|
||||||
@initial_solution.tables.combination(2) do |table_a, table_b|
|
|
||||||
table_a.product(table_b).each do |(person_a, person_b)|
|
|
||||||
table_a.delete(person_a)
|
|
||||||
table_b.delete(person_b)
|
|
||||||
|
|
||||||
table_a << person_b
|
|
||||||
table_b << person_a
|
|
||||||
|
|
||||||
yield(@initial_solution)
|
|
||||||
ensure
|
|
||||||
table_a.delete(person_b)
|
|
||||||
table_b.delete(person_a)
|
|
||||||
|
|
||||||
table_a << person_a
|
|
||||||
table_b << person_b
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,48 +0,0 @@
|
|||||||
module VNS
|
|
||||||
class Engine
|
|
||||||
def target_function(&function)
|
|
||||||
@target_function = function
|
|
||||||
end
|
|
||||||
|
|
||||||
def add_perturbation(klass)
|
|
||||||
@perturbations ||= Set.new
|
|
||||||
@perturbations << klass
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_writer :initial_solution
|
|
||||||
|
|
||||||
def run
|
|
||||||
raise 'No target function defined' unless @target_function
|
|
||||||
raise 'No perturbations defined' unless @perturbations
|
|
||||||
raise 'No initial solution defined' unless @initial_solution
|
|
||||||
|
|
||||||
@best_solution = @initial_solution
|
|
||||||
@best_score = @target_function.call(@best_solution)
|
|
||||||
|
|
||||||
puts "Initial score: #{@best_score.to_f}"
|
|
||||||
|
|
||||||
@perturbations.each do |perturbation|
|
|
||||||
puts "Running perturbation: #{perturbation.name}"
|
|
||||||
optimize(perturbation.new(@best_solution))
|
|
||||||
end
|
|
||||||
|
|
||||||
@best_solution
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def optimize(perturbation)
|
|
||||||
perturbation.each do |alternative_solution|
|
|
||||||
score = @target_function.call(alternative_solution)
|
|
||||||
next if score >= @best_score
|
|
||||||
|
|
||||||
@best_solution = alternative_solution.deep_dup
|
|
||||||
@best_score = score
|
|
||||||
|
|
||||||
puts "New lowest score: #{@best_score.to_f}"
|
|
||||||
|
|
||||||
return optimize(perturbation.class.new(@best_solution))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,9 +0,0 @@
|
|||||||
<h1>Tables arrangements</h1>
|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<% @tables_arrangements.each_with_index do |tables_arrangement, i| %>
|
|
||||||
<li>
|
|
||||||
<p><%= link_to "Arrangement ##{i+1}", tables_arrangement_path(tables_arrangement) %> Discomfort: <%= tables_arrangement.discomfort %></p>
|
|
||||||
</li>
|
|
||||||
<% end %>
|
|
||||||
</ol>
|
|
@ -1,16 +0,0 @@
|
|||||||
<h1>ID: <%= @tables_arrangement.id %></h1>
|
|
||||||
|
|
||||||
<p>Discomfort: <%= @tables_arrangement.discomfort %></p>
|
|
||||||
|
|
||||||
<h2>Seats</h2>
|
|
||||||
|
|
||||||
<% @seats.each do |table_number, seats| %>
|
|
||||||
|
|
||||||
<h3>Table <%= table_number %></h3>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<% seats.each do |seat| %>
|
|
||||||
<li><%= seat.guest.full_name %></li>
|
|
||||||
<% end %>
|
|
||||||
</ul>
|
|
||||||
<% end %>
|
|
@ -11,6 +11,6 @@
|
|||||||
# end
|
# end
|
||||||
|
|
||||||
# These inflection rules are supported but not enabled by default:
|
# These inflection rules are supported but not enabled by default:
|
||||||
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||||
inflect.acronym 'VNS'
|
# inflect.acronym "RESTful"
|
||||||
end
|
# end
|
||||||
|
@ -3,7 +3,12 @@ Rails.application.routes.draw do
|
|||||||
post :import, on: :collection
|
post :import, on: :collection
|
||||||
end
|
end
|
||||||
resources :expenses
|
resources :expenses
|
||||||
resources :tables_arrangements, only: [:index, :show]
|
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||||
|
|
||||||
get 'up' => 'rails/health#show', as: :rails_health_check
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||||
|
get "up" => "rails/health#show", as: :rails_health_check
|
||||||
|
|
||||||
|
# Defines the root path route ("/")
|
||||||
|
# root "posts#index"
|
||||||
end
|
end
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
class CreateTablesArrangements < ActiveRecord::Migration[7.1]
|
|
||||||
def change
|
|
||||||
create_table :tables_arrangements, id: :uuid do |t|
|
|
||||||
t.integer :discomfort
|
|
||||||
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,13 +0,0 @@
|
|||||||
class CreateSeats < ActiveRecord::Migration[7.1]
|
|
||||||
def change
|
|
||||||
create_table :seats, id: :uuid do |t|
|
|
||||||
t.references :guest, null: false, foreign_key: true, type: :uuid
|
|
||||||
t.references :tables_arrangement, null: false, type: :uuid
|
|
||||||
t.integer :table_number
|
|
||||||
|
|
||||||
t.timestamps
|
|
||||||
end
|
|
||||||
|
|
||||||
add_foreign_key :seats, :tables_arrangements, on_delete: :cascade
|
|
||||||
end
|
|
||||||
end
|
|
20
db/schema.rb
generated
20
db/schema.rb
generated
@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do
|
ActiveRecord::Schema[7.1].define(version: 2024_07_11_181632) do
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
|
|
||||||
@ -35,22 +35,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do
|
|||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "seats", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
|
||||||
t.uuid "guest_id", null: false
|
|
||||||
t.uuid "tables_arrangement_id", null: false
|
|
||||||
t.integer "table_number"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
t.index ["guest_id"], name: "index_seats_on_guest_id"
|
|
||||||
t.index ["tables_arrangement_id"], name: "index_seats_on_tables_arrangement_id"
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "tables_arrangements", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
|
|
||||||
t.integer "discomfort"
|
|
||||||
t.datetime "created_at", null: false
|
|
||||||
t.datetime "updated_at", null: false
|
|
||||||
end
|
|
||||||
|
|
||||||
create_table "taggings", force: :cascade do |t|
|
create_table "taggings", force: :cascade do |t|
|
||||||
t.bigint "tag_id"
|
t.bigint "tag_id"
|
||||||
t.string "taggable_type"
|
t.string "taggable_type"
|
||||||
@ -82,7 +66,5 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do
|
|||||||
t.index ["name"], name: "index_tags_on_name", unique: true
|
t.index ["name"], name: "index_tags_on_name", unique: true
|
||||||
end
|
end
|
||||||
|
|
||||||
add_foreign_key "seats", "guests"
|
|
||||||
add_foreign_key "seats", "tables_arrangements", on_delete: :cascade
|
|
||||||
add_foreign_key "taggings", "tags"
|
add_foreign_key "taggings", "tags"
|
||||||
end
|
end
|
||||||
|
13
db/seeds.rb
13
db/seeds.rb
@ -1,6 +1,13 @@
|
|||||||
NUMBER_OF_GUESTS = 50
|
# This file should ensure the existence of records required to run the application in every environment (production,
|
||||||
|
# development, test). The code here should be idempotent so that it can be executed at any point in every environment.
|
||||||
|
# The data can then be loaded with the bin/rails db:seed command (or created alongside the database with db:setup).
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
#
|
||||||
|
# ["Action", "Comedy", "Drama", "Horror"].each do |genre_name|
|
||||||
|
# MovieGenre.find_or_create_by!(name: genre_name)
|
||||||
|
# end
|
||||||
|
|
||||||
TablesArrangement.delete_all
|
|
||||||
Expense.delete_all
|
Expense.delete_all
|
||||||
Guest.delete_all
|
Guest.delete_all
|
||||||
ActsAsTaggableOn::Tagging.delete_all
|
ActsAsTaggableOn::Tagging.delete_all
|
||||||
@ -46,7 +53,7 @@ samples = {
|
|||||||
count.times { acc << affinity_group }
|
count.times { acc << affinity_group }
|
||||||
end
|
end
|
||||||
|
|
||||||
NUMBER_OF_GUESTS.times do
|
300.times do
|
||||||
guest = Guest.create!(first_name: Faker::Name.first_name,
|
guest = Guest.create!(first_name: Faker::Name.first_name,
|
||||||
last_name: Faker::Name.last_name,
|
last_name: Faker::Name.last_name,
|
||||||
email: Faker::Internet.email,
|
email: Faker::Internet.email,
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
namespace :vns do
|
|
||||||
task distribute_tables: :environment do
|
|
||||||
engine = VNS::Engine.new
|
|
||||||
|
|
||||||
engine.add_perturbation(Tables::Swap)
|
|
||||||
|
|
||||||
initial_solution = Tables::Distribution.new(min_per_table: 8, max_per_table: 10)
|
|
||||||
initial_solution.random_distribution(Guest.all.shuffle)
|
|
||||||
|
|
||||||
engine.initial_solution = initial_solution
|
|
||||||
|
|
||||||
engine.target_function(&:discomfort)
|
|
||||||
|
|
||||||
best_solution = engine.run
|
|
||||||
|
|
||||||
best_solution.save!
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,8 +0,0 @@
|
|||||||
FactoryBot.define do
|
|
||||||
factory :guest do
|
|
||||||
first_name { Faker::Name.first_name }
|
|
||||||
last_name { Faker::Name.last_name }
|
|
||||||
email { Faker::Internet.email }
|
|
||||||
phone { Faker::PhoneNumber.cell_phone }
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,5 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe Seat, type: :model do
|
|
||||||
pending "add some examples to (or delete) #{__FILE__}"
|
|
||||||
end
|
|
@ -1,5 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe TablesArrangement, type: :model do
|
|
||||||
pending "add some examples to (or delete) #{__FILE__}"
|
|
||||||
end
|
|
@ -62,5 +62,4 @@ RSpec.configure do |config|
|
|||||||
config.filter_rails_from_backtrace!
|
config.filter_rails_from_backtrace!
|
||||||
# arbitrary gems may also be filtered via:
|
# arbitrary gems may also be filtered via:
|
||||||
# config.filter_gems_from_backtrace("gem name")
|
# config.filter_gems_from_backtrace("gem name")
|
||||||
config.include FactoryBot::Syntax::Methods
|
|
||||||
end
|
end
|
||||||
|
@ -1,158 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
module Tables
|
|
||||||
RSpec.describe DiscomfortCalculator do
|
|
||||||
let(:calculator) { described_class.new(table) }
|
|
||||||
|
|
||||||
describe '#cohesion_penalty' do
|
|
||||||
before do
|
|
||||||
# Overridden in each test except trivial cases
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).and_call_original
|
|
||||||
|
|
||||||
%w[family friends work school].each do |group|
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(group, group).and_return(0)
|
|
||||||
end
|
|
||||||
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'friends').and_return(nil)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'work').and_return(1)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'work').and_return(2)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('family', 'school').and_return(3)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('friends', 'school').and_return(4)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with('work', 'school').and_return(5)
|
|
||||||
end
|
|
||||||
context 'when the table contains just two guests' do
|
|
||||||
context 'when they belong to the same group' do
|
|
||||||
let(:table) { create_list(:guest, 2, affinity_group_list: ['family']) }
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_penalty)).to eq(0) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to completely unrelated groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['family']),
|
|
||||||
create(:guest, affinity_group_list: ['friends'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
it { expect(calculator.send(:cohesion_penalty)).to eq(1) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 1' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['friends']),
|
|
||||||
create(:guest, affinity_group_list: ['work'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_penalty)).to eq(0.5) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 2' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['family']),
|
|
||||||
create(:guest, affinity_group_list: ['work'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_penalty)).to eq(Rational(2, 3)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 3' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['family']),
|
|
||||||
create(:guest, affinity_group_list: ['school'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_penalty)).to eq(Rational(3, 4)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains three guests' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['family']),
|
|
||||||
create(:guest, affinity_group_list: ['friends']),
|
|
||||||
create(:guest, affinity_group_list: ['work'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty)).to eq(1 + Rational(1, 2) + Rational(2, 3))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains four guests of different groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, affinity_group_list: ['family']),
|
|
||||||
create(:guest, affinity_group_list: ['friends']),
|
|
||||||
create(:guest, affinity_group_list: ['work']),
|
|
||||||
create(:guest, affinity_group_list: ['school'])
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty))
|
|
||||||
.to eq(1 + Rational(1, 2) + Rational(2, 3) + Rational(3, 4) + Rational(4, 5) + Rational(5, 6))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains four guests of two evenly split groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['family']),
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['friends'])
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty)).to eq(4)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains six guests of two unevenly split groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['family']),
|
|
||||||
create_list(:guest, 4, affinity_group_list: ['friends'])
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty)).to eq(8)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains six guests of three evenly split groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['family']),
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['friends']),
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['work'])
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty)).to eq(4 * 1 + 4 * Rational(1, 2) + 4 * Rational(2, 3))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains six guests of three unevenly split groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create_list(:guest, 3, affinity_group_list: ['family']),
|
|
||||||
create_list(:guest, 2, affinity_group_list: ['friends']),
|
|
||||||
create_list(:guest, 1, affinity_group_list: ['work'])
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_penalty)).to eq(6 * 1 + 2 * Rational(1, 2) + 3 * Rational(2, 3))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,83 +0,0 @@
|
|||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
module Tables
|
|
||||||
RSpec.describe Swap do
|
|
||||||
describe '#each' do
|
|
||||||
let(:swaps) do
|
|
||||||
acc = []
|
|
||||||
described_class.new(initial_solution).each do |solution|
|
|
||||||
acc << solution.tables.map(&:dup)
|
|
||||||
end
|
|
||||||
acc
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are two tables with two people each' do
|
|
||||||
let(:initial_solution) do
|
|
||||||
Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution|
|
|
||||||
distribution.tables << %i[a b]
|
|
||||||
distribution.tables << %i[c d]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'yields all possible swaps between the tables' do
|
|
||||||
expect(swaps).to contain_exactly(
|
|
||||||
[%i[a d], %i[c b]],
|
|
||||||
[%i[b c], %i[d a]],
|
|
||||||
[%i[a c], %i[d b]],
|
|
||||||
[%i[b d], %i[c a]]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are two tables with three people each' do
|
|
||||||
let(:initial_solution) do
|
|
||||||
Distribution.new(min_per_table: 3, max_per_table: 3).tap do |distribution|
|
|
||||||
distribution.tables << %i[a b c]
|
|
||||||
distribution.tables << %i[d e f]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'yields all possible swaps between the tables' do
|
|
||||||
expect(swaps).to contain_exactly(
|
|
||||||
[%i[b c d], %i[e f a]],
|
|
||||||
[%i[b c e], %i[f d a]],
|
|
||||||
[%i[b c f], %i[d e a]],
|
|
||||||
[%i[c a d], %i[e f b]],
|
|
||||||
[%i[c a e], %i[f d b]],
|
|
||||||
[%i[c a f], %i[d e b]],
|
|
||||||
[%i[a b d], %i[e f c]],
|
|
||||||
[%i[a b e], %i[f d c]],
|
|
||||||
[%i[a b f], %i[d e c]]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are three tables with two people each' do
|
|
||||||
let(:initial_solution) do
|
|
||||||
Distribution.new(min_per_table: 2, max_per_table: 2).tap do |distribution|
|
|
||||||
distribution.tables << %i[a b]
|
|
||||||
distribution.tables << %i[c d]
|
|
||||||
distribution.tables << %i[e f]
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'yields all possible swaps between the tables' do
|
|
||||||
expect(swaps).to contain_exactly(
|
|
||||||
[%i[b c], %i[d a], %i[e f]],
|
|
||||||
[%i[b d], %i[c a], %i[e f]],
|
|
||||||
[%i[a c], %i[d b], %i[e f]],
|
|
||||||
[%i[a d], %i[c b], %i[e f]],
|
|
||||||
[%i[b e], %i[c d], %i[f a]],
|
|
||||||
[%i[b f], %i[c d], %i[e a]],
|
|
||||||
[%i[a e], %i[c d], %i[f b]],
|
|
||||||
[%i[a f], %i[c d], %i[e b]],
|
|
||||||
[%i[a b], %i[d e], %i[f c]],
|
|
||||||
[%i[a b], %i[d f], %i[e c]],
|
|
||||||
[%i[a b], %i[c e], %i[f d]],
|
|
||||||
[%i[a b], %i[c f], %i[e d]]
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Loading…
x
Reference in New Issue
Block a user