简体   繁体   中英

Ruby iterate over an array of hashes

I have the below array of hashes. I want to add a new key,value pair to "hashes" which are in "all" array. Is there any better way of looping through, than what I am doing currently?

stack = {
  "all": [
    "mango",
    "apple",
    "banana",
    "grapes"
  ],
  "mango": {
    "TYPE": "test",
    "MAX_SIZE": 50,
    "REGION": "us-east-1"
  },
  "apple": {
    "TYPE": "dev",
    "MAX_SIZE": 55,
    "REGION": "us-east-1"
  },
  "banana": {
    "TYPE": "test",
    "MAX_SIZE": 60,
    "REGION": "us-east-1"
  },
  "grapes": {
    "TYPE": "dev",
    "MAX_SIZE": 80,
    "REGION": "us-east-1"
  },
  "types": [
    "dev",
    "test"
  ]
}

My code:

  stack['all'].each do |fruit|
   stack[fruit].each do |fruit_name|
     stack[fruit_name]['COUNT'] = stack[fruit_name]['MAX_SIZE'] * 2
   end
  end

Expected output:

stack = {
  "all": [
    "mango",
    "apple",
    "banana",
    "grapes"
  ],
  "mango": {
    "TYPE": "test",
    "MAX_SIZE": 50,
    "REGION": "us-east-1",
    "COUNT" : 100
  },
  "apple": {
    "TYPE": "dev",
    "MAX_SIZE": 55,
    "REGION": "us-east-1",
    "COUNT" : 110
  },
  "banana": {
    "TYPE": "test",
    "MAX_SIZE": 60,
    "REGION": "us-east-1",
    "COUNT" : 120
  },
  "grapes": {
    "TYPE": "dev",
    "MAX_SIZE": 80,
    "REGION": "us-east-1",
    "COUNT" : 160
  },
  "types": [
    "dev",
    "test"
  ]
}

There is no need for the second loop. The following does what you want:

keys = stack[:all].map(&:to_sym)
keys.each do |key|
  stack[key][:COUNT] = stack[key][:MAX_SIZE] * 2
end

In the above code-block stack[:all] will return an array of keys as strings, .map(&:to_sym) will convert each string in the resulting array into a symbol.


Another way to achieve the same result would be to use eitherfetch_values or values_at to retrieve an array of values belonging to the provided keys. The difference being that fetch_values raises an exception if a key is missing while values_at returns nil for that key.

fruits = stack.fetch_values(*stack[:all].map(&:to_sym))
fruits.each do |fruit|
  fruit[:COUNT] = fruit[:MAX_SIZE] * 2
end

If you are wondering why there is a * before stack[:all].map(&:to_sym) , this is to convert the array into individual arguments. In this context * is called the spat operator .

You might write the code as follows.

stack[:all].each do |k|
  h = stack[k.to_sym]
  h[:COUNT] = 2*h[:MAX_SIZE] unless h.nil?
end

When, for example, `k = "mango",

h #=> h={:TYPE=>"test", :MAX_SIZE=>50, :REGION=>"us-east-1", :COUNT=>100}

I've defined the local variable h for three reasons:

  • it simplifies the code by avoiding multiple references to stack[k.to_sym]
  • when debugging it may may be helpful to be able to examine h
  • it makes the code more readable

Note that h merely holds an existing hash; it does not create a copy of that hash, so it has a neglibile effect on memory requirements.

The technique of defining local variables to hold objects that are parts of other objects is especially useful for more complex objects. Suppose, for example, we had the hash

 hash = {
   cat: { sound: "purr", lives: 9 },
   dog: { sound: "woof", toys: ["ball", "rope"] }
 }

Now suppose we wish to add a dog toy

new_toy = "frisbee"

if it is not already present in the array

hash[:dog][:toys]

We could write

hash[:dog][:toys] << new_toy unless hash[:dog][:toys].include?(new_toy)
  #=> ["ball", "rope", "frisbee"] 
hash
  #=> {:cat=>{:sound=>"purr", :lives=>9},
  #    :dog=>{:sound=>"woof", :toys=>["ball", "rope", "frisbee"]}} 

Alternatively, we could write

dog_hash = hash[:dog]
  #=> {:sound=>"woof", :toys=>["ball", "rope"]} 
dog_toys_arr = dog_hash[:toys]
  #=> ["ball", "rope"] 
dog_toys_arr << new_toy unless dog_toys_arr.include?(new_toy)  
  #=> ["ball", "rope", "frisbee"] 
hash
  #=> {:cat=>{:sound=>"purr", :lives=>9},
  #    :dog=>{:sound=>"woof", :toys=>["ball", "rope", "frisbee"]}} 
 

Not only does the latter snippet display intermediate results, it probably is a wash with the first snippet in terms of execution speed and storage requirements and arguably is more readable. It also cuts down on careless mistakes such as

hash[:dog][:toys] << new_toy unless hash[:dog][:toy].include?(new_toy)

If one element of stack[:all] were, for example, "pineapple" , stack[:pineapple] #=> nil since stack has no key :pineapple . If, however, stack contained the key-value pair

nil=>{ sound: "woof", toys: ["ball", "rope"] }

that would become problem. Far-fetched? Maybe, but it is perhaps good practice--in part for readability--to avoid the assumption that h[k] #=> nil means h has no key k ; instead, use if h.key?(k) . For example:

stack[:all].each do |k|
  key = k.to_sym
  if stack.key?(key)
    h = stack[key]
    h[:COUNT] = 2*h[:MAX_SIZE]
  end
end

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM