Home > front end >  Conditional in Ruby Array.collect Iterator Block
Conditional in Ruby Array.collect Iterator Block

Time:10-19

Can anybody shine some light on why this is behaving like this? I'm expecting the collection to omit nil results, and only collect those that qualify under the condition of the include?. First and third result entries, I expect to not be there. For some reason, they are indeed included. What am I doing wrong?

f = [
{"title"=>"Tuesday: Cloudy. High plus 12.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Tuesday night: Cloudy. High plus 2.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Wednesday: Cloudy. High plus 10.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Tuesday night: Cloudy. High plus 2.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}}
]

conditions = f.collect { |i| i['title'] if i['title'].include?('night') }
==> [nil, "Tuesday night: Cloudy. High plus 2.", nil, "Tuesday night: Cloudy. High plus 2."]

CodePudding user response:

This is expected behaviour - map transforms an array one to one and any statement in form:

do_sth if some_condition

returns nil when some condition is falsey and nil is just an object as anything else.

Standard way of dealing with this would be either to use select before call to map or to call compact after map.

Since ruby 2.7 we have another method combining the two: filter_map which ignores returned falsy values:

conditions = f.filter_map { |i| i['title'] if i['title'].include?('night') }
==> ["Tuesday night: Cloudy. High plus 2.", "Tuesday night: Cloudy. High plus 2."]

CodePudding user response:

Because in every Iteration, collect/map returns something (also nil in this case)...

You could use select to filter your array. Something like this:

f.select { |i| i['title'].include?('night') }.collect {|i| i['title']}

CodePudding user response:

#map transforms one collection to another collection of the same size. If the block returns nil values, then those nils will end up in the new collection.

A statement like i['title'] if i['title'].include?('night') will return nil in the case that the title does not contain night.

You can either use #filter_map, #select #map, #inject or #each_with_object

I ran a quick micro-benchmark and suprisingly iterating the collection twice (using #select AND #map) is not slower.

items = [
{"title"=>"Tuesday: Cloudy. High plus 12.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Tuesday night: Cloudy. High plus 2.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Wednesday: Cloudy. High plus 10.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}},

{"title"=>"Tuesday night: Cloudy. High plus 2.",
"link"=>{"type"=>"text/html",
  "href"=>"https://www.weather.gc.ca/city/pages/mb-42_metric_e.html"}}
]
require "benchmark/ips"
Benchmark.ips do |x|
  x.compare!
  x.report("filter_map") do
    items.filter_map { |item| item["title"] if item["title"].include?("night")}
  end
  x.report("select_map") do
    items.select { |item| item["title"].include?("night") }.map { |item| item["title"] }
  end
  x.report("each_with_object") do
    items.each_with_object([]) { |item, accu| accu << item["title"] if item["title"].include?("night"); nil }
  end
  x.report("inject") do
    items.inject([]) { |accu, item| accu << item["title"] if item["title"].include?("night"); accu }
  end
end
  • Related