7

Yes it is a duplicate of Is there a natural_sort_by method for Ruby? , but i think dawg and Eric made it clearer here, at least to me these answers are more exhaustive..

I have an array like this:

arr = ["file1.txt", "file11.txt", "file12.txt", "file2.txt", "file3.txt"]

And i want it to be sorted like this:

arr = ["file1.txt", "file2.txt", "file3.txt", "file11.txt", "file12.txt"]

How can i do this? I tried with sort, but it's not very clear to me..

I sorted the files by size like this:

files=Dir.entries("./").sort { |f| File.size(f) }.select { |f| File.file?(f) }
lucaortis
  • 430
  • 2
  • 11

2 Answers2

5

Eric's answer is great way of doing a Natural Sort Order for the digits only in file names. Works if all file names have the same prefix.

If you want to add a second element (for example, file names that do not have digits in them) you can great a multi-element sort_by be creating a list:

filenames = ["file1.txt", "file11.txt", "file12.txt", "file2.txt", "file3.txt","file.txt", "File.txt"]
filenames.sort_by{ |name| [name[/\d+/].to_i, name] }
=> ["File.txt", "file.txt", "file1.txt", "file2.txt", "file3.txt", "file11.txt", "file12.txt"]

The two element of the sort_by implements:

  1. Integer value of digits if any are found with a regex name[/\d+/].to_i then
  2. Name if no digits or same digits name.

More robustly, you can split the entire string by digits and convert each to an int:

> "abc123def456gh".split(/(\d+)/).map{ |e| Integer(e) rescue e}
=> ["abc", 123, "def", 456, "gh"]

So your Natural Sort becomes:

arr.sort_by{ |s| s.split(/(\d+)/).map{ |e| Integer(e) rescue e}}

So now names and numbers (even multiples names and numbers) are handled correctly:

> arr = ["file1.txt", "file11.txt", "file12.txt", "file2.txt", "file3.txt", "gfile10.txt", "gfile1.txt", "gfile.txt", "file.txt", "afile.txt","afile10.txt","afile2.txt" ]
> arr.sort_by{ |s| s.split(/(\d+)/).map{ |e| Integer(e) rescue e}}
=> ["afile2.txt", "afile10.txt", "afile.txt", "file1.txt", "file2.txt", "file3.txt", "file11.txt", "file12.txt", "file.txt", "gfile1.txt", "gfile10.txt", "gfile.txt"]
dawg
  • 98,345
  • 23
  • 131
  • 206
  • This works, but as i commented on question, ubuntu sorts the files fine if we do sort by name. So my guess is there must be something built in in ruby which could be used for this. For now, i have no choice but to use your solution. Thanks. – Zia Ul Rehman Mughal Jan 13 '20 at 11:47
4

You can extract the first number out of the filename, convert it to an integer and use it inside sort_by:

filenames = ["file1.txt", "file11.txt", "file12.txt", "file2.txt", "file3.txt"]
filenames.sort_by{ |name| name[/\d+/].to_i }
# ["file1.txt", "file2.txt", "file3.txt", "file11.txt", "file12.txt"]

/\d+/ is a regex which means "a sequence of 1 or more digits":

"test123"[/\d+/]
# => "123"
"test"[/\d+/]
# => nil

Note that it only sorts by number and ignores the rest:

["a2", "b1", "z3"].sort_by{ |name| name[/\d+/].to_i }
# => ["b1", "a2", "z3"]
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124