3

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
Zoe Edwards
  • 12,999
  • 3
  • 24
  • 43

3 Answers3

4

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).

Sebastián Palma
  • 32,692
  • 6
  • 40
  • 59
  • Keep in mind though that `dig` was introduced in Ruby 2.4, if I remember correctly, so you can't use this approach in earlier versions. – Marek Lipka Aug 27 '19 at 09:29
  • 1
    Thanks @MarekLipka, I hope he doesn't have problems using dig, I can't think in a cleaner example without using that method. – Sebastián Palma Aug 27 '19 at 10:06
  • 1
    This is a good answer, but: "`next` in an enumerator returns the next object, and move the internal position forward. When the position reached at the end, StopIteration is raised." I think you're conflating [`next` keyword](https://ruby-doc.org/core-2.6/doc/syntax/control_expressions_rdoc.html#label-next+Statement) with the [`Enumerator#next` method](https://ruby-doc.org/core-2.6.3/Enumerator.html#method-i-next). Your code users the former, but your description concerns the latter. – Jordan Running Aug 27 '19 at 16:57
  • Oh, yes @JordanRunning, sorry for that. I've updated that part, thanks! – Sebastián Palma Aug 27 '19 at 17:12
1

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.

Marek Lipka
  • 50,622
  • 7
  • 87
  • 91
0

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

pages.select! do |page|
  page['content'] && page['content']['score'] && page['content']['score'] > 75
end
mrzasa
  • 22,895
  • 11
  • 56
  • 94
  • Thanks for this – my conditions get quite complex, so my line length would become horrible to read quite quickly. But perfectly valid answer! – Zoe Edwards Aug 27 '19 at 09:26
  • 1
    You can move it to a separate method/labdas (or set of lambdas/methods) – mrzasa Aug 27 '19 at 09:29