如何对collection_select由带有数字的字符串组成的下拉菜单进行排序



我正在构建一个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 排序方法处理这种排序,或者我是否需要编写自定义排序方法? 如何最好地实现我想要的结果? 谢谢!

TL;DR

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

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#map我们可以为整个枚举完成此操作,当我们使用它时,我们可以将数字转换为 Fixnum(例如 "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. 对活动记录查询返回的集合执行此操作

我们不是在对字符串数组进行排序,而是对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. 现在一起

为了清理问题,您可能应该将其放在Target模型中,例如:

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)

相关内容

最新更新