Initial version of VNS algorithm (#8)
All checks were successful
Run unit tests / unit_tests (push) Successful in 3m36s
All checks were successful
Run unit tests / unit_tests (push) Successful in 3m36s
Reviewed-on: #8 Co-authored-by: Manuel Bustillo <bustikiller@bustikiller.com> Co-committed-by: Manuel Bustillo <bustikiller@bustikiller.com>
This commit is contained in:
parent
55939658a5
commit
8c4e6a0109
4
Gemfile
4
Gemfile
@ -50,6 +50,7 @@ group :development, :test do
|
||||
gem 'rspec-rails', '~> 6.1.0'
|
||||
gem 'faker'
|
||||
gem 'pry'
|
||||
gem "factory_bot_rails"
|
||||
end
|
||||
|
||||
group :development do
|
||||
@ -65,4 +66,5 @@ end
|
||||
|
||||
gem "money"
|
||||
gem 'acts-as-taggable-on'
|
||||
gem "rubytree"
|
||||
|
||||
gem "rubytree"
|
||||
|
@ -94,8 +94,12 @@ GEM
|
||||
diff-lcs (1.5.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.13.0)
|
||||
faker (3.4.2)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
factory_bot (6.4.6)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
faker (3.1.1)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
i18n (1.14.5)
|
||||
@ -265,6 +269,7 @@ DEPENDENCIES
|
||||
acts-as-taggable-on
|
||||
bootsnap
|
||||
debug
|
||||
factory_bot_rails
|
||||
faker
|
||||
importmap-rails
|
||||
jbuilder
|
||||
|
10
app/controllers/tables_arrangements_controller.rb
Normal file
10
app/controllers/tables_arrangements_controller.rb
Normal file
@ -0,0 +1,10 @@
|
||||
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
|
2
app/helpers/tables_arrangements_helper.rb
Normal file
2
app/helpers/tables_arrangements_helper.rb
Normal file
@ -0,0 +1,2 @@
|
||||
module TablesArrangementsHelper
|
||||
end
|
4
app/models/seat.rb
Normal file
4
app/models/seat.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Seat < ApplicationRecord
|
||||
belongs_to :guest
|
||||
belongs_to :table_arrangement
|
||||
end
|
3
app/models/tables_arrangement.rb
Normal file
3
app/models/tables_arrangement.rb
Normal file
@ -0,0 +1,3 @@
|
||||
class TablesArrangement < ApplicationRecord
|
||||
has_many :seats
|
||||
end
|
25
app/services/tables/discomfort_calculator.rb
Normal file
25
app/services/tables/discomfort_calculator.rb
Normal file
@ -0,0 +1,25 @@
|
||||
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
|
64
app/services/tables/distribution.rb
Normal file
64
app/services/tables/distribution.rb
Normal file
@ -0,0 +1,64 @@
|
||||
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
|
||||
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
|
28
app/services/tables/swap.rb
Normal file
28
app/services/tables/swap.rb
Normal file
@ -0,0 +1,28 @@
|
||||
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
|
48
app/services/vns/engine.rb
Normal file
48
app/services/vns/engine.rb
Normal file
@ -0,0 +1,48 @@
|
||||
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
|
9
app/views/tables_arrangements/index.html.erb
Normal file
9
app/views/tables_arrangements/index.html.erb
Normal file
@ -0,0 +1,9 @@
|
||||
<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>
|
16
app/views/tables_arrangements/show.html.erb
Normal file
16
app/views/tables_arrangements/show.html.erb
Normal file
@ -0,0 +1,16 @@
|
||||
<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
|
||||
|
||||
# These inflection rules are supported but not enabled by default:
|
||||
# ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
# inflect.acronym "RESTful"
|
||||
# end
|
||||
ActiveSupport::Inflector.inflections(:en) do |inflect|
|
||||
inflect.acronym 'VNS'
|
||||
end
|
||||
|
@ -3,12 +3,7 @@ Rails.application.routes.draw do
|
||||
post :import, on: :collection
|
||||
end
|
||||
resources :expenses
|
||||
# Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html
|
||||
resources :tables_arrangements, only: [:index, :show]
|
||||
|
||||
# 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"
|
||||
get 'up' => 'rails/health#show', as: :rails_health_check
|
||||
end
|
||||
|
9
db/migrate/20240724181756_create_tables_arrangements.rb
Normal file
9
db/migrate/20240724181756_create_tables_arrangements.rb
Normal file
@ -0,0 +1,9 @@
|
||||
class CreateTablesArrangements < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :tables_arrangements, id: :uuid do |t|
|
||||
t.integer :discomfort
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
end
|
||||
end
|
13
db/migrate/20240724181853_create_seats.rb
Normal file
13
db/migrate/20240724181853_create_seats.rb
Normal file
@ -0,0 +1,13 @@
|
||||
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.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_07_11_181632) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_07_24_181853) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@ -35,6 +35,22 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_11_181632) do
|
||||
t.datetime "updated_at", null: false
|
||||
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|
|
||||
t.bigint "tag_id"
|
||||
t.string "taggable_type"
|
||||
@ -66,5 +82,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_11_181632) do
|
||||
t.index ["name"], name: "index_tags_on_name", unique: true
|
||||
end
|
||||
|
||||
add_foreign_key "seats", "guests"
|
||||
add_foreign_key "seats", "tables_arrangements", on_delete: :cascade
|
||||
add_foreign_key "taggings", "tags"
|
||||
end
|
||||
|
13
db/seeds.rb
13
db/seeds.rb
@ -1,13 +1,6 @@
|
||||
# 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
|
||||
NUMBER_OF_GUESTS = 50
|
||||
|
||||
TablesArrangement.delete_all
|
||||
Expense.delete_all
|
||||
Guest.delete_all
|
||||
ActsAsTaggableOn::Tagging.delete_all
|
||||
@ -53,7 +46,7 @@ samples = {
|
||||
count.times { acc << affinity_group }
|
||||
end
|
||||
|
||||
300.times do
|
||||
NUMBER_OF_GUESTS.times do
|
||||
guest = Guest.create!(first_name: Faker::Name.first_name,
|
||||
last_name: Faker::Name.last_name,
|
||||
email: Faker::Internet.email,
|
||||
|
18
lib/tasks/vns.rake
Normal file
18
lib/tasks/vns.rake
Normal file
@ -0,0 +1,18 @@
|
||||
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
|
8
spec/factories/guest.rb
Normal file
8
spec/factories/guest.rb
Normal file
@ -0,0 +1,8 @@
|
||||
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
|
5
spec/models/seat_spec.rb
Normal file
5
spec/models/seat_spec.rb
Normal file
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Seat, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
5
spec/models/tables_arrangement_spec.rb
Normal file
5
spec/models/tables_arrangement_spec.rb
Normal file
@ -0,0 +1,5 @@
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TablesArrangement, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
@ -62,4 +62,5 @@ RSpec.configure do |config|
|
||||
config.filter_rails_from_backtrace!
|
||||
# arbitrary gems may also be filtered via:
|
||||
# config.filter_gems_from_backtrace("gem name")
|
||||
config.include FactoryBot::Syntax::Methods
|
||||
end
|
||||
|
158
spec/services/tables/discomfort_calculator_spec.rb
Normal file
158
spec/services/tables/discomfort_calculator_spec.rb
Normal file
@ -0,0 +1,158 @@
|
||||
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
|
Loading…
x
Reference in New Issue
Block a user