如何对包含数字的字符串组成的collection_select下拉菜单进行排序?

时间:2016-01-21 18:13:15

标签: ruby-on-rails ruby sorting ruby-on-rails-4

我正在构建一个Rails(4.1.8)/ Ruby(2.1.5)应用程序。我有以下collection_select表单字段:

 <%= f.collection_select :target_ids, Target.order(:outcome), :id, :outcome, { :prompt => "-- Select one to two --" }, { :multiple => true, :size => 6 } %>

这可以按预期工作,但我需要帮助排序下拉菜单中的项目顺序,其中包含带数字的字符串。我当前的代码按字母按升序对列表进行排序,但字符串中的数字不按顺序排列。这是一个片段,显示菜单现在的样子:

-- Select one to two -- 
Gain 10 pounds
Gain 15 pounds
Gain 1 pound
Lose 10 pounds
Lose 15 pounds
Lose 1 pound
Reduce body fat by 15%
Reduce body fat by 2%

以下是我要对菜单进行排序的方法:

-- Select one to two --
Gain 1 pound
Gain 10 pounds
Gain 15 pounds
Lose 1 pound
Lose 10 pounds
Lose 15 pounds
Reduce body fat by 2%
Reduce body fat by 15%

如何让字符串中的数字按我想要的顺序显示?是否有一种Ruby排序方法可以处理这种排除在外的方法,或者我是否需要编写自定义排序方法?我如何才能最好地达到我想要的结果?谢谢!

1 个答案:

答案 0 :(得分:2)

TL; DR

使用Enumerable#sort_by一个块,从每个outcome中提取前导文本和数字(转换为Fixnums):

class Target < ActiveRecord::Base
  OUTCOME_PARTS_EXPR = /(\D+)(\d+)/

  # ...

  def self.sort_by_outcome
    all.sort_by do |target|
      outcome = target.outcome
      next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
      [ $1, $2.to_i ]
    end
  end
end

然后,在你的控制器中:

@targets_sorted = Target.sort_by_outcome

最后,在你看来:

<%= f.collection_select :target_ids, @targets_sorted, :id, :outcome,
      { :prompt => "-- Select one to two --" }, { :multiple => true, :size => 6 } %>

如何运作

一般来说,你应该尝试在数据库中进行所有排序,但由于我不知道你正在使用什么数据库,我将限制我在Ruby中如何做到这一点的答案。对于相对较少的记录,无论如何,性能损失都可以忽略不计 - 过早优化和所有这些。

在Ruby中,Enumerable#sort_by方法将按块的结果对Enumerable进行排序。看起来您希望首先按数字前面的部分(例如"Gain ""Reduce body fat by ")对数值进行排序,然后按数字的整数值(例如Fixnum 10而不是字符串"10")。让我们分成几步:

1。从字符串中获取所需的部分。

使用正则表达式很容易:

OUTCOME_PARTS_EXPR = /(\D+)(\d+)/
str = "Gain 10 pounds"
str.match(OUTCOME_PARTS_EXPR).captures
# => [ "Gain ", "10" ]

注意:此正则表达式假定始终是数字之前的某些非数字文本。如果情况并非如此,则可能需要进行一些修改。

2。对Enumerable

中的每个元素执行此操作

使用Enumerable#map我们可以为整个Enumerable执行此操作,而我们可以将数字转换为Fixnums(例如"10"10):

arr = [
  "Gain 10 pounds",
  "Gain 15 pounds",
  "Gain 1 pound",
  "Lose 10 pounds",
  "Lose 15 pounds",
  "Lose 1 pound",
  "Reduce body fat by 15%",
  "Reduce body fat by 2%"
]

arr.map do |str|
  next [ str ] unless str =~ OUTCOME_PARTS_EXPR
  [ $1, $2.to_i ]
end

我们传递给map的块做了两件事:如果字符串与我们的正则表达式不匹配,它只返回字符串(作为单元素数组)。否则,它返回一个数组,第一个捕获("Gain ")作为字符串,第二个捕获(10)作为Fixnum。这是结果:

[ [ "Gain ", 10 ],
  [ "Gain ", 15 ],
  [ "Gain ",  1 ],
  [ "Lose ", 10 ],
  [ "Lose ", 15 ],
  [ "Lose ",  1 ],
  [ "Reduce body fat by ", 15 ],
  [ "Reduce body fat by ",  2 ] ]

3。按这些部分排序

现在我们知道如何获取我们需要的部分,我们可以将同一个块传递给sort_by进行排序:

arr.sort_by do |str|
  next [ str ] unless str =~ OUTCOME_PARTS_EXPR
  [ $1, $2.to_i ]
end
# => [ "Gain 1 pound",
#      "Gain 10 pounds",
#      "Gain 15 pounds",
#      "Lose 1 pound",
#      "Lose 10 pounds",
#      "Lose 15 pounds",
#      "Reduce body fat by 2%",
#      "Reduce body fat by 15%" ]

大!但还有最后一步......

4。对ActiveRecord查询返回的集合执行此操作

我们没有对字符串数组进行排序,我们正在对Target个对象进行排序。然而,这并不是一个复杂的问题;我们只需要指向正确的属性,即outcome

Target.all.sort_by do |target|
  outcome = target.outcome
  next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
  [ $1, $2.to_i ]
end

请注意,我将Target.order(:outcome)更改为Target.all。由于我们在Ruby中进行排序,因此在数据库查询中进行排序可能没有意义。

5。现在一起

为了清理,你应该把它放在目标模型中,例如:

class Target < ActiveRecord::Base
  OUTCOME_PARTS_EXPR = /(\D+)(\d+)/

  # ...

  def self.sort_by_outcome
    all.sort_by do |target|
      outcome = target.outcome
      next [ outcome ] unless outcome =~ OUTCOME_PARTS_EXPR
      [ $1, $2.to_i ]
    end
  end
end

作为一种类方法,您可以将其称为Target.sort_by_outcome,或者将其置于Target.where(...).sort_by_outcome之类的查询的末尾。因此,在您的情况下,您希望将其放在控制器中(根据经验,您不应该在视图中执行ActiveRecord查询):

@targets_sorted = Target.sort_by_outcome

...然后在您看来,您将使用@targets_sorted代替Target.order(:outcome)