#ruby-on-rails #sorting
#ruby-on-rails #сортировка
Вопрос:
Мне нужно отсортировать таблицу рейтингов в Rails 3, поэтому у меня есть LeagueTable
модель, Team
model и Match
model.
Базовая сортировка выполняется по сумме баллов, так что это простая часть. Но когда две команды набрали одинаковое количество очков, я хочу отсортировать их по очкам, набранным в матчах между этими двумя командами.
Я понятия не имею, как это сделать.
Редактировать:
# league_table.rb model
class LeagueTable < ActiveRecord::Base
belongs_to :team
end
# match.rb model
class Match < ActiveRecord::Base
belongs_to :team_home, :class_name => "Team"
belongs_to :team_away, :class_name => "Team"
end
# team.rb model
class Team < ActiveRecord::Base
has_many :matches
end
# schema.rb
create_table "league_tables", :force => true do |t|
t.integer "team_id"
t.integer "points"
t.integer "wins"
t.integer "draws"
t.integer "looses"
t.integer "goals_won"
t.integer "goals_lost"
t.datetime "created_at"
t.datetime "updated_at"
t.integer "matches"
t.integer "priority"
end
create_table "matches", :force => true do |t|
t.integer "team_home_id"
t.integer "team_away_id"
t.integer "score_home"
t.integer "score_away"
t.datetime "created_at"
t.datetime "updated_at"
end
create_table "teams", :force => true do |t|
t.string "name"
t.datetime "created_at"
t.datetime "updated_at"
end
Комментарии:
1. Это проблема SQL, поэтому, пожалуйста, сбросьте схему ваших таблиц! KTHX
2. Пожалуйста, добавьте подробности к вашему вопросу о моделях, соответствующих полях и связях между ними.
3. Добавлена информация о рассматриваемых моделях и схеме.
4. Как
LeagueTable
обновляется? Возможно, лучшим решением будет установить порядок сafter_save
обратным вызовом каждый раз, когда он обновляется.5.
LeagueTable
обновляется приMatch
сохранении.
Ответ №1:
Очень интересный вопрос. Вот (более менее), как я бы справился с этим, используя рекурсию и магию group_by
.
- Вам понадобится метод класса, который, учитывая массив команд, вычисляет количество очков (голов за, голов против …), набранных каждой командой в только матчах между этими командами. Предположим, вы возвращаете хэш хэшей:
{team_foo => {:points => 10, :goals_for => 33, :goals_against => 6}, team_bar => {:points => 18, :goals_for => 50, :goals_against => 11}...}
. - Окончательный порядок команд будет помещен в массив
final_order
, который сейчас пуст. - Сведите все команды в массив и примените метод, упомянутый выше. Теперь у вас должны быть команды и количество очков (голов за, голов против …), набранных всеми командами во всех сыгранных на данный момент играх. Сохраните это где-нибудь, скажем, под именем
all_teams_scores
. - Возьмите все команды и сгруппируйте их, используя
group_by
:teams.group_by {|t| all_teams_scores[t][:points]}
. То, что вы получаете,OrderedHash
выглядит следующим образом:{10 => [team_foo], 18 => [team_bar, team_xyz]}
. Ключ — это то, по чему вы группируете, значение всегда является массивом. Сохраните этот хэш, например, какleague_table
. - Отсортируйте ключи
league_table
(не забудьте при необходимости поменять местами) и проверьте значенияleague_table
в соответствии с ними. Если массив содержит два или более элемента, примените пункты 3.-5. к командам в этом массиве. Если массив содержит один элемент, добавьте этот элемент вfinal_order
. - Выполнено.
Некоторые замечания:
- Помните, что в худшем случае у вас останутся две или более команд, которые набрали одинаковое количество очков (голов …) в прямых играх. Вам нужно это обработать или отсортировать случайным образом.
- Вышеуказанные шаги группируются только по точкам. Если вам нужно сначала отсортировать по очкам во всех играх, затем по очкам в прямых играх, это сработает. Если вы хотите сортировать по очкам во всех играх, по очкам в прямых играх, по голам во всех играх — вам нужно доработать приведенный выше алгоритм. Впрочем, это не должно быть сложно 😉
- Вы можете
group_by
использовать массив (что помогает, когда вы хотите отсортировать несколько объектов до или после сортировки по прямым играм). - Если ваш сайт не будет интенсивно использоваться, генерируйте таблицу по запросу. Нет необходимости хранить это в базе данных — если только сезон не закончен и не требуется вносить изменения в результаты игры.
- Если вы решите сохранить таблицу в базе данных, убедитесь, что поля, относящиеся к положению команды в таблице, недоступны для редактирования пользователем. Вероятно, лучшим способом было бы реализовать приведенный выше алгоритм в виде хранимой процедуры (PostgreSQL поддерживает pl / Ruby, хотя я им никогда не пользовался),
create view
основываясь на этом, и получить доступ к таблице ранжирования из Rails через нее (представления, как правило, доступны только для чтения). Конечно, удаление доступа к этим полям из административного интерфейса также нормально для 99,99% ситуаций 😉 - Это может не быть (и, вероятно, таковым не является) оптимальным решением с точки зрения скорости или эффективности памяти (хотя я это не проверял), но я полагаю, что код, созданный в соответствии с этим методом, должен быть легким для чтения, понимания и последующего сопровождения.
Надеюсь, это поможет.
Комментарии:
1. Очень красиво и чисто, стоит попробовать! Но куда мне поместить метод, который возвращает мне окончательно отсортированный массив?
2. Я бы использовал это как метод класса в
LeagueTable
или, если вы не хотите его сохранять, метод класса дляTeam
. Поскольку алгоритм не требует какого-либо конкретного экземпляра команды, метод должен быть методом класса.3. Да, AFAIK, я не могу поместить это в модель, так куда мне поместить метод класса?
4. Если под
def self.sorted
вы имеете в виду название метода с приведенным выше алгоритмом, то да. Метод, упомянутый в пункте 1. алгоритма, должен, очевидно, иметь другое название.
Ответ №2:
Вот как я бы это сделал :
-
Во-первых, я бы просто отсортировал команды по набранным очкам. Как вы сказали, это довольно тривиально.
-
Затем я бы прошелся по отсортированному массиву и проверил возможные команды, у которых одинаковые баллы. Это отобразилось бы в новом массиве хэшей. Для каждого набора команд, которые я бы нашел с одинаковыми баллами, я бы обозначил хэшем. Подумайте о 3 конкурирующих командах :
{ :TeamA => 3, :TeamB => 2 } { :TeamA => 3, :TeamC => 4 } { :TeamB => 1, :TeamC => 0 }
- Теперь я бы разобрался. Чтобы упростить задачу, у вас может быть максимальный или минимальный элемент (каждый раз представляющий команду).
Прохождение с максимальной :
1. max = TeamA
2. max = TeamC
Итак, самая сильная команда — TeamC. Исключите эту команду и повторите. Последние 2 хэша теперь удалены, и у нас остается только первый, который показывает, что TeamA > TeamB. Итак, окончательная сортировка будет :
TeamC > Команда > TeamB
ОБРАТИТЕВНИМАНИЕ: TeamC не лучше TeamB, когда рассматриваются только эти двое. Этот алгоритм дает в целом лучшую команду, основанную на выигрышных очках.
Ваш случай на самом деле проще. Вы просто хотите сравнить две команды. Следовательно, хэш, подобный :
{ :TeamA => 3, :TeamB => 2 }
ясно указывает, что TeamA лучше, чем TeamB, и должна быть оценена выше. Если вы хотите сравнить 3 команды, набравшие одинаковые очки, у вас должны быть другие критерии, например, команда, набравшая больше очков, лучше.
Редактировать
Если следующие 2 фактора для получения лучшей команды — это забитые голы, а затем разница в потерях, у вас будет другой хэш, подобный :
{ :TeamA => [3, 2], :TeamA => [2, 1], :TeamC => [1, 1] }
С [3,2], указывающим [разница забитых мячей и потерянных очков]. Теперь вы можете легко определить лучшие команды на основе этих двух параметров.
Комментарии:
1. Есть вероятность, что более чем у 2 команд будет одинаковое количество очков, поэтому ваш ответ сложный. Куда мне поместить метод сортировки? Я хочу, чтобы это действовало как область видимости.
2. Как я уже писал, когда 3 или более имеют одинаковые точки, вам нужна другая метрика. Простого сравнения 2 недостаточно, потому что A > B и B < C не обязательно означает, что C является лучшим. Что это за показатель?
3. В моем ответе я выбираю команду, у которой наибольший коэффициент начисления очков. Вы можете, например, выбрать команду с большим количеством очков в нападении или с большим количеством очков в обороне. Затем вы бы просто сохранили общую сумму и сравнили ее.
4. Например, TeamA, TeamB и TeamC получили одинаковое количество баллов. Нам нужно извлечь матчи, в которых эти 3 команды играли друг с другом, и подсчитать очки в этих матчах, затем отсортировать это как большую таблицу.
5. И куда поместить метод сортировки?
Ответ №3:
Я бы добавил rival_points
столбец в вашу LeagueTable
модель и обновил его, только если есть другие команды с таким же количеством очков.
Я думал о чем-то подобном этому (не проверял, работает ли это):
class LeagueTable
after_save :set_order_of_equals
def set_order_of_equals
LeagueTable.all.each do |lt|
points_against_rivals = 0
LeagueTable.where('points = ? and matches = ? and team_id <> ?', lt.points, lt.matches, lt.team_id).each do |lt_same_points|
points_against_rivals = lt.team.points_against(lt_same_points.team)
end
lt.rival_points = points_against_rivals
LeagueTable.after_save.clear # clear the after_save callback to prevent it from running endlessly
lt.save
end
end
end
class Team
def points_against(opponent)
points = 0
# Home games
matches.where(:team_away => opponent).each do |m|
if m.score_home == m.score_away
points = 1
elsif m.score_home > m.score_away
points = 3
end
end
# Away games
matches.where(:team_home => opponent).each do |m|
if m.score_away == m.score_home
points = 1
elsif m.score_away > m.score_home
points = 3
end
end
points
end
end
# With this you can get the correct order like this
lt = LeagueTable.order('points desc, matches asc, rival_points desc')
Комментарии:
1. Довольно хорошее решение, но я подожду несколько дней другого и приму ваше, если у меня не будет лучшего.
2. Конечно, никаких проблем. Мне самому интересно, придумает ли кто-нибудь разумное решение для этого.
Ответ №4:
Вот тяжелое, но эффективное решение с использованием SQL. Большая часть сложной логики выполняется в базе данных.
Добавьте новый столбец с именем group_rank
в league_tables
таблицу (по умолчанию должно быть 0). Проверьте, нет ли столкновения точек во время save
работы. Если имеет место точечное столкновение, рассчитайте group_rank
для сталкивающихся команд.
Таким образом, вы можете расположить команды в правильном порядке, используя простое order
предложение.
LeagueTable.all(:order => "points ASC, group_rank ASC")
Добавьте after_save
обратный вызов для определения точечного столкновения в LeagueTable
модели.
# league_table.rb model
class LeagueTable < ActiveRecord::Base
belongs_to :team
after_save :update_group_rank
def update_group_rank
return true unless points_changed?
# rank the rows with new points and old points
rank_group(points) and rank_group(points_was)
end
# rank_group
Метод:
def rank_group(group_points)
group_count = LeagueTable.count(:conditions =>{:points => group_points})
return true unless group_count > 1 # nothing to do
sql = "UPDATE league_tables JOIN
(
SELECT c.team_id, SUM(IF(c.score = 0, 1, c.score)) group_rank
FROM (
SELECT ca.team_home_id team_id, (ca.score_home-ca.score_away) score
FROM matches ca,
(SELECT cba.team_id
FROM league_tables cba
WHERE cba.points = #{group_points}
) cb
WHERE ca.team_home_id = cb.team_id AND ca.score_home >= ca.score_away
UNION
SELECT cc.team_away_id team_id, (cc.score_away-cc.score_home) score
FROM matches cc,
(SELECT cda.team_id
FROM league_tables cda
WHERE cda.points = #{group_points}
) cd
WHERE cc.team_away_id = cd.team_id AND cc.score_away >= cc.score_home
) c
GROUP BY c.team_id
) b ON league_tables.team_id = b.team_id
SET league_tables.group_rank = b.group_rank"
connection.execute(sql)
return true
end
end
Обязательно добавьте индекс в points
столбец.
Примечание: Это решение будет работать в MySQL. Переписать SQL для работы с другими базами данных довольно просто.
Ответ №5:
Я думаю, что это сложно сделать, используя сортировку базы данных. Предварительный расчет перекрестной таблицы с очками между командами возможен, но как использовать это при поиске. Я полагаю, что проблема станет еще сложнее, если три команды сыграют вничью.
Сортируйте то, что вы можете, из базы данных. Затем снова просмотрите список, чтобы отсортировать связи. Вы должны рассчитать тай-брейк для затронутых команд. Либо вы помещаете команды в новый массив сверху вниз, либо добавляете столбец для тай-брейков к командам и прибегаете ко второму проходу.
Я бы добавил в ваш класс модели метод, который разрешает тай-брейки и возвращает отсортированный массив.
Задача нетривиальная, но может быть забавной.
Комментарии:
1. Я знаю, что это должно быть сделано на ruby, а не с помощью SQL, но я понятия не имею, как это сделать, может быть, хэш с вычисленными точками в прямых совпадениях, а затем отсортировать этот хэш и вернуть его обратно в отсортированном порядке?
Ответ №6:
ваша проблема звучит действительно интересно. Я участвовал во многих соревнованиях (волейбол), и в случае равенства очков (когда очки зарабатываются на основе выигранных или проигранных матчей) команда, набравшая наибольшее количество очков и наименьшее количество контрапунктов, будет отсортирована первой. Итак, в этом случае, вместо того, чтобы смотреть на отдельные матчи, просто отсортируйте по общему количеству выигранных и проигранных мячей.
Тогда это было бы просто:
LeagueTable.all.order('points desc, goals_won desc, goals_lost desc')
что, я думаю, было бы довольно приличным порядком. Затем выбирается самая атакующая команда в целом. Вы также отдаете предпочтение наиболее защищенной команде, сортируя по order('points desc, goals_lost desc, goals_won desc')
.
Но это всего лишь короткий путь. Я полагаю, вы не вольны изменять правила сортировки по своему усмотрению, и это действительно приятная проблема для решения. Вот некоторый код, как бы я к этому подошел
all_teams_simple_sort = LeagueTable.all.sort('points desc')
all_teams_sorted = []
teams_equal_points = []
prev_team = all_teams_simple_sort[1]
prev_points = all_teams_simple_sort[1].points
(2..all_teams_simple_sort.size).each do |team_index|
team = all_teams_simple_sort[team_index]
if team.points == prev_team.points
teams_equal_points << prev_team if teams_equal_points.size == 0
teams_equal_points << team
else
if teams_equal_points.size > 0
add_equals_sorted_to all_teams_sorted, teams_equal_sorted
teams_equal_sorted = []
else
all_teams_sorted << prev_team
end
all_teams_sorted << team
end
prev_team = team
end
Это должно охватить все команды, объединить все команды с равными очками и добавить все остальные, если необходимо.
Теперь нам нужно только написать самую сложную функцию add_equals_sorted_to
, которая добавит команды с равными очками в правильном порядке к результату- сортировке.
def add_equals_sorted_to(result_sorted, equals_unsorted)
team_ids = equals_unsorted.collect(amp;:team_id)
# get all the matches for between those teams
matches = Match.where('team_home_id in (?) and team_away_id in (?)', team_ids.join(','), team_ids.join(','))
# create an empty hash for each team
team_score = {}
team_ids.each {|id| team_scores[id] = {:won => 0, :lost => 0} }
matches.each do |match|
team_scores[match.team_home_id] = {:won => team_scores[match.team_home_id][:won] match.score_home, :lost => team_scores[match.team_home_id][:lost] match.score_away }
team_scores[match.team_away_id] = {:won => team_scores[match.team_away_id][:won] match.score_away, :lost => team_scores[match.team_home_id][:lost] match.score_home }
end
# get the team with the highest :won and add to result_sorted
# and repeat until no more left
end
Этот код не тестировался 🙂 Но я надеюсь, что это должно помочь вам начать.