Ruby中是否有自然排序的方法?

18

我有一个文件列表,其中包含许多属性。其中一个属性是文件名,这也是我希望按照其排序的方式。但是,该列表的顺序类似于:filename 1,filename 2,filename 10,filename 20。

Ruby的sort_by方法会产生以下结果:

files = files.sort_by { |file| file.name }
=> [filename 1, filename 10, filename 2, filename 20]

我希望能够得到更加易于理解的列表,例如filename 1、filename 2、filename 10、filename 20。

我找到了natural_sort gem,但它似乎只像sort方法那样工作。我需要一些可以指定按照什么对数组进行排序的东西。

有人能提供帮助吗?

7个回答

29

这是另一种“自然排序”方法的实现:

class String
  def naturalized
    scan(/[^\d\.]+|[\d\.]+/).collect { |f| f.match(/\d+(\.\d+)?/) ? f.to_f : f }
  end
end

这将类似于"文件名 10"的内容转换为一个简单的数组,其中数字被浮点数所替代,即[ "文件名",10.0 ]

您可以在列表中使用此功能:

files.sort_by! { |file| file.name.to_s.naturalized }

这样做的好处是可以在不可预测的位置对任意数字进行操作。该块中的谨慎使用.to_s 调用是为了确保在排序时有一个字符串而不是无意的nil


哇,这真是神奇。在我的用例中,标识符可能会被“.”分隔。因此,我从scan()中使用的正则表达式中删除了两个'.'。我认为这不会破坏任何东西。 - Yannick Wurm
这意味着任何带有小数点的值都将被解释为单独的数字。10.2会在10.1之后但在10.11之前。 - tadman
没错,我必须删除 .'s(还有第三个!),否则 3.3 和 3.25 将排序错误。所以:scan(/[^\d]+|[\d]+/).collect { |i| i.match(/\d+/) ? i.to_i : i } - Dan Kegel
1
然后浮点数似乎不是将单词转换为数字的正确方法。所以:scan(/[^\d]+|[\d]+/).collect { |w| w.match(/\d+/) ? w.to_i : w } - Dan Kegel
如果您不想要空格,您可以将\s添加到第一个正则表达式匹配器中,像这样:irb(main):021:0> "Filename 10".scan(/[^\s\d\.]+|[\d\.]+/) 会产生 => ["Filename", "10"]。否则,我看到 irb(main):020:0> "Filename 10".scan(/[^\d\.]+|[\d\.]+/) 会产生 => ["Filename ", "10"](请注意 "Filename" 后面的空格)。 - cdmo

19

字符串自然排序的通用答案

array.sort_by {|e| e.split(/(\d+)/).map {|a| a =~ /\d+/ ? a.to_i : a }}

1
这在简单的数组上失败,例如["a1", "aa"],因为[ "a", 1 ] <=> [ "a", "a" ]返回nil,而sort_by不能处理它。(我不确定为什么会返回nil。) - Zach Wily
本应该是\d+,我的错。 - shurikk

9
我已经创建了一个“自然排序宝石”(natural sort gem)的GitHub项目。它可以按照属性进行排序,如下所示:

natural sort gem

# Sort an array of objects by the 'number' attribute
Thing = Struct.new(:number, :name)
objects = [
  Thing.new('1.1', 'color'),
  Thing.new('1.2', 'size'),
  Thing.new('1.1.1', 'opacity'),
  Thing.new('1.1.2', 'lightness'),
  Thing.new('1.10', 'hardness'),
  Thing.new('2.1', 'weight'),
  Thing.new('1.3', 'shape')
  ]
Naturally.sort_by(objects, :number)

# => [#<struct Thing number="1.1", name="color">,
      #<struct Thing number="1.1.1", name="opacity">,
      #<struct Thing number="1.1.2", name="lightness">,
      #<struct Thing number="1.2", name="size">,
      #<struct Thing number="1.3", name="shape">,
      #<struct Thing number="1.10", name="hardness">,
      #<struct Thing number="2.1", name="weight">]

1
好棒的宝石! 顺便说一句,我可能错过了这个,因为我创建了一个回答建议使用你的宝石。现在才看到这个。我会保留我的答案,以便我们有更好的覆盖率,但是你做得很好。 - Joshua Pinter
@JoshuaPinter 谢谢! - Dogweather

6
只要文件名始终为"file #",就可以执行以下操作进行排序: files.sort_by{|f| f.name.split(" ")[1].to_i } 这将在空格处分割文件名,并获取数字进行排序。

[1] 返回由split返回的数组中的第二个项目,即数字。 - Teoulas
2
或者你可以使用.last代替[1],这样写:files.sort_by{|f| f.name.split(" ").last.to_i } - William
1
另外,split也可以将空格作为分隔符进行拆分,因此files.sort_by{|f| f.name.split.last.to_i }同样适用。只是为了让事情更加整洁 :) - William
我使用了 .last 方法,因为文件名中有许多空格间隔。 - Nate Bird
没有问题,但要记住,如果'filename'不总是以文件开头,则此方法可能不是最佳方法,因为它严格按数字排序,因此"awesomefile 50"将出现在"zoo file 1"之后。 - William
好的。我还有其他的排序标准,但我只是举了一个基本的例子来理解原则。谢谢! - Nate Bird

2

自然排序宝石

安装

gem "natural_sort"

用法

list = ["a10", "a", "a20", "a1b", "a1a", "a2", "a0", "a1"]
list.sort(&NaturalSort) # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]

该死,回到这里,这仍然是最好的方法。这是一个非常好用的小工具,可以自然地对事物进行排序。 - Joshua Pinter

0
array.sort_by{|x| ( x.class == Array ? x.join(" ") : x.to_s ).split(/(\d+)/).map{|x| x.to_s.strip }.select{|x| x.to_s != "" }.map{|x| x =~ /\d+/ ? x.to_s.rjust(30) : x }}

即使匹配项的类型不同,此方法可以通过sort_by方法比较数组与数组。即使存在更深层嵌套的数组。例如:

[ "3  a   22", "b  22     1", "   b  5  ", [11, 2, [4, 5]] ] #=>
[ "3  a   22", [11, 2, [4, 5]], "   b  5  ", "b  22     1" ]

这里的重点是,在排序过程中,如果一个项目是嵌套数组,则我们先将其转换为字符串。如果字符串的某些部分仅包含数字,则我们不会将它们转换为数值,而是用空格扩展它们,例如:
30 #=> "                         30"

通过将所有对象转换为兼容的字符串,以实现排序时能够进行比较,从而实现按数字排序,但仅限于其在位置上匹配对象是数值类型。

-3

它正在正确地排序。这里的问题是名称不适合按照您想要的方式进行排序。就字符串而言,10在2之前,21在5之前。

如果您希望像数字一样对其进行排序,则有两种方法:

1-更改所有清单,在只有一个数字的数字前面添加前导0。

2-像William建议的那样,拆分名称,将字符串转换为整数并按其排序。

我建议选择选项1,因为第二个选项依赖于名称的标准化。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接