Rails 3复杂的排序

时间:2011-04-29 14:25:58

标签: ruby-on-rails sorting

我需要在Rails 3中对排名表进行排序,因此我有LeagueTable模型,Team模型和Match模型。
基本排序是通过积分总结完成的,所以这很容易。但是当两支球队获得相同的分数时,我想按照这两支球队之间的比赛赢得的分数进行排序 我不知道该怎么做。

编辑:

# 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

6 个答案:

答案 0 :(得分:1)

我会这样做:

  1. 首先,我会根据他们的观点对球队进行排序。正如你所说,这是微不足道的。

  2. 然后,我将遍历已排序的数组并检查具有相同点的可能团队。这将显示在一个新的哈希数组中。对于我会发现具有相同点的每组球队,我会用哈希表示。想想3个竞争团队:

  3. { :TeamA => 3, :TeamB => 2 }
    { :TeamA => 3,  :TeamC => 4 }
    { :TeamB => 1,  :TeamC => 0 }
    
    1. 现在,我会排序。为了简化操作,您可以拥有max或min元素(每次代表一个团队)。
    2. 以max:

      遍历
      1. max = TeamA
      2. max = TeamC
      

      最强大的团队是TeamC。消除该团队并重复。最后2个哈希现在被淘汰了,我们只剩下第一个,这表明TeamA&gt; TeamB。因此,最终的排序将是:

      TeamC&gt; TeamA&gt; TeamB

      通知:只有这两个被认为是TeamC并不比TeamB好。根据获胜点,该算法可以提供更好的团队。

      你的情况实际上更简单。你只想比较两支球队。因此,哈希就像:

      { :TeamA => 3, :TeamB => 2 }
      

      明确表示TeamA优于TeamB,应该排名更高。如果你想比较具有相同分数的3支球队,你必须有另一个标准,比如最高得分球队更好。

      修改

      如果获得最佳团队的下两个因素是得分并且然后失去差异,那么你会有另外一个哈希:

      { :TeamA => [3, 2], :TeamA => [2, 1], :TeamC => [1, 1] }
      

      用[3,2]表示[进球得分,失去差距]。您现在可以根据这两个参数轻松识别最佳团队。

答案 1 :(得分:1)

我会在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')

答案 2 :(得分:1)

这是一个SQL重要而有效的解决方案。大多数困难的逻辑都在执行 DB。

将一个名为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以与其他数据库一起工作是相当直接的。

答案 3 :(得分:1)

非常有趣的问题。这是(更少)我将如何处理它,使用递归和group_by的魔力。

  1. 你需要一个类方法,给定一组球队,计算每支球队在这些球队之间的比赛中获得的分数(目标,目标......) 。假设您返回散列哈希:{team_foo => {:points => 10, :goals_for => 33, :goals_against => 6}, team_bar => {:points => 18, :goals_for => 50, :goals_against => 11}...}
  2. 团队的最终订单将被放入一个数组final_order,现在是空的。
  3. 将所有团队带到一个阵列并应用上述方法。你现在应该拥有所有球队在当前所有比赛中获得的球队和得分(进球数,目标数......)。将其存储在某处,例如名称all_teams_scores
  4. 选择所有团队并使用group_byteams.group_by {|t| all_teams_scores[t][:points]}对其进行分组。你得到的是OrderedHash,看起来像是:{10 => [team_foo], 18 => [team_bar, team_xyz]}。关键是你分组的,值总是一个数组。存储此哈希,例如为league_table
  5. league_table的键进行排序(记得在需要时撤消)并根据它们检查league_table的值。如果数组有两个或更多元素,则应用点3.-5。对阵列中的团队。如果数组有一个元素,请将此元素追加到final_order
  6. 完成。
  7. 一些评论:

    • 请记住,在最糟糕的情况下,你会留下两个或更多的团队在直接游戏中获得相同数量的积分(目标......)。你需要处理它,或者随机排序。
    • 以上步骤仅按分数进行。如果您需要首先按所有游戏中的积分排序,然后直接游戏中的积分,它将起作用。如果你想按所有游戏中的积分,直接游戏中的积分,所有游戏中的目标进行排序 - 你需要优化上述算法。不过,这应该不难;)
    • 您可以group_by一个数组(当您想要在直接游戏排序之前或之后对多种事物进行排序时,这会有所帮助。)
    • 除非您的网站将被大量使用,否则请在需要时生成该表。无需将其存放在数据库中 - 除非赛季结束且不会对游戏结果进行任何更改。
    • 如果您决定将表存储在数据库中,请确保用户无法编辑与表中团队位置相关的字段。最好的方法可能是将上述算法实现为存储过程(PostgreSQL支持pl / Ruby,虽然我从未使用它),create view基于此,并通过它访问Rails的联盟表(视图一般, 只读)。当然,对于99.99%的情况,从管理界面中删除对这些字段的访问也是可以的;)
    • 这可能不是(也可能不是)在速度或内存效率方面的最佳解决方案(虽然我没有检查过),但我认为根据这种方法生成的代码应该易于阅读,理解和维护稍后。

    希望这有帮助。

答案 4 :(得分:0)

我认为使用数据库排序很难做到这一点。可以使用团队之间的点来预先计算交叉表,但是如何在搜索中使用它。我想如果三支球队并列,问题就更难了。

对数据库中的内容进行排序。然后再次浏览列表以理清关系。您必须计算受影响团队的决胜局。您可以将团队从上到下放入一个新阵列,或者向团队添加一个抢七栏,并在第二轮中使用。

我会在你的模型类中添加一个方法来解决关系中断并返回一个已排序的数组。

这项任务非常重要,但可能很有趣。

答案 5 :(得分:0)

你的问题听起来很有趣。我参加了很多比赛(排球),如果得分相等(根据赢或输的比赛赢得积分),得分最多的队伍和最少的对位将先排序。因此,在这种情况下,不是真正关注单个匹配,而是按总赢得目标和-lost排序。

那么这很简单:

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(&: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

此代码未经过测试:)但我希望它能让您入门。