Python has an elegant syntax for taking all or part of a list and optionally transforming its values, should they meet zero or more conditions. It's called "list comprehensions", and looks like this:
In [1]: [x for x in range(10) if x % 2 == 0] Out[1]: [0, 2, 4, 6, 8]
(...er, that's without the transformation part. But that's irrelevant for this question...)
I'm not finding a "simple" or "elegant" way of doing this in ruby, but maybe I just don't know where to look. The best I've found is this:
irb(main):031:0> (0..10).collect {|x| x unless x % 2 == 0} .compact
=> [1, 3, 5, 7, 9]...which, shall we say, is inelegantly adequate. You have to add the .compact part to remove the
nil
s that will be left in otherwise:
irb(main):032:0> (0..10).collect {|c| c unless c%2==0}
=> [nil, 1, nil, 3, nil, 5, nil, 7, nil, 9, nil]What's the better way? I'm hoping there is one...
Some ideas
Ok, so I'm poking around to see if I can answer your call, and learn something myself.
Might this do the trick:
a.delete_if {|x| x % 2 == 0}
I realize this only handles deletes, but I think that's the only special case where you're running into the nil issue. Otherwise, the collect! method should work for other types of transformations...
Am hardly an expert, but
Am hardly an expert, but these both achieve the same without the additional method:
(0..10).find_all {|x| x if x % 2 == 0}(0..10).select {|x| x if x % 2 == 0}Oh, nevermind ...
you don't just want to select the values of course.
I'd probably do something
I'd probably do something like this:
(0..10).find_all {|x| x % 2 == 0}
Honestly, I consider list comprehensions to be one of the least elegant aspects of python--which says a lot about the overall elegance of the language I guess.
d'oh
Ok, so I meant:
(0..10).find_all {|x| x % 2 != 0}
erk.
Thanks, all for the suggestions. I should have been more precise: I want to be able to do arbitrary tranformations in-line, too. More like the following python:
Trying all of your suggestions above, I'm stuck with:
irb(main):015:0* (0..10).find_all {|x| x*3 if x%2 == 0} => [0, 2, 4, 6, 8, 10] irb(main):016:0> (0..10).select {|x| x*3 if x%2 == 0} => [0, 2, 4, 6, 8, 10] irb(main):017:0> (0..10).collect {|x| x*3 if x%2 == 0} => [0, nil, 6, nil, 12, nil, 18, nil, 24, nil, 30] irb(main):018:0> (0..10).collect {|x| x*3 if x%2 == 0}.compact => [0, 6, 12, 18, 24, 30]That's frustrating. Why aren't the values transforming with select and find_all? Is something wrong above? This is ruby-1.8.4, locally built on centos4-x86_64, fwiw.
ah! list, not range.
This works for lists, whereas above all the examples are ranges. Interesting.
irb(main):043:0> d = [0,1,2,3,4,5,6,7,8,9,10] => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] irb(main):044:0> d.select {|x| x*3 if x%2==0} => [0, 2, 4, 6, 8, 10] irb(main):045:0> d.find_all {|x| x*3 if x%2==0} => [0, 2, 4, 6, 8, 10]Hmm. Why is a range not like a list? /me scratches head and scores one for python on being more intuitive here.
Transform the values with map
I think what you want is something like:
(1..10).find_all {|x| x%2 == 0}.map {|x| x*3}
Find and map in one step
If you really want to find and map with one block you could roll it up yourself like this:
You have to add it to Range separately, but we can just pass the buck to the new Array method:
Now you can use find_map with an Array or a Range like so:
P.S. How do you do this insanely great code format preserving and syntax highlighting? It's fantastic!
Doesn't anyone here know Ruby?
Not sure what a range is in Python, but I'd say it is wrong. (Read on for why)
python: [x for x in range(10) if x % 2 == 0]
Out[1]: [0, 2, 4, 6, 8]
To get this same result for Ruby, it takes one more character
(notice the triple '...')
ruby: (0...10).select {|x| x if x % 2 == 0}
[0, 2, 4, 6, 8]
I would expect range to include both upper and lower bounds:
ruby: (0..10).select {|x| x if x % 2 == 0}
[0, 2, 4, 6, 8, 10]
The trouble with list comprehensions, IMO, is that they are special.
And what a silly name (IMO). Block are pervasive in Ruby.
Nothing special here. No extra brain power at all.
A simple 'ri Array' would have found the select method.
----------------------------------------------------------- Array#select
array.select {|item| block } -> an_array
------------------------------------------------------------------------
Invokes the block passing in successive elements from _array_,
returning an array containing those elements for which the block
returns a true value (equivalent to +Enumerable#select+).
a = %w{ a b c d e f }
a.select {|v| v =~ /[aeiou]/} #=> ["a", "e"]
Notice how you can expect Ruby to do the right thing and the iterator
treats the Range similar to an array.
select usage
The block passed to select needs to return a boolean, not a value.
>> (0..10).select {|x| x % 2 == 0} => [0, 2, 4, 6, 8, 10]There was no need for x if x % 2 ==0, only the boolean. Amusing choice of title, though.
I see the selects being written here attempting to operate on the variable. If this does do anything, it will be in-place on the array (or range) that you're operating on. For an array, this is not really a problem, other than you probably didn't want to do that:
>> a = ('a'..'z').to_a => ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"] >> a.select do |l| l =~ /[aeiou]/ ? (l << '-mod'; l) : nil end => ["a-mod", "e-mod", "i-mod", "o-mod", "u-mod"] >> a => ["a-mod", "b", "c", "d", "e-mod", "f", "g", "h", "i-mod", "j", "k", "l", "m", "n", "o-mod", "p", "q", "r", "s", "t", "u-mod", "v", "w", "x", "y", "z"]Obviously, you could make a copy of the array at this point, and it would solve the problem, at the obvious cost. See at the end for some examples of another way to achieve a similar effect in 'one line'.
With a range, the outcome is different:
>> a = ('a'..'z') => "a".."z" >> a.select do |l| l =~ /[aeiou]/ ? (l << '-mod'; l) : nil end => ["a-mod"] >> a => "a-mod".."z"Why? because you've in-place modified the range. Now, what is an enumerator to do with a range, when you've modified it in-line? What is the next item from 'a-mod'..'z' from 'a'?
'a' is no longer in the range.
If you want numeric iteration over a list, you first of all need a list!
>> r=[]; ('a'..'z').each {|v| r << v if v =~ /[aeiou]/}; r => ["a", "e", "i", "o", "u"]The last ';var' is only for irb to print the array we just created, you can remove it, or, if you really really want to, you could do this (might be suitable for a function)...
There are some things in the ruby grammar I truly love, no matter how 'useless' they may seem.
Inject
Another try
I have just learnt Ruby today so I may be wrong, but I would first filter with 'select' and then apply the function with 'collect':
(0..10).select {|x| (x%2) == 0 }.collect {|x| 3*x} => [0, 6, 12, 18, 24, 30]Is this idiomatic Ruby?
Post new comment