John
John

Reputation: 237

Ruby select method selecting values that do not meet criteria

Have the following code which should select every other character of a string and make a new string out of them:

def bits(string)
  string.chars.each_with_index.select {|m, index| m if index % 2 == 0}.join
end

However, select returns this output with test case "hello":

"h0l2o4"

When using map instead I get the desired result:

"hlo"

Is there a reason why select would not work in this case? In what scenarios would it be better to use map over select and vice versa

Upvotes: 0

Views: 925

Answers (3)

Cary Swoveland
Cary Swoveland

Reputation: 110685

If you use Enumerable#map, you will return an array having one element for each character in the string.

arr = "try this".each_char.map.with_index { |c,i| i.even? ? c : nil }
  #=> ["t", nil, "y", nil, "t", nil, "i", nil]

which is the same as

arr = "try this".each_char.map.with_index { |c,i| c if i.even? }
  #=> ["t", nil, "y", nil, "t", nil, "i", nil]

My initial answer suggested using Array#compact to remove the nils before joining:

arr.compact.join
  #=> "tyti"

but as @npn notes, compact is not necessary because Array#join applies NilClass.to_s to the nil's, converting them to empty strings. Ergo, you may simply write

arr.join
  #=> "tyti"

Another way you could use map is to first apply Enumerable#each_cons to pass pairs of characters and then return the first character of each pair:

"try this".each_char.each_cons(2).map(&:first).join
  #=> "tyti"

Even so, Array#select is preferable, as it returns only the characters of interest:

"try this".each_char.select.with_index { |c,i| i.even? }.join
  #=> "tyti"

A variant of this is:

even = [true, false].cycle
  #=> #<Enumerator: [true, false]:cycle> 
"try this".each_char.select { |c| even.next }.join
  #=> "tyti"

which uses Array#cycle to create the enumerator and Enumerator#next to generate its elements.

One small thing: String#each_char is more memory-efficient than String#chars, as the former returns an enumerator whereas the latter creates a temporary array.

In general, when the receiver is an array,

Me, I'd use a simple regular expression:

"Now is the time to have fun.".scan(/(.)./).join
  #=> "Nwi h iet aefn"

Upvotes: 0

nPn
nPn

Reputation: 16748

The reason that select does not work in this case is that select "Returns an array containing all elements of enum for which the given block returns a true value" (see the doc here), so what you get in your case is an array of arrays [['h',0],['l',2],['o',4]] which you then join to get "h0l2o4".

So select returns a subset of an enumerable. map returns a one to one mapping of the provided enumerable. For example the following would "fix" your problem by using map to extract character from each value returned by select.

def bits(string)
  string.chars.each_with_index.select {|m, index| m if index % 2 == 0}.map { |pair| pair.first }.join
end

puts(bits "hello") 
=> hlo

For lots of reasons this is not a good way to get every other character from a string however.

Here is another example using map. In this case each index is mapped to either the character or nil then joined.

def bits(string)
  string.chars.each_index.map {|i| string[i] if i.even? }.join
end

Upvotes: 0

kcdragon
kcdragon

Reputation: 1733

If you still want to use select, try this.

irb(main):005:0> "hello".chars.select.with_index {|m, index| m if index % 2 == 0}.join
=> "hlo"

each_with_index does not work because it is selecting both the character and the index and then joining all of that.

Upvotes: 1

Related Questions