I have a list of files with a bunch of attributes. One of the attributes is the file name which is how I would like to sort the list. However, the list goes something l开发者_如何学Pythonike this: filename 1, filename 2, filename 10, filename 20.
The ruby sort_by method produces this:
files = files.sort_by { |file| file.name }
=> [filename 1, filename 10, filename 2, filename 20]
I would like a more human readable list like filename 1, filename 2, filename 10, filename 20
I found the natural_sort gem but it seems to only work like the sort method. I need something where I can specify what to sort the array by.
Any help?
Here's another take on a "natural" sort method:
class String
def naturalized
scan(/[^\d\.]+|[\d\.]+/).collect { |f| f.match(/\d+(\.\d+)?/) ? f.to_f : f }
end
end
This converts something like "Filename 10"
into a simple array with floats in place of numbers [ "Filename", 10.0 ]
You can use this on your list:
files.sort_by! { |file| file.name.to_s.naturalized }
This has the advantage of working on arbitrary numbers in unpredictable positions. The paranoid .to_s
call in that block is to ensure that there is a string and not an inadvertent nil
when sorting.
generic answer for strings natural sort
array.sort_by {|e| e.split(/(\d+)/).map {|a| a =~ /\d+/ ? a.to_i : a }}
I've created a natural sort gem. It can sort by an attribute like this:
# 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">]
As long as files are always named "file #"
, you could do
files.sort_by{|f| f.name.split(" ")[1].to_i }
This splits on the space, and grabs the number to do the sorting.
Natural Sort gem.
Install
gem "natural_sort"
Usage
list = ["a10", "a", "a20", "a1b", "a1a", "a2", "a0", "a1"]
list.sort(&NaturalSort) # => ["a", "a0", "a1", "a1a", "a1b", "a2", "a10", "a20"]
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 }}
This can compare arrays by arrays in the sort_by method even if the type of the matching items differ. Even if there are deeper nested arrays. Example:
[ "3 a 22", "b 22 1", " b 5 ", [11, 2, [4, 5]] ] #=>
[ "3 a 22", [11, 2, [4, 5]], " b 5 ", "b 22 1" ]
The point here is that during the sort if an item is a nested array then we convert it to a string beforehand. And if parts of the string contain digits only then we do not convert them to numeric values but instead extend them with spaces, like:
30 #=> " 30"
This way all objects will be compatible strings and the sorting will be able to compare them resulting in a numeric sort if the matching objects at their positions are numbers only.
It is sorting correctly. The problem here is that the names aren't good to sort the way you want. In means of string, 10 comes before 2 and 21 comes before 5.
If you want it to sort it like it was numbers, you have 2 approaches:
1 - Change all your listings to add a leading 0 before numbers with just one digit.
2 - Do as William suggested, aplit the name, transform the string to integer and sort by it.
I would recommend option 1 since the second rely on the padronization of the names.
精彩评论