我正在构建一个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)
。