-1

I'm looking for a way to qualify if a filename ends with a suffix inside an array.

filetypes = %w(.flac .wv)
Dir.open(Dir.pwd).each do |filename|
  q = "." + filename.split('.').last.to_s
  pp filename if filetypes.include?(q)
end

I'm thinking there has to be some kind of built-in iterator that traverses the qualifier array. Am I missing something here?

filetypes = %w(.flac .wv)
Dir.open(Dir.pwd).each do |filename|
  pp filename if filename.end_with?(filetypes)
end

This doesn't work. The docs say this:

end_with?([suffixes]+) → true or false

So it suggests an array can be traversed, but it's not working for me. How can I pass an array to end_with??

anothermh
  • 9,815
  • 3
  • 33
  • 52
Rich_F
  • 1,830
  • 3
  • 24
  • 45
  • 1
    Maybe it should be `filetypes = %w(.flac .wv)`, instead of `a = %w(.flac .wv)`? – iGian Oct 24 '18 at 21:06
  • Yes, sorry, I missed that. Will fix. – Rich_F Oct 24 '18 at 21:07
  • 1
    In general, I think you would find it more helpful to ask how to achieve a certain end rather than restricting consideration to a specific approach (here using `end_with?`). The latter are sometimes referred to as ["XY problems"](https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). – Cary Swoveland Oct 24 '18 at 22:01
  • @CarySwoveland OK an enumerable, but no method on the string object. It seems an Enumerable is the quickest way. – Rich_F Oct 24 '18 at 22:02
  • @CarySwoveland My question wasn't how to get this to work as I did in the original post. That wasn't my goal. In my own interest, I was looking to see if there was indeed a method to do this as opposed to any Enumerable. – Rich_F Oct 24 '18 at 22:05
  • 1
    I was going by the title of the question. – Cary Swoveland Oct 24 '18 at 23:03

2 Answers2

5

You're just missing the splat operator (*):

filetypes = %w(.flac .wv)

Dir.open(Dir.pwd).each do |filename|
  pp(filename) if filename.end_with?(*filetypes)
end
Scott Schupbach
  • 1,284
  • 9
  • 21
1

You can do this with a simple one-liner that avoids using end_with?:

Dir[Dir.pwd + '/*.{flac,wv}'].each { |filename| pp filename }

For your question about what values are accepted by end_with?, the answer is that it accepts a comma-delimited list of values, e.g.:

'foo'.end_with?('a', 'b', 'c')
=> false
'foo'.end_with?('a', 'b', 'c', 'o')
=> true

The documentation example of end_with?([suffixes]+) → true or false can be read as "end_with? accepts one or more suffixes delimited by commas, which will be treated as an array by the receiving object, and if the string ends with any of the suffixes then return true, else return false."

You can also use the splat operator to convert an array to an acceptable value for end_with?:

'foo'.end_with?(*['a', 'b', 'c'])
=> false
'foo'.end_with?(*['a', 'b', 'c', 'o'])
=> true

Update for clarification of examples given

Judging from the comments below, Rich_F was extremely confused by this answer about the splat operator so I will clarify my answer to help ensure that no one else is confused by the example I have provided.

The splat operator * in Ruby will convert an array's contents to a comma-delimited list of arguments. It is often used when passing an array as an argument to a method, for example:

array = ['a', 'b', 'c']
'foo'.end_with?(*array)

This will "splat" the array into a comma delimited list of arguments, and is functionally equivalent to:

'foo'.end_with?(*['a', 'b', 'c'])

Likewise, it is functionally equivalent to:

'foo'.end_with?('a', 'b', 'c')

Likewise, it is functionally equivalent to:

array = %w(a b c)
'foo'.end_with?(*array)

Likewise, it is functionally equivalent to:

'foo'.end_with?(*%w(a b c))

Likewise, it is functionally equivalent to:

filetypes = %w(.flac .wv)
filename.end_with?(*filetypes)

Given this knowledge about how the splat operator affects arrays and the examples for passing arrays as arguments to methods, one can deduce that the given code example in the question can be altered as follows:

filetypes = %w(.flac .wv)
Dir.open(Dir.pwd).each do |filename|
  pp filename if filename.end_with?(*filetypes)
end

The only change is to add *. By adding the splat operator * before the argument filetypes, the filetypes array will be expanded as comma-delimited arguments for the call to end_with?.

This is not an efficient solution to the problem because it requires iterating over the filetypes array once for every iteration of the directory listing and iterates over all objects in the directory regardless of matching, as opposed to using Dir[Dir.pwd + '/*.{flac,wv}'].each { |filename| pp filename } which returns only the relevant files and does not have to iterate over a second array for each iteration. As such, using end_with? in the example given is massively inefficient and should not be used in any real code.

For example, given a directory with 10,000 files ending in .flac:

require 'benchmark'

dir = Dir[Dir.pwd + '/*.{flac,vw}']; nil
10.times { puts Benchmark.measure { 10000.times { dir.each { |filename| nil } } } }
  4.271809   0.001728   4.273537 (  4.274684)
  4.279765   0.002286   4.282051 (  4.283524)
  4.334877   0.004689   4.339566 (  4.343982)
  4.269334   0.001593   4.270927 (  4.272033)
  4.256148   0.001545   4.257693 (  4.258734)
  4.261371   0.001733   4.263104 (  4.264229)
  4.254568   0.001085   4.255653 (  4.256379)
  4.259886   0.001245   4.261131 (  4.261711)
  4.258024   0.001964   4.259988 (  4.261133)
  4.236385   0.001142   4.237527 (  4.238184)

In comparison to the original example in the question, which is over twice as slow:

require 'benchmark'

dir = Dir[Dir.pwd + '/*']; nil
filetypes = %w(flac vw); nil
10.times { puts Benchmark.measure { 10000.times { dir.each { |filename| nil if filename.end_with?(*filetypes) } } } }
   9.509041   0.003634   9.512675 (  9.514197)
   9.484041   0.003079   9.487120 (  9.488686)
   9.508674   0.002872   9.511546 (  9.512768)
   9.508382   0.002809   9.511191 (  9.512343)
   9.762489   0.011043   9.773532 (  9.783415)
   9.607308   0.005655   9.612963 (  9.616716)
   9.962166   0.009848   9.972014 (  9.978026)
   9.621152   0.005883   9.627035 (  9.631075)
  10.811991   0.010787  10.822778 ( 10.831729)
  10.461568   0.013571  10.475139 ( 10.487688)

The difference becomes much more pronounced when the number of matching files is far below the number of non-matching files. Here, the directory has 1,000 .flac files and 9,000 .txt files:

require 'benchmark'

dir = Dir[Dir.pwd + '/*.{flac,vw}']; nil
10.times { puts Benchmark.measure { 10000.times { dir.each { |filename| nil } } } }
  0.384366   0.000210   0.384576 (  0.384669)
  0.384336   0.000186   0.384522 (  0.384717)
  0.386674   0.000178   0.386852 (  0.386947)
  0.383575   0.000075   0.383650 (  0.383671)
  0.382555   0.000090   0.382645 (  0.382692)
  0.384618   0.000048   0.384666 (  0.384677)
  0.384687   0.000199   0.384886 (  0.384976)
  0.386517   0.000193   0.386710 (  0.386842)
  0.386167   0.000132   0.386299 (  0.386388)
  0.390683   0.000093   0.390776 (  0.390817)

Compared to the original example using the splat operator, which is over 36 times slower:

require 'benchmark'

dir = Dir[Dir.pwd + '/*']; nil
filetypes = %w(flac vw); nil
10.times { puts Benchmark.measure { 10000.times { dir.each { |filename| nil if filename.end_with?(*filetypes) } } } }
  11.182205   0.014303  11.196508 ( 11.210042)
  11.154286   0.011573  11.165859 ( 11.178611)
  11.081012   0.009853  11.090865 ( 11.098028)
  11.084294   0.009361  11.093655 ( 11.101870)
  10.990442   0.007118  10.997560 ( 11.002036)
  11.044119   0.009284  11.053403 ( 11.058608)
  11.072604   0.009114  11.081718 ( 11.087941)
  11.127151   0.009354  11.136505 ( 11.143270)
  11.172101   0.012262  11.184363 ( 11.195138)
  11.126791   0.010767  11.137558 ( 11.145617)
anothermh
  • 9,815
  • 3
  • 33
  • 52
  • That's what I was thinking, then wanting that to see an input array somehow expressed as that comma delimited suffices. But I hope this thread shows the wish to qualify through an array instead of `Array.include?('stringthing')`. – Rich_F Oct 24 '18 at 21:11
  • 1
    I have no idea what you were trying to say in this comment and I've read it multiple times. If you're saying you still want to pass an array, then use the splat operator `*` on your array as demonstrated in the most recent edit to my answer. – anothermh Oct 24 '18 at 21:16
  • Sorry, yes, you are right on with what you said. `myarray.*',')` works. – Rich_F Oct 24 '18 at 21:18
  • It hasn't. If you read the post, there is nothing for a reciprocating iteration through an array of qualifiers. Yes I have written a workaround, but the question was about a method that iterates through an array. Please read the original post. – Rich_F Oct 24 '18 at 21:28
  • OK, you have edited it and you want me to say that you have given me the answer. You didn't. You gave me a workaround instead of a method I thought I missed. Please stop editing posts then demanding recognition for something you didn't provide. – Rich_F Oct 24 '18 at 21:31
  • I have no idea what you mean about a "reciprocating iteration through an array of qualifiers." If you can explain your question in clear English then I recommend you edit your original question to include that question. The edit you're referring to was made after you said "Sorry, yes, you are right on with what you said," indicating it was the correct answer, and was also made to correct multiple transcription errors in your code example. – anothermh Oct 24 '18 at 21:32
  • See [How do I ask a good question?](https://stackoverflow.com/help/how-to-ask) -- your original question explicitly asked only one thing: "Am I missing something?" which is a bad question, and implicitly asked one thing: "it suggests an array can be traversed, but it's not working for me" -- my edit made your implicit question explicit, because _Questions_ should include an actual question. – anothermh Oct 24 '18 at 21:46
  • Read the post. What you provided, an iterator, was already in the post. It is very clean. Even before you messed with it. Reading English takes context. It's that simple. If you posted an iterator to answer something that had an iterator in it, you've made no sense. – Rich_F Oct 24 '18 at 22:25
  • It's been answered by someone else who got the whole post. It seems yet again, you aren't paying attention. Good luck with that. – Rich_F Oct 24 '18 at 22:40
  • The other answer says "use the splat operator." My answer says "use the splat operator." If you're confused or don't understand the example I gave you then I'm happy to explain to you how they are identical answers. – anothermh Oct 24 '18 at 22:47
  • I have added an extensive explanation of my answer including multiple examples demonstrating the use of `*`. I have also included benchmarks showing why the approach is poor. I hope that it will help you to understand both how and why the splat operator works, and enable you to be a better Ruby programmer overall. Good luck. – anothermh Oct 25 '18 at 00:03