Ruby iterate over an array of hashes - ruby

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 either fetch_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

Related

How to loop through a nested array and retrieve values

I have the following array:
my_tst = [
[
{
"name": "shield",
"version": "8.6.3"
},
{
"name": "bosh-dns",
"version": "1.17.0"
},
{
"name": "nessus_agent",
"version": "1.0.24"
},
{
"name": "node-exporter",
"version": "4.2.0"
},
{
"name": "syslog",
"version": "11.6.1"
}
],
[
{
"name": "shield",
"version": "8.6.3"
},
{
"name": "bosh-dns",
"version": "1.16.0"
},
{
"name": "nessus_agent",
"version": "1.0.24"
},
{
"name": "node-exporter",
"version": "4.2.0"
},
{
"name": "syslog",
"version": "11.6.1"
}
]
]
I am trying to loop through the array and output only the values of name.
I used this loop:
my_tst["name"].each do |run|
p run
end
The loop is returning an Error:
TypeError: no implicit conversion of String into Integer
How do I output all values in the nested array?
You're trying to use [] in an array, which is meant to be used passing a numeric parameter in order to access its elements by their index. You're passing a string, which is the way you get values from hashes, and there's the problem.
You have an array of arrays containing hashes (with an interesting indentation), so in that case you need to first iterate the "main" array, to be able to get the hashes over each array.
This is one way you can achieve that:
my_tst.each_with_object([]) do |e, arr|
e.each { |f| arr << f[:name] }
end
# ["shield", "bosh-dns", "nessus_agent", "node-exporter", "syslog", "shield", "bosh-dns", "nessus_agent", "node-exporter", "syslog"]
Or:
data.flat_map do |e|
e.map { |f| f[:name] }
end
Anyway, there's going to be a nested iteration.

Convert a standard JSON array into a 2d array object

In ruby, how would I parse this JSON into a 2d location array object?
I want to convert it to a simple object like this:
[["Seattle"]["Washington"],["Seaton"]["Illinois"]]
I tried a few things and having trouble with it and there are not very many examples of this that I could find via Google search.
{
"data": [
{
"city": "Seattle",
"state": "Washington",
"zip": "98104",
"country": "US",
"empty": false,
"county": null
},
{
"city": "Seaton",
"state": "Illinois",
"zip": "61476",
"country": "US",
"empty": false,
"county": null
}
]
}
Here is what I tried so far (which doesn't get me quite there):
require 'rubygems'
require 'json'
...
parsed = JSON.parse(string)
parsed["data"].each do |location|
unless location["city"].nil?
location.each do |location_item|
puts location_item.inspect
end
end
end
I would do something like this:
require 'json'
JSON.parse(string)['data'].map { |hash| [hash['city'], hash['state']] }
#=> [["Seattle", "Washington"],["Seaton","Illinois"]]
require 'json'
Depending on your requirements,
JSON.parse(str)["data"].flat_map { |h| h.values_at("city", "state") }
#=> ["Seattle", "Washington", "Seaton", "Illinois"]
or
JSON.parse(str)["data"].map { |h| h.values_at("city", "state") }
#=> [["Seattle", "Washington"], ["Seaton", "Illinois"]]

from Neo4j to GraphJSON with Ruby

I'm trying to get visualizations using d3.js or alchemy.js--but alchemy, in particular, requires the datasource to be in GraphJSON.
I've been playing around with the tutorials and examples of Max De Marzi (using neography), Michael Hunger (cy2neo, js), Neo4j, and Neo4j.rb -- but I cannot seem to get all the way there. Mostly because I don't know what I'm doing--but this is how I'm trying to learn.
What I'm trying to achieve would be along the lines of:
https://bl.ocks.org/mbostock/3750558
or the default visualization here: http://graphalchemist.github.io/Alchemy/#/docs
And you can see what GraphJSON formatting should look like by finding it on this page also: http://graphalchemist.github.io/Alchemy/#/docs
If I run the following...
get '/followers' do
Neo4j::Session.open(:server_db, "http://localhost:7474")
query = Neo4j::Session.query('MATCH (a--(b)--(c) RETURN a,b,c LIMIT 30')
puts "--------------"
puts query_to_graph_json(query)
query_to_graph_json(query)
end
# This is supposed to grab nodes and edges, but it never gets edges.
# It's originally from a conversation at the neo4j.rb site
def query_to_graph_json(query)
nodes = {}
edges = {}
add_datum = Proc.new do |datum|
case datum
when Neo4j::ActiveNode, Neo4j::Server::CypherNode
nodes[datum.neo_id] = {
id: datum.neo_id,
properties: datum.props #was attributes, but kept saying that wasn't a method
}
when Neo4j::ActiveRel, Neo4j::Server::CypherRelationship
edges[[datum.start_node.neo_id, datum.end_node.neo_id]] = {
source: datum.start_node.neo_id,
target: datum.end_node.neo_id,
type: datum.rel_type,
properties: datum.props
}
else
raise "Invalid value found: #{datum.inspect}"
end
end
query.each do |row|
row.to_a.each do |datum|
if datum.is_a?(Array)
datum.each {|d| add_datum.call(d) }
else
add_datum.call(datum)
end
end
end
{
nodes: nodes.values,
edges: edges.values
}.to_json
end
I'll get...
{
"nodes": [
{
"id": 597,
"properties": {
"name": "John",
"type": "Person"
}
},
{
"id": 127,
"properties": {
"name": "Chris",
"type": "Person"
}
},
{
"id": 129,
"properties": {
"name": "Suzie",
"type": "Person"
}
},
],
"edges": [
]
}
The problem being that I need the edges.
If I run...
get '/followers' do
content_type :json
neo = Neography::Rest.new("http://localhost:7474")
cypher = "MATCH (a)--(b)--(c) RETURN ID(a),a.name,ID(b),b.name,ID(c),c.name LIMIT 30"
puts neo.execute_query(cypher).to_json
end
I'll get a table of paths. But it's not formatted in the way I need--and I have no idea how it might get from this format to the GraphJSON format.
{
"columns": [
"ID(a)",
"a.name",
"ID(b)",
"b.name",
"ID(c)",
"c.name"
],
"data": [
[
597,
"John",
127,
"Chris",
129,
"Suzie"
],
[
597,
"John",
6,
"Adam",
595,
"Pee-Wee"
]
]
}
I think that one problem that you're having is that, instead of matching two nodes and one relationship, you're matching three nodes and two relationships. Here's your MATCH:
MATCH (a)--(b)--(c)
It should be like:
MATCH (a)-[b]-(c)
In a MATCH clause the [] can be excluded and you can just do a raw -- (or --> or <--) which represents the relationship.
You probably want to be querying for one specific direction though. If you query bidirectionally you'll get the same relationship twice with the start and end nodes switched.
Using neo4j-core (which I biased towards as one of the maintainers ;)
nodes = []
rels = []
session.query('(source)-[rel]->(target)').pluck(:source, :rel, :target).each do |source, rel, target|
nodes << source
nodes << target
rels << rel
end
{
nodes: nodes,
edges: rels
}.to_json
Also note that if you don't specify any labels your query might be slow, depending on the number of nodes). Depends on what you need ;)
This Cypher query should return the edges array as per the example format:
MATCH (a)-[r]-(b)
WITH collect(
{
source: id(a),
target: id(b),
caption: type(r)
}
) AS edges
RETURN edges
Running this against some sample data, the results look like this:
[
{
"source": 9456,
"target": 9454,
"caption": "LIKES"
},
{
"source": 9456,
"target": 9454,
"caption": "LIKES"
},
{
"source": 9456,
"target": 9455,
"caption": "LIKES"
},
{
"source": 9454,
"target": 9456,
"caption": "LIKES"
}
]

Ruby: Delete a reoccurring hash in a large nested data structure

I am trying to move data between services and need to remove a reoccurring hash from a large record that contains both hashes and arrays.
The hash to remove from every section of the record is
{
"description": "simple identifier",
"name": "id",
"type": "id"
},
Heres example data :
{"stuff": { "defs": [
{
"description": "simple identifiery",
"name": "id",
"type": "id"
},
{
"name": "aDate",
"type": "date"
},
{
"defs": [
{
"description": "simple identifier",
"name": "id",
"type": "id"
},
{
"case-sensitive": true,
"length": null,
"name": "Id",
"type": "string"
},
{
"name": "anotherDate",
"type": "dateTime"
}
],
},
{
"defs": [
{
"description": "simple identifier",
"name": "id",
"type": "id"
},
...lots more....
I created a couple recursive function to remove the element(s) but I'm left with an empty hash '{}'. I also tried to remove the parent but found that I removed the hashes parent and not the hash itself.
I'm pretty sure I could create a new hash and populate it with the data I want but there must be a way to do this.
I am not working in rails and would like to avoid using rails gems.
I figured this out by looking at the data structure closer. The elements that need to be removed are always in an array so before recursing check if the hash key/value exists and delete if so. I'm sure this could be coded better so let me know what you think.
def recursive_delete!(node, key, value)
if node.is_a?(Array)
node.delete_if { |elm| elm[key] == value }
node.each do |elm|
recursive_delete!(elm, key, value)
end
elsif node.is_a?(Hash)
node.each_value do |v|
recursive_delete!(v, key, value)
end
end
end
If you are looking for the way to delete the same hash as you have inside complex Array/Hash data structure, it's easy:
def remove_hash_from(source, hsh)
return unless source.is_a?(Hash) || source.is_a?(Array)
source.each do |*args|
if args.last == hsh
source.delete(args.first)
elsif args.last.is_a?(Hash) || args.last.is_a?(Array)
remove_hash_from(args.last, hsh)
end
end
source
end
data = [
{h: 'v',
j: [{h: 'v'},
{a: 'c'},
8,
'asdf']
},
asdf: {h: 'v', j: 'c'}
]
remove_hash_from(data, {h: 'v'})
# => [{:h=>"v", :j=>[{:a=>"c"}, 8, "asdf"]}, {:asdf=>{:h=>"v", :j=>"c"}}]
Possibly, you will need to adjust method above for your needs. But common idea is clear, I hope.

Merging records in JSON with Ruby

I have two json files that I'm trying to merge. The JSONs have different formatting (see below). I'd like to merge records, so [0] from file one and [0] from file two would become one record [0] in the new merged file.
The first JSON (file_a.json), appears like so:
{
"query": {
"count": 4,
"created": "2012-11-21T23:07:00Z",
"lang": "en-US",
"results": {
"quote": [
{
"Name": "Bill",
"Age": "46",
"Number": "3.55"
},
{
"Name": "Jane",
"Age": "33",
"Number": nil
},
{
"Name": "Jack",
"Age": "55",
"Number": nil
},
{
"Name": "Xavier",
"Age": nil,
"Number": "153353535"
}
]
}
}
}
The second JSON (file_b.json) appears like so:
[
{
"Number2": 25253,
"Number3": 435574,
"NAME": "Bill"
},
{
"Number2": 345353,
"Number3": 5566,
"NAME": "Jane"
},
{
"Number2": 56756,
"Number3": 232435,
"NAME": "Jack"
},
{
"Number2": 7457,
"Number3": 45425,
"NAME": "Xavier"
}
]
None of the keys are the same in both JSONs (well, actually "Name" is a key in both, but in the first the key is "Name" and in the second its "NAME" - just so I can check that the merge works correctly - so I want "Name" and "NAME" in the final JSON), the first record in the first file matches with the first record in the second file, and so on.
So far, I tried merging like this:
merged = %w[a b].inject([]) { |m,f| m << JSON.parse(File.read("file_#{f}.json")) }.flatten
But this of course merged them, but not how I wanted them merged (they are merged sucessively, and because of the different formatting, it gets quite ugly).
I also tried merging like this:
a = JSON.parse(File.read("file_a.json"))
b = JSON.parse(File.read("file_b.json"))
merged = a.zip(b)
Came closer but still not correct and the formatting was still horrendous.
In the end, what I want is this (formatting of second JSON - headers from first JSON can be junked):
[
{
"Name": "Bill",
"Age": 46,
"Number": 3.55,
"Number2": 25253,
"Number3": 435574,
"NAME": "Bill"
},
{
"Name": "Jane",
"Age": 33,
"Number": nil,
"Number2": 345353,
"Number3": 5566,
"NAME": "Jane"
},
{
"Name": "Jack",
"Age": 55,
"Number": nil,
"Number2": 56756,
"Number3": 232435,
"NAME": "Jack"
},
{
"Name": "Xavier",
"Age": nil,
"Number": 153353535,
"Number2": 7457,
"Number3": 45425,
"NAME": "Xavier"
}
]
Any help is appreciated. Thanks a lot.
Hеllo, seems format changed from last time :)
UPDATE: more readable version that also convert corresponding values to integers/floats:
require 'json'
require 'ap'
a = JSON.parse(File.read('./a.json'))['query']['results']['quote'] rescue []
b = JSON.parse(File.read('./b.json'))
final = []
a.each_with_index do |ah,i|
unless bh = b[i]
bh = {}
puts "seems b has no #{i} key, merging skipped"
end
final << ah.merge(bh).inject({}) do |f, (k,v)|
if v.is_a?(String)
if v =~ /\A\d+\.\d+\Z/
v = v.to_f
elsif v =~ /\A\d+\Z/
v = v.to_i
end
end
f.update k => v
end
end
ap final
will display:
[
[0] {
"Name" => "Bill",
"Age" => 46,
"Number" => 3.55,
"Number2" => 25253,
"Number3" => 435574,
"NAME" => "Bill"
},
[1] {
"Name" => "Jane",
"Age" => 33,
"Number" => nil,
"Number2" => 345353,
"Number3" => 5566,
"NAME" => "Jane"
},
[2] {
"Name" => "Jack",
"Age" => 55,
"Number" => nil,
"Number2" => 56756,
"Number3" => 232435,
"NAME" => "Jack"
},
[3] {
"Name" => "Xavier",
"Age" => nil,
"Number" => 153353535,
"Number2" => 7457,
"Number3" => 45425,
"NAME" => "Xavier"
}
]
Here is a working demo
Btw, your json is a bit wrong in both files.
See the fixed versions here and here

Resources