Fogmeister
Fogmeister

Reputation: 77631

Ruby iterating array of dictionaries

I have a JSON data structure like this...

{
  "items": [
      {
        "person": { // person hash }
      },
      {
        "dog": { // dog hash }
      },
      {
        "fruit": { // fruit hash }
      },
      {
        “person”: { // person hash }
      }
    ]
  }
}

Each item in the array contains only one key:value pair. The key is the bot that tells me what type of item the value is.

What I'd like to do is iterate the array and run a different function for each type of item.

So I have something like this...

items = data.dig('items')

items.map do |item|
  if person = item.dig('person')
    transform_person(person)
  elsif dog = item.dig('dog')
    transform_dog(dog)
  elsif fruit = item.dig('fruit')
    transform_fruit(fruit)
  end
end

But I feel like there should be a more elegant way to do this?

Apologies. I appear to have left some ambiguity in my question.

The initial array may contain multiple items with the same key. What I am trying to do is map to an array of items that are transformed into what is required by the front end. The input contains a strange structure and info that is not needed by the front end.

So the output array order must match the input array order.

Sorry for the confusion.

Upvotes: 0

Views: 1899

Answers (4)

Cary Swoveland
Cary Swoveland

Reputation: 110675

Let f be a given method that takes as an argument a hash. Without loss of generality, suppose it is as follows. This corresponds to the OP's transform_person, transform_dog and transform_fruit methods combined.

def f(h)
  case h.keys.first
  when :person then "somebody"
  when :dog    then "doggie" 
  when :fruit  then "juicy"
  end
end

Suppose we are also given (no need for dig here)

items = data[:items]
  #=> [{:person=>{:name=>"Melba"}},
  #    {:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
  #    {:fruit=>{:good=>"raspberries"}}]

and

key_order = [:bird, :marsupial, :dog, :person]

We wish to find the first element k of key_order for which items contains a hash h for which h.key?(k) #=> true. If such a hash h is found we are to then execute f(h).

First compute a hash key_map.

key_map = items.each_with_object({}) { |g,h| h[g.keys.first] = g }
  #=> {:person=>{:person=>{:name=>"Melba"}},
  #    :dog=>{:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
  #    :fruit=>{:fruit=>{:good=>"raspberries"}}}

Then we simply execute

k = key_order.find { |k| key_map[k] }
  #=> :dog
k ? f(key_map[k]) : nil
  #=> "doggie"

Upvotes: 1

tadman
tadman

Reputation: 211560

First you'll want to define the key preference in a constant:

PECKING_ORDER = %w[ person dog fruit ]

Then you can use that to find it:

def detect(item)
  PECKING_ORDER.lazy.map do |key|
    [ key, item.dig(key) ]
  end.find do |key, v|
    v
  end
end

Where that can dig up the first item that's found. lazy is used here so it doesn't dig them all up needlessly, just does them one at a time until there's a hit.

This gives you a key/value pair which you can use with dynamic dispatch:

items.each do |item|
  key, value = detect(item)

  if (key)
    send(:"transform_#{key}", value)
  end
end

Upvotes: 2

Anthony
Anthony

Reputation: 15957

if you know the mapping, you could make a pseudo factory hash:

methods_mapped = {
  "person" => ->(person) { do_something_with_person(person) },
  "dog" => ->(dog) { do_something_with_dog(dog) },
  "fruit" => ->(fruit) { do_something_with_fruit(fruit) }
}

items.map do |item|
  key = item.keys.first # what if keys.size > 1 ?
  method = methods_mapped.fetch(key)
  method.call(item[key])
end

or you could it from the opposite direction:

methods_mapped.each do |key, method|
  method.call(items.dig(key))
end

Upvotes: 1

rorra
rorra

Reputation: 9693

I would kept it simple:

items.map do |item|
  do_something_with_person(item) if item.dig('person')
  do_something_with_dog(item) if item.dig('dog')
  do_something_with_fruit(item) if item.dig('fruit')
end

or

items.each do |item|
  case item
    when item.dig('person') then do_something_with_person(item)
    when item.dig('dog') then do_something_with_dog(item)
    when item.dig('fruit') then do_something_with_fruit(item)
  end
end

or

def do_something(item)
  case
    when item.dig('person') then do_something_with_person(item)
    when item.dig('dog') then do_something_with_dog(item)
    when item.dig('fruit') then do_something_with_fruit(item)
  end
end
items.map { |item| do_something(item) }

Upvotes: 0

Related Questions