You could simplify your code quite a bit. Here's one way:
def find_empties_sorted(directory, dirprefix, fsuffix)
Dir["#{directory}/#{dirprefix}**/"].each_with_object([]) { |e,arr|
arr << e[/[^\/]+(?=\/$)/] if Dir["#{e}**.#{fsuffix}"].empty? }
.each_with_object ({}) do |x,h|
n = x[/\d+$/]
ni = n ? n.to_i : n
(h[ni] ||= []) << (n ? x[0,x.size-n.size] + ni.to_s : x)
end.sort { |(k1,_), (k2,_)| k1.nil? ? (k2.nil? ? 0 : 1) : (k2.nil? ? -1 : k1<=>k2) }
.transpose
.last
.flatten
end
For the data you used, this would return:
#=> ["e1", "e2", "e3", "e4", "e4", "e5",
# "e9", "e10", "e11", "emptier", "empty"]
If you want /
at the end of each subdir, change .flatten
to
.flatten.map { |s| s << '/' }
#=> ["e1/", "e2/", "e3/", "e4/", "e4/", "e5/",
# "e9/", "e10/", "e11/", "emptier/", "empty/"]
I've used Enumerable#each_with_object (v1.9+) twice, as
it provides a convenient way to build an object such as an array or hash (which is returned).
In the first case the object is an initially-empty array (with block variable arr
); in the second it is an initially-empty hash (with block variable h
).
I will use your data--with two changes--to explain what's going on here. I've added an empty directory e
and a file f.txt
to the directory to testdir
.
directory = 'testcase'
dirprefix = 'e'
fsuffix = 'txt'
a = Dir["#{directory}/#{dirprefix}**/"].each_with_object([]) { |e,arr|
arr << e[/[^\/]+(?=\/$)/] if Dir["#{e}**.#{fsuffix}"].empty? }
#=> [ "e", "e003", "e004", "e1", "e10", "e11",
# "e2", "e4", "e5", "e9", "emptier", "empty"]
A word about the regex /[^\/]+(?=\/$)/
(which I've used to avoid the need to chdir
). [^\/]+
looks for a string that contains one or more characters other than /
. (?=\/$)
is called a positive lookahead. It is not part of the match (which is why it is sometimes referred to as having "zero width"), but it requires that the match be followed by /$
, where $
signifies the end-of-line. This strips off directory
from the beginning of e
. To make it easier to remove leading zeros on trailing numbers, I've not included /
at the end of each subdir. (It can be added at the end if desired.)
Next we execute
b = a.each_with_object ({}) do |x,h|
n = x[/\d+$/]
ni = n ? n.to_i : n
(h[ni] ||= []) << (n ? x[0,x.size-n.size] + ni.to_s : x)
end
Initially the hash h
is empty. x
is first assigned the value "e" (from the array a
). We compute
n = x[/\d+$/] #=> nil (because no trailing number)
ni = n ? n.to_i : n #=> nil
(h[ni] ||= []) << (n ? x[0,x.size-n.size] + ni.to_s : x) # h => { nil=>["e"] }
h[ni] ||= []
is shorthand for h[ni] = h[ni] || []
, so if h[ni]
is undefined (nil
), as it is here, ||
causes h[ni] => []
; if h[i]
is already defined as an array (so is neither nil
nor false
),[]
is not considered. Alternatively (as @Nat mentioned), we could substitute (Hash.new { |h,k| h[k] = [] })
for ({})
following each_with_object
.
Next, x
is assigned the value "e003"
. We compute
n = x[/\d+$/] #=> "003"
ni = n ? n.to_i : n #=> 3
(h[ni] ||= []) << (n ? x[0,x.size-n.size] + ni.to_s : x)
# h => { nil=>["e"], 3=>["e3"] }
Continuing through the elements of a, we obtain:
b = {nil=>["e", "emptier", "empty"], 3=>["e3"], 4=>["e4", "e4"], 1=>["e1"],
10=>["e10"], 11=>["e11"], 2=>["e2"], 5=>["e5"], 9=>["e9"]}
We now sort the keys. b.sort
would raise the exception ArgumentError: comparison of Array with Array failed
because Fixnum objects
cannot be compared with nil
. We therefore need to define the "spaceship" method, <=>
that
sort
is to use. Since we want the key nil
to be last, we do it this way:
c = b.sort { |(k1,_), (k2,_)| k1.nil? ? (k2.nil? ? 0 : 1) : (k2.nil? ? -1 : k1<=>k2) }
#=> [[1, ["e1"]], [2, ["e2"]], [3, ["e3"]], [4, ["e4", "e4"]], [5, ["e5"]],
# [9, ["e9"]], [10, ["e10"]], [11, ["e11"]], [nil, ["e", "emptier", "empty"]]]
Note that sort
returns an array. We could have written:
c = b.sort { |(k1,v1), (k2,v2)|...
where k1 => v1
is one member of the hash and k1 => v1
is another, but since we are sorting on the keys, and do not use the values v1
and v2
, we can replace the latter with underscores.
We could have done this differently, using sort
without a block, had we used n
instead of nil
for the key for directories without trailing numbers, and chosen n
so that it was larger than any of the other keys. This is one way to do that:
empties = find_empties_sorted(directory, dirprefix, fsuffix)
Dir["#{directory}/#{dirprefix}**/"].each_with_object([]) { |e,arr|
arr << e[/[^\/]+(?=\/$)/] if Dir["#{e}**.#{fsuffix}"].empty? }
n = ('1' + '0'*empties.map(&:size).max).to_i #=> 10000000
We are almost finished. Next we transpose to obtain an array with only two elements:
d = c.transpose
#=> [[1, 2, 3, 4, 5, 9, 10, 11, nil],
# [["e1"], ["e2"], ["e3"], ["e4", "e4"], ["e5"],
# ["e9"], ["e10"], ["e11"], ["e", "emptier", "empty"]]]
As we only want the second element:
e = d.last
#=> [["e1"], ["e2"], ["e3"], ["e4", "e4"], ["e5"],
# ["e9"], ["e10"], ["e11"], ["e", "emptier", "empty"]]
And lastly, we must flatten:
e.flatten
#=> ["e1", "e2", "e3", "e4", "e4", "e5",
# "e9", "e10", "e11", "e", "emptier", "empty"]
This array is returned by the method.
If you would prefer less chaining, go ahead and add temporary variables and/or additional methods. The more you work with Ruby, however, the more chaining becomes the natural thing to do. It's no more difficult to debug, as you can do what I did in explaining what I've done.
In deciding the proper balance between conciseness and the use of temporary variables and separate methods, I'm always thinking about how the code reads. Aside from performance, my main objectives are to make my code read well and read fast. I want an experienced Rubyist to be able to read and comprehend it in minimum time. Personally, I much prefer reading tightly-chained code to code that is all spread out, uses lots of very short methods, lots of temporary variables with highly-descriptive (and accordingly, long) names, and so on.
It would take very little time (well under 30 seconds) for any experienced Rubyist to scan my code and have a general idea of what I was doing: creating an array of directories, converting that to a hash, sorting the hash on its keys, transposing the resulting array, extracting its last element and flattening. Yes, they would have to look more closely at certain parts, such as the regexes, sort criteria, and so on, and for that I would help by adding a few comments. Comprehension of some parts of the code, including .transpose
, .last
and .flatten
would be virtually automatic, so it makes perfect sense to express those three operations as compactly as possible: here, three words separated by dots.
What I've just said is the opinion of one Ruby hobbiest. I invite others--particularly those who write Ruby code for a living--to weigh-in on these issues by adding comments.