Compare commits
	
		
			7 Commits
		
	
	
		
			4725ac40b9
			...
			d511103596
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | d511103596 | ||
| 8a4e192385 | |||
| ebc7586670 | |||
| a38fefeb1f | |||
| b21d323e36 | |||
| 2dd11bb83d | |||
| 8c4e6a0109 | 
							
								
								
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Gemfile
									
									
									
									
									
								
							| @ -50,6 +50,7 @@ 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 | ||||||
| @ -65,4 +66,5 @@ end | |||||||
| 
 | 
 | ||||||
| gem "money" | gem "money" | ||||||
| gem 'acts-as-taggable-on' | gem 'acts-as-taggable-on' | ||||||
|  | 
 | ||||||
| gem "rubytree" | gem "rubytree" | ||||||
| @ -94,8 +94,12 @@ 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) | ||||||
|     faker (3.4.2) |     factory_bot (6.4.6) | ||||||
|       i18n (>= 1.8.11, < 2) |       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) |     globalid (1.2.1) | ||||||
|       activesupport (>= 6.1) |       activesupport (>= 6.1) | ||||||
|     i18n (1.14.5) |     i18n (1.14.5) | ||||||
| @ -265,6 +269,7 @@ DEPENDENCIES | |||||||
|   acts-as-taggable-on |   acts-as-taggable-on | ||||||
|   bootsnap |   bootsnap | ||||||
|   debug |   debug | ||||||
|  |   factory_bot_rails | ||||||
|   faker |   faker | ||||||
|   importmap-rails |   importmap-rails | ||||||
|   jbuilder |   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 | ||||||
							
								
								
									
										65
									
								
								app/services/tables/distribution.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								app/services/tables/distribution.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | |||||||
|  | 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 | ||||||
							
								
								
									
										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 | # 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 "RESTful" |   inflect.acronym 'VNS' | ||||||
| # end | end | ||||||
|  | |||||||
| @ -3,12 +3,7 @@ Rails.application.routes.draw do | |||||||
|     post :import, on: :collection |     post :import, on: :collection | ||||||
|   end |   end | ||||||
|   resources :expenses |   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. |   get 'up' => 'rails/health#show', as: :rails_health_check | ||||||
|   # 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 | ||||||
|  | |||||||
							
								
								
									
										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. | # 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 |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
| 
 | 
 | ||||||
| @ -35,6 +35,22 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_11_181632) 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" | ||||||
| @ -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 |     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,13 +1,6 @@ | |||||||
| # This file should ensure the existence of records required to run the application in every environment (production, | NUMBER_OF_GUESTS = 50 | ||||||
| # 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 | ||||||
| @ -53,7 +46,7 @@ samples = { | |||||||
|   count.times { acc << affinity_group } |   count.times { acc << affinity_group } | ||||||
| end | end | ||||||
| 
 | 
 | ||||||
| 300.times do | NUMBER_OF_GUESTS.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, | ||||||
|  | |||||||
							
								
								
									
										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! |   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 | ||||||
|  | |||||||
							
								
								
									
										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 | ||||||
							
								
								
									
										83
									
								
								spec/services/tables/swap_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								spec/services/tables/swap_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,83 @@ | |||||||
|  | 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