How to build a Perl multidimensional array or hash? - arrays

I have a set of CSV values like this:
device name, CPU value, frequency of CPU value, CPU in percentage
For example
router1,5,10,4
router1,5,1,5
router2,5,10,4
router2,5,2,5
router3,4,5,6
router3,7,6,5
I need to form a data structure like this:
array = {
router1 => [5,10,4],[5,1,5],
router2 => [5,10,4],[5,2,5],
router3 => [4,5,6],[7,6,5]
}
I need help in forming this data structure in Perl.
I have tried visualizing how to do this but am unable to do so. I would appreciate any help on this.
The end goal for me is to convert this into a JSON object.

This should get you started. It uses the DATA file handle so that I could embed the data in the program itself. I have used to_json from the JSON module to format the hash as JSON data. The statement $_ += 0 for #values converts the contents of #values from string to to numeric, to avoid quotation marks in the resultant JSON data.
use strict;
use warnings;
use JSON;
my %data;
while (<DATA>) {
chomp;
my ($device, #values) = split /,/;
$_ += 0 for #values;
push #{ $data{$device} }, \#values;
}
print to_json(\%data, { pretty => 1, canonical => 1 });
__DATA__
router1,5,10,4
router1,5,1,5
router2,5,10,4
router2,5,2,5
router3,4,5,6
router3,7,6,5
output
{
"router1" : [
[
5,
10,
4
],
[
5,
1,
5
]
],
"router2" : [
[
5,
10,
4
],
[
5,
2,
5
]
],
"router3" : [
[
4,
5,
6
],
[
7,
6,
5
]
]
}

Here is a simple solution which prints desired JSON object.
#!/usr/bin/env perl
use strict;
use warnings;
use 5.010;
my %hash;
while (my $line = <DATA>) {
chomp $line;
my ($device, #cpu_values) = split(/,/, $line);
my $cpu_token = join(",", #cpu_values);
$hash{$device} .= '[' . $cpu_token . '], ';
}
my #devices = keys %hash;
print "array = { \n";
foreach (sort #devices) {
print "$_ => [$hash{$_}]\n";
}
print "}\n";
__DATA__
router1,5,10,4
router1,5,1,5
router2,5,10,4
router2,5,2,5
router3,4,5,6
router3,7,6,5

In Perl you need to use references in the way of anonymous arrays and hashes to make multidimensional arrays, arrays of arrays, hashes containing hashes and anywhere in between. perlreftut should cover how to accomplish what you are trying to do. Here is an example I wrote the other day that could help explain as well:
print "\nFun with multidimensional arrays\n";
my #myMultiArray = ([1,2,3],[1,2,3],[1,2,3]);
for my $a (#myMultiArray){
for my $b (#{$a}){
print "$b\n";
}
}
print "\nFun with multidimensional arrays containing hashes\nwhich contains an anonymous array\n";
my #myArrayFullOfHashes = (
{'this-key'=>'this-value','that-key'=>'that-value'},
{'this-array'=>[1,2,3], 'this-sub' => sub {return 'hi'}},
);
for my $a (#myArrayFullOfHashes){
for my $b (keys %{$a}){
if (ref $a->{$b} eq 'ARRAY'){
for my $c (#{$a->{$b}}){
print "$b.$c => $c\n";
}
} elsif ($a->{$b} =~ /^CODE/){
print "$b => ". $a->{$b}() . "\n";
} else {
print "$b => $a->{$b}\n";
}
}
}

Related

sort hash of array by value

I am trying to sort by value in a HoA wherein key => [ a, b, c]
I want to sort alphabetically and have tried and read with no success. I think its the commas, but please help! Below is a short snippet. The raw data is exactly how it appears in the data dumper print vs. the CLI. I have to use some sort of delimiter otherwise the cli output is tedious! Thank you!
use strict;
use warnings;
my ( $lsvm_a,$lsvm_b,%hashA,%hashB );
my $vscincludes = qr/(^0x\w+)\,\w+\,\w+.*/; #/
open (LSMAP_A, "-|", "/usr/ios/cli/ioscli lsmap -vadapter vhost7 -field clientid vtd backing -fmt ," ) or die $!;
while ($lsvm_a = (<LSMAP_A>)) {
chomp($lsvm_a);
next unless $lsvm_a =~ /$vscincludes/;
#{$hashA{$1}} = (split ',', $lsvm_a);
}
open (LSMAP_B, "-|", "/usr/sbin/clcmd -m xxxxxx /usr/ios/cli/ioscli lsmap -vadapter vhost29 -field clientid vtd backing -fmt ," ) or die $!;
while ($lsvm_b = (<LSMAP_B>)) {
chomp($lsvm_b);
next unless $lsvm_b =~ /$vscincludes/;
push #{$hashA{$1}}, (split ',', $lsvm_b);
}
print "\n\nA:";
for my $key ( sort { $hashA{$a} cmp $hashA{$b} } keys %hashA ) {
print "$key => '", join(", ", #{$hashA{$key}}), "'\n";
}
##
print "===\nB:";
foreach my $key ( sort { (#{$hashB{$a}}) cmp (#{$hashB{$b}}) } keys %hashB ) {
print "$key ==> #{$hashB{$key}}\n";
}
print "\n\n__DATA_DUMPER__\n\n";
use Data::Dumper; print Dumper \%hashA; print Dumper \%hashB;
Output
A:
0x00000008 => '0x00000008, atgdb003f_avg01, hdisk10, atgdb003f_ovg01, hdisk96, atgdb003f_pvg01, hdisk68, atgdb003f_rvg01, hdisk8, vtscsi0, atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924'
===
B:
0x00000008 => '0x00000008, atgdb003f_avg01, hdisk10, atgdb003f_data, atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924, atgdb003f_ovg01, hdisk96, atgdb003f_pvg01, hdisk68, atgdb003f_rvg01, hdisk8'
__DATA_DUMPER__
$VAR1 = {
'0x00000008' => [
'0x00000008',
'atgdb003f_avg01',
'hdisk10',
'atgdb003f_ovg01',
'hdisk96',
'atgdb003f_pvg01',
'hdisk68',
'atgdb003f_rvg01',
'hdisk8',
'vtscsi0',
'atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924'
]
};
$VAR1 = {
'0x00000008' => [
'0x00000008',
'atgdb003f_avg01',
'hdisk10',
'atgdb003f_data',
'atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924',
'atgdb003f_ovg01',
'hdisk96',
'atgdb003f_pvg01',
'hdisk68',
'atgdb003f_rvg01',
'hdisk8'
]
};
### CLI out ###
###0x00000008,atgdb003f_avg01,hdisk10,atgdb003f_ovg01,hdisk96,atgdb003f_pvg01,hdisk68,atgdb003f_rvg01,hdisk8,vtscsi0,atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924
###0x00000008,atgdb003f_avg01,hdisk10,atgdb003f_data,atgdb003f_data.5bcd027df10f27bf9a880ce7bc1dd924,atgdb003f_ovg01,hdisk96,atgdb003f_pvg01,hdisk68,atgdb003f_rvg01,hdisk8
Update The arrayrefs (hash values) have multiple elements after all, and need be sorted. Then
for my $key (keys %h) { #{$h{$key}} = sort #{$h{$key}} }
or, more efficiently† (and in the statement modifier form, with less noise but perhaps less clear)
$h{$_} = [ sort #{$h{$_}} ] for keys %h;
The sort by default uses lexicographical sort, as wanted.
Keys are desired to be sorted numerically, but note that while we can rewrite the arrays to make them sorted it is not so with hashes, which are inherently unordered. We can print sorted of course
foreach my $k (sort { $a <=> $b } keys %h) { ... }
This will warn if keys aren't numbers.
† By 56% – 60% in my benchmarks on three different machines, with both v5.16 and v5.30.0
Original post
I take it that you need to sort a hash which has an arrayref for a value, whereby that arrayref has a single element. Then sort on that, first, element
foreach my $key ( sort { $hashB{$a}->[0] cmp $hashB{$b}->[0] } keys %hashB ) {
print "$key ==> #{$hashB{$key}}\n";
}
See the cmp operator under Equality operators in perlop. It takes scalars, which are stringwise compared (so the attempted sorting with an array from the question is wrong since cmp would get lengths of those arrays to sort by!)
In my understanding your hash to sort is like
$VAR1 = {
'0x00000008' => [ 'atgdb003f_avg01,hdisk10,atgdb003f_ovg01,...' ],
...
}
where each value is an arrayref with exactly one element.

How can I make an array of values after duplicate keys in a hash?

I have a question regarding duplicate keys in hashes.
Say my dataset looks something like this:
>Mammals
Cats
>Fish
Clownfish
>Birds
Parrots
>Mammals
Dogs
>Reptiles
Snakes
>Reptiles
Snakes
What I would like to get out of my script is a hash that looks like this:
$VAR1 = {
'Birds' => 'Parrots',
'Mammals' => 'Dogs', 'Cats',
'Fish' => 'Clownfish',
'Reptiles' => 'Snakes'
};
I found a possible answer here (https://www.perlmonks.org/?node_id=1116320). However I am not sure how to identify the values and the duplicates with the format of my dataset.
Here's the code that I have been using:
use Data::Dumper;
open($fh, "<", $file) || die "Could not open file $file $!/n";
while (<$fh>) {
chomp;
if($_ =~ /^>(.+)/){
$group = $1;
$animals{$group} = "";
next;
}
$animals{$group} .= $_;
push #{$group (keys %animals)}, $animals{$group};
}
print Dumper(\%animals);
When I execute it the push function does not seem to work as the output from this command is the same as when the command is absent (in the duplicate "Mammal" group, it will replace the cat with the dog instead of having both as arrays within the same group).
Any suggestions as to what I am doing wrong would be highly appreciated.
Thanks !
You're very close here. We can't get exactly the output you want from Data::Dumper because hashes can only have one value per key. The easiest way to fix that is to assign a reference to an array to the key and add things to it. But since you want to eliminate the duplicates as well, it's easier to build hashes as an intermediate representation then transform them to arrays:
use Data::Dumper;
my $file = "animals.txt";
open($fh, "<", $file) || die "Could not open file $file $!/n";
while (<$fh>) {
chomp;
if(/^>(.+)/){
$group = $1;
next;
}
$animals{$group} = {} unless exists $animals{$group};
$animals{$group}->{$_} = 1;
}
# Transform the hashes to arrays
foreach my $group (keys %animals) {
# Make the hash into an array of its keys
$animals{$group} = [ sort keys %{$animals{$group}} ];
# Throw away the array if we only have one thing
$animals{$group} = $animals{$group}->[0] if #{ $animals{$group} } == 1;
}
print Dumper(\%animals);
Result is
$VAR1 = {
'Reptiles' => 'Snakes',
'Fish' => 'Clownfish',
'Birds' => 'Parrots',
'Mammals' => [
'Cats',
'Dogs'
]
};
which is as close as you can get to what you had as your desired output.
For ease in processing the ingested data, it may actually be easier to not throw away the arrays in the one-element case so that every entry in the hash can be processed the same way (they're all references to arrays, no matter how many things are in them). Otherwise you've added a conditional to strip out the arrays, and you have to add another conditional test in your processing code to check
if (ref $item) {
# This is an anonymous array
} else {
# This is just a single entry
}
and it's easier to just have one path there instead of two, even if the else just wraps the single item into an array again. Leave them as arrays (delete the $animals{$group} = $animals{$group}->[0] line) and you'll be fine.
Given:
__DATA__
>Mammals
Cats
>Fish
Clownfish
>Birds
Parrots
>Mammals
Dogs
>Reptiles
Snakes
>Reptiles
Snakes
(at the end of the source code or a file with that content)
If you are willing to slurp the file, you can do something with a regex and a HoH like this:
use Data::Dumper;
use warnings;
use strict;
my %animals;
my $s;
while(<DATA>){
$s.=$_;
}
while($s=~/^>(.*)\R(.*)/mg){
++$animals{$1}{$2};
}
print Dumper(\%animals);
Prints:
$VAR1 = {
'Mammals' => {
'Cats' => 1,
'Dogs' => 1
},
'Birds' => {
'Parrots' => 1
},
'Fish' => {
'Clownfish' => 1
},
'Reptiles' => {
'Snakes' => 2
}
};
Which you can arrive to your format with this complete Perl program:
$s.=$_ while(<DATA>);
++$animals{$1}{$2} while($s=~/^>(.*)\R(.*)/mg);
while ((my $k, my $v) = each (%animals)) {
print "$k: ". join(", ", keys($v)) . "\n";
}
Prints:
Fish: Clownfish
Birds: Parrots
Mammals: Cats, Dogs
Reptiles: Snakes
(Know that the output order may be different than file order since Perl hashes do not maintain insertion order...)

Print hash of arrays as colums to file

Is there a way to print a hash of arrays to a file with the keys to the hash as header and the values of the arrays as tab ( or anything else ) delimited columns ?
My last try was something like this:
foreach my $key(sort keys %outHash){
my $temp1 = join ("\n",#{$outHash{$key}});
my $temp2 = $key."\n".$temp1;
print OUTPUT "$temp2\t";
print OUTPUT "\n";
}
Which produces just horrible output.
Any help would be greatly appreciated !
Thank you.
If I understand you correctly - you have a hash of arrays, that you want to print in columns.
To do that:
#!/usr/bin/env perl
use strict;
use warnings;
my %test = (
a => [ 1, 2, 3 ],
b => [ 4, 5, 6, 7 ],
);
my #header = sort keys %test;
print join (",", #header), "\n";
while ( map {#$_} values %test ) {
my #row;
push( #row, shift #{ $test{$_} } // '' ) for #header;
print join (",", #row ), "\n";
}
But you'd probably better off with a different data structure, that's got your data organise row-wise, and iterate row by row using a hash slice instead.
You can try this:
use feature qw(say);
use strict;
use warnings;
my %out_hash = (
a => [1,2,3],
b => [3,4,5],
);
my #keys = sort keys %out_hash;
say join "\t", #keys;
my $N = 3;
for my $i (0 .. ($N-1)) {
my #row;
for my $key (#keys) {
push #row, $out_hash{$key}->[$i];
}
say join "\t", #row;
}
Output:
a b
1 3
2 4
3 5

In Perl, how can I stringify a two dimensional array?

Technically speaking there are no multi-dimensional arrays in Perl, but you can use single dimensional arrays in Perl to act as if they had more than one dimension.
In Perl each element of an array can be a reference to another array, but syntactically they would look like a two-dimensional array.
I want to convert 2-dimensional integer array into string in Perl. I have declared 2-dimensional integer array as follows:
my #array1=[[1,2,3],[1,2,3],[1,2,3]];
OR
my #array2=((1,2,3),(1,2,3),(1,2,3));
now I need to create a subroutine that will return string as "{{1,2,3},{1,2,3},{1,2,3}}". I have tried the following subroutine:
sub TwoDArrayOutputString {
my ($outputs)= #_;
my $finaloutput="{";
foreach my $output ($outputs) {
foreach my $out (#$output) {
$finaloutput.="{"."}";
#$finaloutput.="{".join(',',#output)."}";
}
$finaloutput.=",";
}
$finaloutput.="}";
return $finaloutput;
}
sub TwoDArrayOutputString1 {
my ($outputs)= #_;
if ( ref($outputs) eq "REF" ) {$outputs = ${$outputs};}
my $finaloutput="{";
foreach my $output ($outputs) {
foreach my $out (#$output) {
$finaloutput.="{"."}";
#$finaloutput.="{".join(',',#output)."}";
}
$finaloutput.=",";
}
$finaloutput.="}";
return $finaloutput;
}
sub TwoDArrayOutputString2{
my ($array)= #_;
my $finaloutput="{";
for my $row ( 0..$#array ) {
my #columns = #{ $array[$row] }; # Dereferencing my array reference
$finaloutput.="{";
for my $column ( #columns ) {
$finaloutput.=$column.",";
}
$finaloutput=substr($finaloutput,0,length($finaloutput)-1);
$finaloutput.="}".",";
}
$finaloutput=substr($finaloutput,0,length($finaloutput)-1);
$finaloutput.="}";
return $finaloutput;
}
print TwoDArrayOutputString(#array1)."\n";
print TwoDArrayOutputString1(#array1)."\n";
print TwoDArrayOutputString2(#array1)."\n"."\n"."\n"."\n";
print TwoDArrayOutputString(#array2)."\n";
print TwoDArrayOutputString1(#array2)."\n";
print TwoDArrayOutputString2(#array2)."\n"."\n"."\n"."\n";
Output:
{{}{}{},}
{{}{}{},}
}
{,}
{,}
}
and my expected output is {{1,2,3},{1,2,3},{1,2,3}}.
First off, both of your syntaxes are wrong (compared to what I think you think they do):
my #array1=[[1,2,3],[1,2,3],[1,2,3]];
This results in #array1 holding a single reference to an anonymous array which further holds three references to three anonymous arrays when what I think you want is:
my $array1 = [[1,2,3],[1,2,3],[1,2,3]];
$array1 now is a reference to an array that holds three references to three anonymous arrays.
my #array2=((1,2,3),(1,2,3),(1,2,3));
In this case, you are just fooling yourself with all the extra parentheses: All you have is a single array whose elements are 1, 2, 3, 1, 2, 3, 1, 2, 3.
You say
now I need to create a subroutine that will return string as {{1,2,3},{1,2,3},{1,2,3}}.
That is an odd requirement. Why exactly do you need to create such a subroutine?
If you want to serialize the array as a string, you'd be better off using one of the more standard and interoperable ways of doing it, and pick a format such as JSON, YAML, XML, Data::Dumper, or something else.
For example:
$ perl -MJSON::MaybeXS=encode_json -E '#array1=([1,2,3],[1,2,3],[1,2,3]); say encode_json \#array1'
[[1,2,3],[1,2,3],[1,2,3]]
or
$ perl -MData::Dumper -E '#array1=([1,2,3],[1,2,3],[1,2,3]); say Dumper \#array1'
$VAR1 = [
[
1,
2,
3
],
[
1,
2,
3
],
[
1,
2,
3
]
];
or
$ perl -MYAML::XS -E '#array1=([1,2,3],[1,2,3],[1,2,3]); say Dump \#array1'
---
- - 1
- 2
- 3
- - 1
- 2
- 3
- - 1
- 2
- 3
or
$ perl -MXML::Simple -E '#array1=([1,2,3],[1,2,3],[1,2,3]); say XMLout(\#array1)'
<opt>
<anon>
<anon>1</anon>
<anon>2</anon>
<anon>3</anon>
</anon>
<anon>
<anon>1</anon>
<anon>2</anon>
<anon>3</anon>
</anon>
<anon>
<anon>1</anon>
<anon>2</anon>
<anon>3</anon>
</anon>
</opt>
If your purpose is to learn how to traverse a multi-dimensional structure and print it, doing it correctly requires attention to a few details. You could study the source of YAML::Tiny:
sub _dump_array {
my ($self, $array, $indent, $seen) = #_;
if ( $seen->{refaddr($array)}++ ) {
die \"YAML::Tiny does not support circular references";
}
my #lines = ();
foreach my $el ( #$array ) {
my $line = (' ' x $indent) . '-';
my $type = ref $el;
if ( ! $type ) {
$line .= ' ' . $self->_dump_scalar( $el );
push #lines, $line;
} elsif ( $type eq 'ARRAY' ) {
if ( #$el ) {
push #lines, $line;
push #lines, $self->_dump_array( $el, $indent + 1, $seen );
} else {
$line .= ' []';
push #lines, $line;
}
} elsif ( $type eq 'HASH' ) {
if ( keys %$el ) {
push #lines, $line;
push #lines, $self->_dump_hash( $el, $indent + 1, $seen );
} else {
$line .= ' {}';
push #lines, $line;
}
} else {
die \"YAML::Tiny does not support $type references";
}
}
#lines;
}
Now, for your simple case, you could do something like this:
#!/usr/bin/env perl
use feature 'say';
use strict;
use warnings;
my #array = ([1, 2, 3], [4, 5, 6], [7, 8, 9]);
say arrayref_to_string([ map arrayref_to_string($_), #array]);
sub arrayref_to_string { sprintf '{%s}', join(q{,}, #{$_[0]}) }
Output:
{{1,2,3},{4,5,6},{7,8,9}}
You could do something like below:
#!/usr/bin/perl
use strict;
use warnings;
my #array1=[[1,2,3],[1,2,3],[1,2,3]];
foreach my $aref (#array1){
foreach my $inner (#$aref){
print "{";
foreach my $elem (#$inner){
print "$elem";
print ",";
}
print "}";
}
}
PS: I did not understand second array in your example i.e. my #array2=((1,2,3),(1,2,3),(1,2,3));. It's basically just my #array2=(1,2,3,1,2,3,1,2,3);.
One way could be with Data::Dumper. But correctly pass array or array-refs to Dumper. Your #array2 is one-dimensional array.
#!/usr/bin/perl
use strict;
use warnings;
use Data::Dumper;
my #array1=[[1,2,3],[1,2,3],[1,2,3]];
my $string = Dumper(#array1);
$string =~ s/\n|\s+|.*?=|;//g;
$string =~ s/\[/\{/g;
$string =~ s/\]/\}/g;
print $string."\n";
output:
{{1,2,3},{1,2,3},{1,2,3}}

split numeric array at specific positions

I am trying to split a numerical array into smaller arrays such that each of the smaller arrays cannot contain any numbers that differ.
Example: The array (2,2,2,2,2,9,3,3,3,3) should be split into the three arrays (2,2,2,2,2), (9) and (3,3,3,3).
Here is what I tried:
my #arr = (2,2,2,2,2,9,3,3,3,3);
my #result = ();
my $last = -1;
my #newarr = ();
for my $i (0 .. $#arr){
if ( ($i>0 && $arr[$i] != $last) || $i == $#arr ){
push #result, \#newarr;
#newarr = ();
}
$last = $arr[$i];
push #newarr, $arr[$i];
}
Firstly, this code does not give me the desired result. I think my mistake is when I push the reference to #newarr into #result, but then I re-initialize #newarr.
Secondly, aren't there more elegant ways to do this? I looked at the functions split and splice, but could not think of a good solution.
List::MoreUtils has the "part" function:
use Data::Dumper;
use feature 'state';
use List::MoreUtils 'part';
my #array = ( 2,2,2,2,2, 9, 3,3,3,3 );
my #part = part {
state $prev;
state $i = -1;
$i++ if !defined($prev) || $_ ne $prev;
$prev = $_;
$i
} #array;
print Dumper #part;
With 'part', the value that the code block returns dictates the top level array index where the current value will be pushed into an anonymous array. $prev starts out undefined, so the first element in the input will trigger $i to increment to 0, so all of the '2's will end up in #{$part[0]}. As soon as an element in #array doesn't match $prev, the index will be incremented, and subsequent elements end up in #{$part[1]}. Each time a change is detected, a new grouping starts.
Update:
If this segment of code might be used more than once, the 'state' variables will persist their values across calls. In such a case, state is more trouble than it's worth, and one should just use lexicals in a subroutine:
use Data::Dumper;
use List::MoreUtils 'part';
my #array = ( 2,2,2,2,2, 9, 3,3,3,3 );
my #part = partition(#array);
print Dumper \#part;
sub partition {
my( $prev, $i ) = ( undef, -1 );
return part {
$i++ if ! defined($prev) || $_ ne $prev;
$prev = $_;
$i;
} #_;
}
Creating an array of arrays grouped by like elements.
For a refresher on complex data structures, check out perldsc.
use strict;
use warnings;
my #array = (2,2,2,2,2,9,3,3,3,3);
my #grouped;
for (#array) {
if (! #grouped || $grouped[-1][0] != $_) {
push #grouped, [];
}
push #{$grouped[-1]}, $_;
}
use Data::Dump;
dd #grouped;
Outputs:
([2, 2, 2, 2, 2], [9], [3, 3, 3, 3])
use List::Util 'reduce';
my #arr = (2,2,2,2,2,9,3,3,3,3);
my $result = reduce {
if ( #$a && $b == $a->[-1][0] ) {
push #{ $a->[-1] }, $b
}
else {
push #$a, [ $b ]
}
$a
} [], #arr;
Simpler but maybe more confusing to read:
my $result = reduce {
push #{ $a->[ #$a && $b == $a->[-1][0] ? -1 : #$a ] }, $b;
$a
} [], #arr;
my #arr = (2,2,2,2,2,9,3,3,3,3);
my %h;
my #newarr = map {
my $ok = !$h{$_};
push #{$h{$_}}, $_;
$ok ? $h{$_} : ();
}
#arr;
use Data::Dumper; print Dumper \#newarr;
or
my #arr = (2,2,2,2,2,9,3,3,3,3);
my %h;
my #newarr;
for my $v (#arr) {
if (!$h{$v}) {
push #newarr, ($h{$v} = []);
}
push #{$h{$v}}, $v;
}
output
$VAR1 = [
[
2,
2,
2,
2,
2
],
[
9
],
[
3,
3,
3,
3
]
];
Mandatory regex answer:
my #result = map [ (ord) x length ], grep --$|, join( '', map chr, #arr ) =~ /((.)\2*)/sg;
(under no warnings "non_unicode";).
This will do what you ask. It works by packing the contents of the data as a set of digits and counts and then unpacks it in the required format. The output data is in #num. I have used Data::Dump only to display the resulting data structure.
use strict;
use warnings;
my #arr = (2,2,2,2,2,9,3,3,3,3);
my (%rep, #num);
$rep{$_}++ or push #num, $_ for #arr;
#num = map [ ($_) x $rep{$_} ], #num;
use Data::Dump;
dd \#num;
output
[[2, 2, 2, 2, 2], [9], [3, 3, 3, 3]]
Update
The above solution collects all the elements with the same value into one group, even if they came from separate sequences. If you need the output arrays to be split at every change of value then this will do what you need.
use strict;
use warnings;
my #arr = (2,2,2,2,2,9,9,9,2,2,2,9,9,9);
my #groups;
for (#arr) {
push #groups, [ ] unless #groups and $_ == $groups[-1][-1];
push #{ $groups[-1] }, $_;
}
use Data::Dump;
dd \#groups;
output
[[2, 2, 2, 2, 2], [9, 9, 9], [2, 2, 2], [9, 9, 9]]
Update 2
Here's another version in view of your answer to ikegami's comment, which revealed that a list of values and their associated counts is probably closer to what you need.
use strict;
use warnings;
my #arr = (2,2,2,2,2,9,9,9,2,2,2,9,9,9);
my #groups;
for (#arr) {
if (#groups and $_ == $groups[-1][0]) {
$groups[-1][1] += 1;
}
else {
push #groups, [ $_, 1 ];
}
}
use Data::Dump;
dd \#groups;
output
[[2, 5], [9, 3], [2, 3], [9, 3]]
You can create a hash of arrays where by the key of the has will be a number. and for each time you encounter that number you can push it on to the array reference of the hash. thus all numbers will be split into arrays as you expected. you can then just iterate through the hash to print the arrays or access each array by its number.
use strict;
use Data::Dumper;
my #arr = (2,2,2,2,2,9,3,3,3,3);
my %hash;
push(#{$hash{$_}},$_) foreach (#arr);
print Dumper(\%hash);
output
$VAR1 = {
'3' => [
3,
3,
3,
3
],
'9' => [
9
],
'2' => [
2,
2,
2,
2,
2
]
};

Resources