0

I have been searching for a solution to this issue for a couple of days now, and I'm hoping someone can help out. Given this data structure:

'foo' => {
  'bar' => [
    {
      'baz' => {'faz' => '1.2.3'},
      'name' => 'name1'
    },
    {
      'baz' => {'faz' => '4.5.6'},
      'name' => 'name2'
    },
    {
      'baz' => {'faz' => '7.8.9'},
      'name' => 'name3'
    }
  ]
}

I need to find the value of 'faz' that begins with a '4.', without using each. I have to use the '4.' value as a key for a hash I will create while looping over 'bar' (which obviously I can't do if I don't yet know the value of '4.'), and I don't want to loop twice.

Ideally, there would be an elegant one-line solution to return the value '4.5.6' to me.

I found this article, but it doesn't address the full complexity of this data structure, and the only answer given for it is too verbose; the looping-twice solution is more readable. I'm using Ruby 2.3 on Rails 4 and don't have the ability to upgrade. Are there any Ruby gurus out there who can guide me?

Mik
  • 443
  • 4
  • 9
  • You need an open brace before `foo`. I’m guessing you also want `foo` and `bar` to be literals (e.g., `’foo’` and `’bar’`). While you’re at it, assign the hash to a variable (e.g., `h = { ‘foo’=>...}`), so that readers can refer to that variable in answers and comments without having to define it. – Cary Swoveland Nov 12 '19 at 17:11
  • 1
    `h['foo']['bar'].find { |hh| hh['baz']['faz'].start_with? '4.' }['baz']['faz']`? Naah! – iGian Nov 12 '19 at 19:50
  • Why the Rails tag? – Cary Swoveland Nov 12 '19 at 21:20

1 Answers1

1

You can use select to filter results.

data = {'foo' => {'bar' => [{'baz' => {'faz' => '1.2.3'}, 'name' => 'name1'}, {'baz' => {'faz' => '4.5.6'}, 'name' => 'name2'}, {'baz' => {'faz' => '7.8.9'}, 'name' => 'name3'}]}}

data.dig('foo', 'bar').select { |obj| obj.dig('baz', 'faz').slice(0) == '4' }
#=> [{"baz"=>{"faz"=>"4.5.6"}, "name"=>"name2"}]

# or if you prefer the square bracket style
data['foo']['bar'].select { |obj| obj['baz']['faz'][0] == '4' }

The answer assumes that every element inside the bar array has the nested attributes baz -> faz.

If you only expect one result you can use find instead.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • Why have you used `dig`, rather than `data['foo']['bar']` and `obj['baz']['faz']`? The OP has not suggested, in words or the example, that the nested keys may not be present. If a key that is supposed to be there is missing `dig` would merrily return `nil`, masking the omission. In that case we would want an exception to be raised. – Cary Swoveland Nov 12 '19 at 17:47
  • @CarySwoveland I prefer `dig` with multiple arguments over chaining `[]` calls, only for style. Presence is still required since `select` or `slice` will only work when the correct object is returned. But you could replace `data.dig('foo', 'bar')` with `data['foo']['bar']` if you prefer the look of that. Updated the answer with the alternative. – 3limin4t0r Nov 12 '19 at 20:23
  • Suppose `'baz' => {'faz' => '4.5.6'}` had been mistakenly written `'bazz' => {'faz' => '4.5.6'}` then by using `dig` (when looking for `4`) an empty array would have been returned, which may have been a possible outcome. Had `'baz'` and `'faz'` been used explicitly, an exception would be raised, which would be helpful. There are of course other examples of errors that are missed and the use of `dig` causes a non-empty, but erroneous, array to be returned. – Cary Swoveland Nov 12 '19 at 21:11