Merge pull request 'Use group affinities in discomfort calculator' (#215) from discomfort-calculation-revamp into main
Reviewed-on: #215
This commit is contained in:
commit
7c968f555f
@ -36,17 +36,21 @@ class AffinitiesController < ApplicationController
|
|||||||
end
|
end
|
||||||
|
|
||||||
def default
|
def default
|
||||||
|
hierarchy = AffinityGroupsHierarchy.new
|
||||||
|
|
||||||
for_each_group do |group_id|
|
for_each_group do |group_id|
|
||||||
Tables::DiscomfortCalculator.cohesion_discomfort(id_a: @group.id, id_b: group_id).to_f
|
hierarchy.default_discomfort(@group.id, group_id).to_f
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def reset
|
def reset
|
||||||
|
hierarchy = AffinityGroupsHierarchy.new
|
||||||
|
|
||||||
affinities = Group.pluck(:id).combination(2).map do |(group_a_id, group_b_id)|
|
affinities = Group.pluck(:id).combination(2).map do |(group_a_id, group_b_id)|
|
||||||
{
|
{
|
||||||
group_a_id:,
|
group_a_id:,
|
||||||
group_b_id:,
|
group_b_id:,
|
||||||
discomfort: Tables::DiscomfortCalculator.cohesion_discomfort(id_a: group_a_id, id_b: group_b_id).to_f
|
discomfort: hierarchy.default_discomfort(group_a_id, group_b_id).to_f
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AffinityGroupsHierarchy < Array
|
class AffinityGroupsHierarchy < Array
|
||||||
include Singleton
|
DEFAULT_DISCOMFORT = 1
|
||||||
|
|
||||||
def initialize
|
def initialize
|
||||||
super
|
super
|
||||||
@ -16,6 +16,9 @@ class AffinityGroupsHierarchy < Array
|
|||||||
|
|
||||||
hydrate(group)
|
hydrate(group)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
load_discomforts
|
||||||
|
freeze
|
||||||
end
|
end
|
||||||
|
|
||||||
def find(id)
|
def find(id)
|
||||||
@ -37,8 +40,35 @@ class AffinityGroupsHierarchy < Array
|
|||||||
@references[id_a].distance_to_common_ancestor(@references[id_b])
|
@references[id_a].distance_to_common_ancestor(@references[id_b])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def discomfort(id_a, id_b)
|
||||||
|
return 0 if id_a == id_b
|
||||||
|
|
||||||
|
@discomforts[uuid_to_int(id_a) + uuid_to_int(id_b)] || DEFAULT_DISCOMFORT
|
||||||
|
end
|
||||||
|
|
||||||
|
def default_discomfort(id_a, id_b)
|
||||||
|
return 0 if id_a == id_b
|
||||||
|
|
||||||
|
dist = distance(id_a, id_b)
|
||||||
|
|
||||||
|
return DEFAULT_DISCOMFORT if dist.nil?
|
||||||
|
|
||||||
|
Rational(dist, dist + 1)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_discomforts
|
||||||
|
@load_discomforts ||= GroupAffinity.pluck(:group_a_id, :group_b_id,
|
||||||
|
:discomfort).each_with_object({}) do |(id_a, id_b, discomfort), acc|
|
||||||
|
acc[uuid_to_int(id_a) + uuid_to_int(id_b)] = discomfort
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def uuid_to_int(uuid)
|
||||||
|
uuid.gsub('-', '').hex
|
||||||
|
end
|
||||||
|
|
||||||
def hydrate(group)
|
def hydrate(group)
|
||||||
group.children.each do |child|
|
group.children.each do |child|
|
||||||
register_child(group.id, child.id)
|
register_child(group.id, child.id)
|
||||||
|
@ -6,19 +6,10 @@
|
|||||||
|
|
||||||
module Tables
|
module Tables
|
||||||
class DiscomfortCalculator
|
class DiscomfortCalculator
|
||||||
class << self
|
private attr_reader :table, :hierarchy
|
||||||
def cohesion_discomfort(id_a:, id_b:)
|
def initialize(table:, hierarchy: AffinityGroupsHierarchy.new)
|
||||||
distance = AffinityGroupsHierarchy.instance.distance(id_a, id_b)
|
|
||||||
|
|
||||||
return 1 if distance.nil?
|
|
||||||
|
|
||||||
Rational(distance, distance + 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private attr_reader :table
|
|
||||||
def initialize(table:)
|
|
||||||
@table = table
|
@table = table
|
||||||
|
@hierarchy = hierarchy
|
||||||
end
|
end
|
||||||
|
|
||||||
def calculate
|
def calculate
|
||||||
@ -59,7 +50,7 @@ module Tables
|
|||||||
#
|
#
|
||||||
def cohesion_discomfort
|
def cohesion_discomfort
|
||||||
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)|
|
||||||
count_a * count_b * self.class.cohesion_discomfort(id_a: a, id_b: b)
|
count_a * count_b * hierarchy.discomfort(a, b)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -13,6 +13,7 @@ module Tables
|
|||||||
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
|
||||||
@max_per_table = max_per_table
|
@max_per_table = max_per_table
|
||||||
|
@hierarchy = AffinityGroupsHierarchy.new
|
||||||
@tables = []
|
@tables = []
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -35,12 +36,6 @@ module Tables
|
|||||||
"#{@tables.count} tables, discomfort: #{discomfort}"
|
"#{@tables.count} tables, discomfort: #{discomfort}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def pretty_print
|
|
||||||
@tables.map.with_index do |table, i|
|
|
||||||
"Table #{i + 1} (#{table.count} ppl): (#{local_discomfort(table)}) #{table.map(&:name).join(', ')}"
|
|
||||||
end.join("\n")
|
|
||||||
end
|
|
||||||
|
|
||||||
def deep_dup
|
def deep_dup
|
||||||
self.class.new(min_per_table: @min_per_table, max_per_table: @max_per_table).tap do |new_distribution|
|
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)
|
new_distribution.tables = @tables.map(&:dup)
|
||||||
@ -68,7 +63,7 @@ module Tables
|
|||||||
private
|
private
|
||||||
|
|
||||||
def local_discomfort(table)
|
def local_discomfort(table)
|
||||||
table.discomfort ||= DiscomfortCalculator.new(table:).calculate
|
table.discomfort ||= DiscomfortCalculator.new(table:, hierarchy:).calculate
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -75,159 +75,5 @@ module Tables
|
|||||||
it { expect(calculator.send(:table_size_penalty)).to eq(10) }
|
it { expect(calculator.send(:table_size_penalty)).to eq(10) }
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#cohesion_discomfort' 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.id, friends.id).and_return(nil)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, work.id).and_return(1)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, work.id).and_return(2)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(family.id, school.id).and_return(3)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(friends.id, school.id).and_return(4)
|
|
||||||
allow(AffinityGroupsHierarchy.instance).to receive(:distance).with(work.id, school.id).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, group: family) }
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_discomfort)).to eq(0) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to completely unrelated groups' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, group: family),
|
|
||||||
create(:guest, group: friends)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_discomfort)).to eq(1) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 1' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, group: friends),
|
|
||||||
create(:guest, group: work)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_discomfort)).to eq(0.5) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 2' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, group: family),
|
|
||||||
create(:guest, group: work)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_discomfort)).to eq(Rational(2, 3)) }
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when they belong to groups at a distance of 3' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, group: family),
|
|
||||||
create(:guest, group: school)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it { expect(calculator.send(:cohesion_discomfort)).to eq(Rational(3, 4)) }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when the table contains three guests' do
|
|
||||||
let(:table) do
|
|
||||||
[
|
|
||||||
create(:guest, group: family),
|
|
||||||
create(:guest, group: friends),
|
|
||||||
create(:guest, group: work)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort)).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, group: family),
|
|
||||||
create(:guest, group: friends),
|
|
||||||
create(:guest, group: work),
|
|
||||||
create(:guest, group: school)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort))
|
|
||||||
.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, group: family),
|
|
||||||
create_list(:guest, 2, group: friends)
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort)).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, group: family),
|
|
||||||
create_list(:guest, 4, group: friends)
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort)).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, group: family),
|
|
||||||
create_list(:guest, 2, group: friends),
|
|
||||||
create_list(:guest, 2, group: work)
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort)).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, group: family),
|
|
||||||
create_list(:guest, 2, group: friends),
|
|
||||||
create_list(:guest, 1, group: work)
|
|
||||||
].flatten
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns the sum of the penalties for each pair of guests' do
|
|
||||||
expect(calculator.send(:cohesion_discomfort)).to eq((6 * 1) + (2 * Rational(1, 2)) + (3 * Rational(2, 3)))
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
Loading…
x
Reference in New Issue
Block a user