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 nil
s 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