Collecting hash entries with identical values - arrays

I have an array that looks like below
unorganized_array = [
{:identifier => '1', :groupinfo => [{:color => 'blue', :texture => 'soft'}]},
{:identifier => '1', :groupinfo => [{:color => 'green', :texture => 'hard'}]},
{:identifier => '2', :groupinfo => [{:color => 'red', :texture => 'spiky'}]}]
[{:identifier => '1', :groupinfo => [
{:color => 'blue', :texture => 'soft'}]},
{:identifier => '1', :groupinfo => [
{:color => 'green', :texture => 'hard'}]},
{{:identifier => '2', :groupinfo =>
[{:color => 'red', :texture => 'spiky'}]}
I want to collect all the entries with the same :identifier into that identifier's :groupinfo. This has an extra :identifier => '2' group compared to the previous example:
organized_array = [{:identifier => '1', :groupinfo => [
{:color => 'blue', :texture => 'soft'},
{:color => 'green', :texture => 'hard'}]},
{:identifier => '2', :groupinfo =>
[{:color => 'red', :texture => 'spiky'},
{:color => 'gray', :texture => 'squishy}]}]
I feel like Hash#merge and Hash#inject would be useful here but I'm unsure as to how to implement them.
I'm making unorganized_array from an array that looks like
original_array = [['blue', 'soft', '1', 'irrelevant'], ['green','hard','1','irrelevant1'],
['red','spiky','2','irrelevant2']]
perhaps there is a better method than going from original_array -> unorganized_array -> organized_array?
so far I've been trying to use #map and #each with for loops to group them together, i.e.
unorganized_array = original_array.map! do |first, second, third, fourth|
{:identifier => third, :groupinfo => [{:color => first, :texture => second}]}
end

unorganized_array.map(&:dup)
.group_by { |e| e.delete(:identifier) }
.map { |k, v| [k, v.flat_map { |h| h[:groupinfo] } ] }
.map { |k, v| { identifier: k, groupinfo: v } }
The above gives on the input shown:
#⇒ [ { :groupinfo => [
# { :color => "blue", :texture => "soft" },
# { :color => "green", :texture => "hard" } ],
# :identifier => "1" },
# { :groupinfo => [
# { :color => "red", :texture => "spiky" } ],
# :identifier => "2" } ]

At the moment, you're generating an Array of 2-key Hashes, the second of which contains an Array of attributes. Why not instead use the "identifier" as your key for a Hash? After all, that's precisely what a key is.
This seems cleaner:
{"1"=>[{:color=>"blue", :texture=>"soft"},
{:color=>"green", :texture=>"hard"}],
"2"=>[{:color=>"red", :texture=>"spiky"}]}
And here's one way of generating this:
my_hash = original_array.reduce({}) do |r,s|
r.merge( s[2] => (r[s[2]] || []) + [{color: s[0], texture: s[1]}] )
end
To #the Tin Man's point, understanding Ruby's Enumerable class is a more worthy objective than discovery of any particular solution we can provide.
Working with your Hash
Here's one way of iterating through all your items.
my_hash.each do |key,arr|
puts "Identifier \##{key} has #{arr.size} items"
arr.each_with_index do |item,index|
puts "Item \##{index+1} is #{item[:color]} and #{item[:texture]}"
end
end
Outputs:
Identifier #1 has 2 items
Item #1 is blue and soft
Item #2 is green and hard
Identifier #2 has 1 items
Item #1 is red and spiky

arr =
[{:identifier => '1', :groupinfo => [{:color => 'blue, :texture => 'soft}]},
{:identifier => '1', :groupinfo => [{:color => 'green', :texture => 'hard'}]},
{:identifier => '2', :groupinfo => [{:color => 'red', :texture => 'spiky'}]}]
There are a couple of ways you can do this by building hashes.
Use the form of Hash::new that employs a default that determines h[k] when the hash h does not have a key k.
hash_with_default = Hash.new { |h,k| { identifier: k, groupdata: [] } }
arr.each_with_object(hash_with_default) { |g,h| h[g[:identifier]] =
{ identifier: g[:identifier],
groupdata: h[g[:identifier]][:groupdata] << g[:groupinfo].first } }.values
#=> [{:identifier=>"1", :groupinfo=>[{:color=>"blue", :texture=>"soft"},
# {:color=>"green", :texture=>"hard"}]},
# {:identifier=>"2", :groupinfo=>[{:color=>"red", :texture=>"spiky"}]}]
Use the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged
arr.each_with_object({}) { |g,h| h.update(g[:identifier] => g) { |k,o,n|
{ identifier: o[:identifier], groupinfo: h[k][:groupinfo] + g[:groupinfo] } } }.values
#=> [{:identifier=>"1", :groupinfo=>[{:color=>"blue", :texture=>"soft"},
# {:color=>"green", :texture=>"hard"}]},
# {:identifier=>"2", :groupinfo=>[{:color=>"red", :texture=>"spiky"}]}]
Please consult the doc for the meanings of the three block variables, k, o and n.

I think this is not the best way but I suggested to do:
1- group your array by identifier
unorganized_array.group_by{|x| x[:identifier]}
#=> {"1"=>[{:identifier=>"1", :groupinfo=>[{:color=>"blue", :texture=>"soft"}]},
{:identifier=>"1", :groupinfo=>[{:color=>"green", :texture=>"hard"}]}],
"2"=>[{:identifier=>"2", :groupinfo=>[{:color=>"red", :texture=>"spiky"}]}]}
2 - collect your grouped_array elements in the expected hash and collect the group info values. Now every element into the array contains related items.
group_array.each.collect {|k,g| { :identifier => k, :group_info => g.collect{ |x| x[:groupinfo].last } } }
3- result is the organized array
[{:identifier=>"1", :group_info=>[
{:color=>"blue", :texture=>"soft"},
{:color=>"green", :texture=>"hard"}
]},
{:identifier=>"2", :group_info=>[{:color=>"red", :texture=>"spiky"}]}]

Related

How to group an array of hash by its value which has key-value pair in Ruby (Rails)?

So I have following array of hash:
my_array = [
{
"date" => "2022-12-01",
"pic" => "Jason",
"guard" => "Steven",
"front_desk" => "Emily"
},
{
"date" => "2022-12-02",
"pic" => "Gilbert",
"guard" => "Johnny",
"front_desk" => "Bella"
},
{
"date" => "2022-12-03",
"pic" => "Steven",
"guard" => "Gilbert",
"front_desk" => "Esmeralda"
}
]
My question is how do I change the structure of my array (grouping) by date in Ruby (Rails 7). Or in other word, I want to change my array into something like this:
my_array = [
{
"2022-12-01" => {
"pic" => "Jason",
"guard" => "Steven",
"front_desk" => "Emily"
{
},
{
"2022-12-02" => {
"pic" => "Gilbert",
"guard" => "Johnny",
"front_desk" => "Bella"
}
},
{
"2022-12-03" => {
"pic" => "Steven",
"guard" => "Gilbert",
"front_desk" => "Esmeralda"
}
}
]
Anyway, thanks in advance for the answer
I have tried using group_by method to group by its date, but it doesn't give the output I wanted
I've tried this method:
my_array.group_by { |element| element["date"] }.values
If you simply want a 1:1 mapping of your input objects to an output object of a new shape, then you just need to use Array#map:
my_array.map {|entry| {entry["date"] => entry.except("date")} }
(Hash#except comes from ActiveSupport, and is not standard Ruby, but since you're in Rails it should work just fine).
Both solutions assume the key "date" will be unique. If we cannot make this assumption safely, then each date should be mapped to an array of hashes.
my_array.each_with_object({}) do |x, hsh|
date = x["date"]
hsh[date] ||= []
hsh[date] << x.except("date")
end
Result:
{
"2022-12-01" => [
{"pic"=>"Jason", "guard"=>"Steven", "front_desk"=>"Emily"}
],
"2022-12-02" => [
{"pic"=>"Gilbert", "guard"=>"Johnny", "front_desk"=>"Bella"}
],
"2022-12-03" => [
{"pic"=>"Steven", "guard"=>"Gilbert", "front_desk"=>"Esmeralda"}
]
}
Or you may like:
my_array
.sort_by { |x| x["date"] }
.group_by { |x| x["date"] }
.transform_values { |x| x.except("date") }

Check data structure and disregard if hash or array

I have a bunch of Hashes inside of an array. When checking my keys and values I get the expected output except for some special cases as they refer to more Arrays/Hashes.
Think of something like this:
#AoH = ( { 'husband' => "homer", 'wife' => "marge" },
{ 'people' => [{'Bob'=> 24, 'Lukas'=> 37}] },
{ 'vegetables' => { 'tomato' => "red", 'carrot' => "orange"} });
My function iterates through the array and displays my keys and values as in the following:
sub function(...){
print "$key => $value\n";
}
husband => homer
wife => marge
people => ARRAY(0x6b0d80)
Bob => 24
Lukas => 37
vegetables => HASH(0x2570d38)
tomato => red
carrot => orange
Now I want to access my keys and values, but when getting something like ARRAY or HASH as value, I want to disregard that hash and not print it.
Is there some kind of way to only access Values with type scalar?
So far I tried this:
if ($value eq 'ARRAY') {
}
elsif ($value eq ref {}) {
}
else {
print "$key => $value\n";
}
But, it ends up printing exactly the same as above and does not disregard the other data structures.
For an arbitrary data structure like yours, you can use Data::Traverse:
use warnings;
use strict;
use Data::Traverse qw(traverse);
my #AoH = ( { 'husband' => "homer", 'wife' => "marge" },
{ 'people' => [{'Bob'=> 24, 'Lukas'=> 37}] },
{ 'vegetables' => { 'tomato' => "red", 'carrot' => "orange"} });
traverse { print "$a => $b\n" if /HASH/ } \#AoH;
Output:
wife => marge
husband => homer
Bob => 24
Lukas => 37
carrot => orange
tomato => red
Following demo code does not utilize external modules, provided for educational purpose.
use strict;
use warnings;
use feature 'say';
my #AoH = ( { 'husband' => "homer", 'wife' => "marge" },
{ 'people' => [{'Bob'=> 24, 'Lukas'=> 37}] },
{ 'vegetables' => { 'tomato' => "red", 'carrot' => "orange"} });
drill_in( \#AoH );
sub drill_in {
my $data = shift;
if( ref $data eq 'ARRAY' ) {
drill_in($_) for #$data;
} elsif ( ref $data eq 'HASH' ) {
while( my($k, $v ) = each %{$data} ) {
(ref $v eq 'ARRAY' or ref $v eq 'HASH') ? drill_in($v) : say "$k => $v";
}
}
}
Output
husband => homer
wife => marge
Lukas => 37
Bob => 24
tomato => red
carrot => orange

How to create a new hash table if I have one big hash

I have an array of hashes:
{hashed_data = [
{:name => "frontend", :session_total => 145, :byte => 54667},
{:name => "backend_stagging", :session_total => 546, :byte => 895747},
{:name => "backend", :session_total => 5468, :byte => 8957447},
{:name => "frontend", :session_total => 54, :byte => 67387}
]
I must create the following hash. It does not have to be sorted.
hashed_data_modify = {
:frontend => {
:name => "frontend",
:summary => {:session_total => 546, :byte => 54667}
   :backend => {
:name => "backend",
:details => {:session_total => 5468, :byte => 8957447},
   :summary => { :name => "backend_stagging", :session_total => 546, :byte => 895747 }
}
that is, create a new key: frontend where hashed_data[:name] == "frontend" and create a key: backend where hashed_data[:name] == "backend". The frontend key contains only the data for the frontend data and for the backend only the backend.
I tried my sorting_method, it is bad:
hashed_data.select do |h|
if (h[:name] == "frontend") then
return hash = {
:frontend => {
:name => hashed_data[:name],
:details => [:session_total => hashed_data[:stot], :byte_in => hashed_data[:bin]]
}
} # :name => "frontend" etc.
(h[:name == "backend")
return hash = {
:backend => {:name => hashed_data[:name] #:name => "backend"}
} etc.
end
end
Please help.
I'm guessing this is what you want.
hashed_data.group_by { |h| h[:name] }.
map { |_,v| v.max_by { |g| g[:session_total] } }
#=> [{:name=>"frontend", :session_total=>145, :byte=>54667},
# {:name=>"backend_stagging", :session_total=>546, :byte=>895747},
# {:name=>"backend", :session_total=>5468, :byte=>8957447}]
Note that the first calculation is the following.
hashed_data.group_by { |h| h[:name] }
#=> {"frontend"=>[{:name=>"frontend", :session_total=>145, :byte=>54667},
# {:name=>"frontend", :session_total=>54, :byte=>67387}],
# "backend_stagging"=>[{:name=>"backend_stagging", :session_total=>546,
# :byte=>895747}],
# "backend"=>[{:name=>"backend", :session_total=>5468, :byte=>8957447}]}
Firstly, you say you want the first element of the array returned to be:
{:name=>"frontend", :session_total=>546, :byte=>54667}
I'm guessing the value of :session_total is a typo, and should be 145.
Secondly, I assume when two or more hashes have the same value of :name (here just "frontend"), you want to keep the one for which the value session_total is greatest (but that's a truly wild guess).
Thirdly, you seem to want to change the value "backend_staging" to the key :summary. If so, that's not central to the question and just a nuisance for those giving answers, so I've disregarded that requirement.

Combine hash properties from array of hashes

I'm parsing an excel spreadsheet and trying to combine data rows by id across tables and files. This is a condensed, simplified version of what I have. With a PHP/JS background, I prefer conceptualizing hashes as objects, so %aoo means array of objects instead of array of hashes...
#!/usr/bin/env perl
use v5.10.0;
use strict;
use warnings;
use Data::Dump;
use Data::Dumper;
# Array of objects
# Each object is a row from a table
my $aoo1 = [
{
"id" => 1,
"name" => "Dan",
"team" => "red"
},
{
"id" => 2,
"name" => "Arnold",
"team" => "red"
},
{
"id" => 3,
"name" => "Kristen",
"team" => "red"
}
];
my #aoo2 = (
{
"id" => 1,
"position" => "web developer",
},
{
"id" => 2,
"position" => "CEO",
},
{
"id" => 3,
"position" => "Secretary",
}
);
my #aoo3 = (
{
"id" => 1,
"tenure" => "1yr",
},
{
"id" => 2,
"tenure" => "25yr",
},
{
"id" => 3,
"tenure" => "5yr",
}
);
# object of arrays
# each property is a table name from spreadsheet
my %ooa;
%ooa = (
"People List" => $aoo1,
"Position List" => \#aoo2,
"Tenure List" => \#aoo3
);
# dd \%ooa;
while (my ($list_name, $aoo) = each %ooa)
{
# $aoo reftype is array | [ %object, %object, %object ]
# Do something to look into other objects for same id...
}
I want to be able to create a new object for each unique row in the file, so I can filter the values and then write it to a CSV file.
Ex. of the end result
%complete_row = (
'id' => 1,
'name' => 'Dan',
'team' => 'red',
'position => 'Web Dev',
'tenure' => '1yr'
);
Put the 2nd and 3rd arrays into hashes mapping ID to the hash. Then loop through the people and use the ID to get the data from position and tenure hashes.
use strict;
use warnings;
use Data::Dumper;
my $people = [
{
id => 1,
name => "Dan",
team => "red"
},
{
id => 2,
name => "Arnold",
team => "red"
},
{
id => 3,
name => "Kristen",
team => "red"
}
];
my $positions = [
{
id => 1,
position => "web developer",
},
{
id => 2,
position => "CEO",
},
{
id => 3,
position => "Secretary",
}
];
my $tenures = [
{
id => 1,
tenure => "1yr",
},
{
id => 2,
tenure => "25yr",
},
{
id => 3,
tenure => "5yr",
}
];
# hash each by ID
my %position_hash = map { $_->{id} => $_ } #$positions;
my %tenure_hash = map { $_->{id} => $_ } #$tenures;
# combine
my $complete = [];
foreach my $person (#$people) {
my $id = $person->{id};
my $complete_row = {
%$person,
position => $position_hash{$id}->{position},
tenure => $tenure_hash{$id}->{tenure},
};
push #$complete, $complete_row
}
print "complete = " . Dumper($complete);
This should work:
my %newHash;
foreach my $arrRef(map {$ooa{$_}} keys %ooa) { #reading all values of ooa hash, each value is an array ref
foreach my $hashRef(#$arrRef) { #reading each array element, each array element is a hash ref
foreach my $key(keys %{$hashRef}) { #reading all keys of each internal hash
$newHash{$hashRef->{'id'}}{$key} = $hashRef->{$key}; #building new hash of hashes with id as key and value as hash ref
}
}
}
my #newArray = map {$newHash{$_}} keys %newHash; #converting hash of hashes into array of hashes

Add string to array nested inside of hash

I have a set of nested hashes. I would like to add the string "Assembly" to the array value associated with [:dennis_ritche][:languages]
def adding_to_dennis
programmer_hash =
{
:grace_hopper => {
:known_for => "COBOL",
:languages => ["COBOL", "FORTRAN"]
},
:alan_kay => {
:known_for => "Object Orientation",
:languages => ["Smalltalk", "LISP"]
},
:dennis_ritchie => {
:known_for => "Unix",
:languages => ["C"]
}
}
programmer_hash[:dennis_ritchie][:languages] << "Assembly"
end
This is the error I get no implicit conversion of Symbol into Integer"
I think the problem you're seeing is you're manipulating the hash inside the method and as a result are inadvertently returning the wrong thing. This method returns an Array because that's the last operation performed (<< on Array return the modified Array).
To fix it define a method that does the manipulation:
def add_to_hash(hash, programmer = :dennis_ritchie, language = 'Assembly')
hash[programmer][:languages] << language
end
Make that independent of the definition:
programmer_hash =
{
:grace_hopper => {
:known_for => "COBOL",
:languages => ["COBOL", "FORTRAN"]
},
:alan_kay => {
:known_for => "Object Orientation",
:languages => ["Smalltalk", "LISP"]
},
:margaret_hamilton => {
:known_for => "Apollo Program",
:languages => ["Assembly"]
},
:dennis_ritchie => {
:known_for => "Unix",
:languages => ["C"]
}
}
Then call it to manipulate the hash:
add_to_hash(programmer_hash)
The programmer_hash structure is then updated.

Resources