Zoe Edwards
Zoe Edwards

Reputation: 13667

How to reject or select based on more than one condition in Ruby

An API I’m using returns different content depending on circumstances.

Here’s a snippet of what the API might return:

pages = [
  {
    'id' => 100,
    'content' => {
      'score' => 100
    },
  },
  {
    'id' => 101,
    'content' => {
      'total' => 50
    },
  },
  {
    'id' => 102,
  },
]

content is optional, and can contain different items.

I would like to return a list of pages where the score is more than 75.

So far this is as small as I can make it:

pages.select! do |page|
  page['content']
end
pages.select! do |page|
  page['content']['score']
end
pages.select! do |page|
  page['content']['score'] > 75
end

If I was using JavaScript, I would do this:

pages.select(page => {
    if (!page['content'] || !page['content']['score']) {
        return false
    }
    return page['content']['score'] > 75
})

Or perhaps if I was using PHP, I would array_filter in a similar way.

However, trying to do the same thing in Ruby throws this error:

LocalJumpError: unexpected return

I understand why you cannot do that. I just want to find a simple way to achieve this in Ruby.


Here’s my make-believe Ruby I want to magically work:

pages = pages.select do |page|
  return unless page['content']
  return unless page['content']['score']
  page['content']['score'] > 75
end

Upvotes: 4

Views: 1020

Answers (3)

Sebastián Palma
Sebastián Palma

Reputation: 33420

You can use dig, which can access to nested keys in a hash, it returns nil if it can't access to at least one of them, then use the safe operator to do the comparison:

p pages.select { |page| page.dig('content', 'score')&.> 75 }
# [{"id"=>100, "content"=>{"score"=>100}}]

Notice this "filter" is done in one step, so you don't need to mutate your object.

For your make-belive approach, you need to replace the returns with next:

pages = pages.select do |page|
  next unless page['content']
  next unless page['content']['score']
  page['content']['score'] > 75
end

p pages # [{"id"=>100, "content"=>{"score"=>100}}]

There next is used to skip the rest of the current iteration.


You can save one line in that example, if you use fetch and pass as the default value an empty hash:

pages.select do |page|
  next unless page.fetch('content', {})['score']

  page['content']['score'] > 75
end

Same way you can do just page.fetch('content', {}).fetch('score', 0) > 75 (but better don't).

Upvotes: 5

mrzasa
mrzasa

Reputation: 23327

You can use logical "and" (&&) to concatenate conditions

pages.select! do |page|
  page['content'] && page['content']['score'] && page['content']['score'] > 75
end

Upvotes: 1

Marek Lipka
Marek Lipka

Reputation: 51151

You can actually achieve this using lambda, where return means what you expect it to mean:

l = lambda do |page|
  return false if !page['content'] || !page['content']['score']

  page['content']['score'] > 75
end

pages.select(&l)
# => [{"id"=>100, "content"=>{"score"=>100}}]

but I guess it's not very practical.

Upvotes: 1

Related Questions