From f3b70f5a317df070d7fa18f76bfd3924fe038219 Mon Sep 17 00:00:00 2001 From: Manuel Bustillo Date: Sun, 10 Nov 2024 11:22:51 +0100 Subject: [PATCH] Apply a penalty if table sizes are not honored --- app/services/tables/discomfort_calculator.rb | 26 +++++++++- app/services/tables/distribution.rb | 6 ++- app/services/tables/table.rb | 5 +- .../tables/discomfort_calculator_spec.rb | 50 ++++++++++++++++++- 4 files changed, 80 insertions(+), 7 deletions(-) diff --git a/app/services/tables/discomfort_calculator.rb b/app/services/tables/discomfort_calculator.rb index 09b5356..0d40670 100644 --- a/app/services/tables/discomfort_calculator.rb +++ b/app/services/tables/discomfort_calculator.rb @@ -3,16 +3,38 @@ module Tables class DiscomfortCalculator private attr_reader :table - def initialize(table) + def initialize(table:) @table = table end def calculate - cohesion_penalty + table_size_penalty + cohesion_penalty end 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 table.map(&:group_id).tally.to_a.combination(2).sum do |(a, count_a), (b, count_b)| distance = AffinityGroupsHierarchy.instance.distance(a, b) diff --git a/app/services/tables/distribution.rb b/app/services/tables/distribution.rb index 48d4ea9..52621c3 100644 --- a/app/services/tables/distribution.rb +++ b/app/services/tables/distribution.rb @@ -4,7 +4,7 @@ require_relative '../../extensions/tree_node_extension' module Tables class Distribution - attr_accessor :tables + attr_accessor :tables, :min_per_table, :max_per_table def initialize(min_per_table:, max_per_table:) @min_per_table = min_per_table @@ -17,6 +17,8 @@ module Tables max_tables = (people.count * 1.0 / @min_per_table).ceil @tables = people.in_groups(rand(min_tables..max_tables), false) .map { |group| Table.new(group) } + .each { |table| table.min_per_table = @min_per_table } + .each { |table| table.max_per_table = @max_per_table } end def discomfort @@ -62,7 +64,7 @@ module Tables private def local_discomfort(table) - table.discomfort ||= DiscomfortCalculator.new(table).calculate + table.discomfort ||= DiscomfortCalculator.new(table:).calculate end end end diff --git a/app/services/tables/table.rb b/app/services/tables/table.rb index 92b0f08..8f58854 100644 --- a/app/services/tables/table.rb +++ b/app/services/tables/table.rb @@ -2,7 +2,8 @@ module Tables class Table < Array - attr_accessor :discomfort + attr_accessor :discomfort, :min_per_table, :max_per_table + def initialize(*args) super reset @@ -14,4 +15,4 @@ module Tables original_discomfort end end -end \ No newline at end of file +end diff --git a/spec/services/tables/discomfort_calculator_spec.rb b/spec/services/tables/discomfort_calculator_spec.rb index 0e22602..b7a3030 100644 --- a/spec/services/tables/discomfort_calculator_spec.rb +++ b/spec/services/tables/discomfort_calculator_spec.rb @@ -3,13 +3,61 @@ require 'rails_helper' module Tables RSpec.describe DiscomfortCalculator do - let(:calculator) { described_class.new(table) } + let(:calculator) { described_class.new(table:) } let(:family) { create(:group, name: 'family') } let(:friends) { create(:group, name: 'friends') } let(:work) { create(:group, name: 'work') } 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 before do # Overridden in each test except trivial cases