Apply a penalty if table sizes are not honored #108

Merged
bustikiller merged 1 commits from table-size-discomfort into main 2024-11-10 10:25:37 +00:00
4 changed files with 80 additions and 7 deletions

View File

@ -3,16 +3,38 @@
module Tables module Tables
class DiscomfortCalculator class DiscomfortCalculator
private attr_reader :table private attr_reader :table
def initialize(table) def initialize(table:)
@table = table @table = table
end end
def calculate def calculate
cohesion_penalty table_size_penalty + cohesion_penalty
end end
private private
#
# Calculates the penalty associated with violating the table size constraints. The penalty is
# zero when the limits are honored, and it increases linearly as the number of guests deviates
# from the limits. Overcapacity is penalized more severely than undercapacity.
#
# @return [Number] The penalty associated with violating the table size constraints.
#
def table_size_penalty
case table.size
when 0...table.min_per_table then 2 * (table.min_per_table - table.size)
when table.min_per_table..table.max_per_table then 0
else 5 * (table.size - table.max_per_table)
end
end
#
# Calculates the discomfort of the table based on the cohesion of the guests. The total discomfort
# is calculated as the sum of the discomfort of each pair of guests. The discomfort of a pair of
# guests is a rational number between 1 (unrelated groups) and 0 (same group).
#
# @return [Number] Total discomfort of the table.
#
def cohesion_penalty def cohesion_penalty
table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)|
distance = AffinityGroupsHierarchy.instance.distance(a, b) distance = AffinityGroupsHierarchy.instance.distance(a, b)

View File

@ -4,7 +4,7 @@ require_relative '../../extensions/tree_node_extension'
module Tables module Tables
class Distribution class Distribution
attr_accessor :tables attr_accessor :tables, :min_per_table, :max_per_table
def initialize(min_per_table:, max_per_table:) def initialize(min_per_table:, max_per_table:)
@min_per_table = min_per_table @min_per_table = min_per_table
@ -17,6 +17,8 @@ module Tables
max_tables = (people.count * 1.0 / @min_per_table).ceil max_tables = (people.count * 1.0 / @min_per_table).ceil
@tables = people.in_groups(rand(min_tables..max_tables), false) @tables = people.in_groups(rand(min_tables..max_tables), false)
.map { |group| Table.new(group) } .map { |group| Table.new(group) }
.each { |table| table.min_per_table = @min_per_table }
.each { |table| table.max_per_table = @max_per_table }
end end
def discomfort def discomfort
@ -62,7 +64,7 @@ module Tables
private private
def local_discomfort(table) def local_discomfort(table)
table.discomfort ||= DiscomfortCalculator.new(table).calculate table.discomfort ||= DiscomfortCalculator.new(table:).calculate
end end
end end
end end

View File

@ -2,7 +2,8 @@
module Tables module Tables
class Table < Array class Table < Array
attr_accessor :discomfort attr_accessor :discomfort, :min_per_table, :max_per_table
def initialize(*args) def initialize(*args)
super super
reset reset

View File

@ -3,13 +3,61 @@
require 'rails_helper' require 'rails_helper'
module Tables module Tables
RSpec.describe DiscomfortCalculator do RSpec.describe DiscomfortCalculator do
let(:calculator) { described_class.new(table) } let(:calculator) { described_class.new(table:) }
let(:family) { create(:group, name: 'family') } let(:family) { create(:group, name: 'family') }
let(:friends) { create(:group, name: 'friends') } let(:friends) { create(:group, name: 'friends') }
let(:work) { create(:group, name: 'work') } let(:work) { create(:group, name: 'work') }
let(:school) { create(:group, name: 'school') } let(:school) { create(:group, name: 'school') }
describe '#table_size_penalty' do
before do
table.min_per_table = 5
table.max_per_table = 7
end
context 'when the number of guests is in the lower bound' do
let(:table) { Table.new(create_list(:guest, 5)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is within the table size limits' do
let(:table) { Table.new(create_list(:guest, 6)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is in the upper bound' do
let(:table) { Table.new(create_list(:guest, 7)) }
it { expect(calculator.send(:table_size_penalty)).to eq(0) }
end
context 'when the number of guests is one unit below the lower bound' do
let(:table) { Table.new(create_list(:guest, 4)) }
it { expect(calculator.send(:table_size_penalty)).to eq(2) }
end
context 'when the number of guests is two units below the lower bound' do
let(:table) { Table.new(create_list(:guest, 3)) }
it { expect(calculator.send(:table_size_penalty)).to eq(4) }
end
context 'when the number of guests is one unit above the upper bound' do
let(:table) { Table.new(create_list(:guest, 8)) }
it { expect(calculator.send(:table_size_penalty)).to eq(5) }
end
context 'when the number of guests is two units above the upper bound' do
let(:table) { Table.new(create_list(:guest, 9)) }
it { expect(calculator.send(:table_size_penalty)).to eq(10) }
end
end
describe '#cohesion_penalty' do describe '#cohesion_penalty' do
before do before do
# Overridden in each test except trivial cases # Overridden in each test except trivial cases