I have a data structure flattened by Hash::Flatten
For example,
flatten( { a => [ 'x', { b => 'y' } ] } )
produces
my $flat = {
'a:0' => 'x'
'a:1.b' => 'y',
};
I want to generate a flattened hash key from the a list of keys and indexes from a key Data::Diver's functions would accept.
For example,
my #key = ('a', 1, 'b');
should return
my $key = "a:1.b";
I have looked at Hash::Flatten, but it seems it can only flatten the whole hash, which is not what I am looking for. I just want to flatten a single (nested) key at a time.
To avoid replicating the escaping mechanism of Hash::Flatten, I tried the following:
use Data::Diver qw( DiveVal );
use Hash::Flatten qw( flatten );
my #key = ('a', 1, 'b');
DiveVal(my $h = {}, #key) = 1;
my ($key) = keys(%{ flatten($h) );
But that can just as easily return a:0 as a:1.b. Does anyone have any recommendations?
Only the key in which you are interested will have a defined value, so only a small change is needed.
use Data::Diver qw( DiveVal );
use Hash::Flatten qw( flatten );
sub flat_key {
DiveVal(my $h = {}, #_) = 1;
my $flat = flatten($h);
return ( grep $flat->{$_}, keys(%$flat) )[0];
}
my #key = ('a', 1, 'b');
my $key = flat_key(#key); # a:1.b
Because this uses Data::Diver, you can also use references to indicate that a number is really a hash key.
my #key = ('a', 1, 'b');
my $key = flat_key(map \$_, #key); # a.1.b
Alternatively, the escaping mechanism is well documented.
sub _flat_key_escape {
my ($s) = #_;
$s =~ s/([\\.:])/\\$1/g;
return $s;
}
sub flat_key {
my $key;
die("usage") if !#_;
for my $subkey (#_) {
if (ref($subkey)) { $key .= '.' . _flat_key_escape($$subkey); }
elsif ($subkey !~ /^-?[0-9]+\z/) { $key .= '.' . _flat_key_escape($subkey); }
else { $key .= ':' . _flat_key_escape($subkey); }
}
return substr($key, 1);
}
This is simple to do without reference to either Hash::Flatten or Data::Diver. The latter's DiveVal distinguishes between hash keys and array indices using the regex /^-?\d+$/, so we can do the same to discover whether a item in a sequence's Hash::Flatten default contraction should be preceded by a colon : (array index) or a dot . (hash key).
That gives the subroutine flatten_key below
use strict;
use warnings;
use 5.010;
my #key = ('a', 1, 'b');
my $key = flatten_key(#key);
say $key;
say flatten_key(qw/ a b c 1 2 3 /);
sub flatten_key {
join '', shift, map /^-?\d+$/ ? ":$_" : ".$_", #_;
}
output
a:1.b
a.b.c:1:2:3
Update
If you need to use the Data::Diver convention that any value passed as a scalar reference is a hash key, even if it looks like a number, then you can expand that subroutine like this. It's slightly more awkward because the first item in the sequence needs to be processed as well, but for some reason it doesn't take a delimiter character. So I've chosen to add a delimiter to all the items and then remove it from the first.
say flatten_key('a', 'b', \1, \2, 'c', 'd', 1, 2);
sub flatten_key {
my #key = map {
ref() ? ".$$_" :
/^-?\d+$/ ? ":$_" :
".$_"
} #_;
$key[0] =~ s/^[:.]//;
join '', #key;
}
output
a.b.1.2.c.d:1:2
Update
Also accounting for hash keys that themselves contain dots or colons:
say flatten_key(qw/ a .. :: b /);
sub flatten_key {
my #key = map {
(my $s = ref() ? $$_ : $_) =~ s/(?=[:.\\])/\\/g;
/^-?\d+$/ ? ":$s" : ".$s"
} #_;
$key[0] =~ s/^[:.]//;
join '', #key;
}
output
a.\.\..\:\:.b
Related
I have an array of "words" (strings), which consist of letters from an "alphabet" with user-defined sequence. E.g my "alphabet" starts with "ʔ ʕ b g d", so a list of "words" (bʔd ʔbg ʕʔb bʕd) after sort by_my_alphabet should be ʔbd ʕʔb bʔd bʕd.
sort by_my_alphabet (bʔd ʔbg ʕʔb bʕd) # gives ʔbd ʕʔb bʔd bʕd
Is there a way to make a simple subroutine by_my_alphabet with $a and $b to solve this problem?
Simple, and very fast because it doesn't use a compare callback, but it needs to scan the entire string:
use utf8;
my #my_chr = split //, "ʔʕbgd";
my %my_ord = map { $my_chr[$_] => $_ } 0..$#my_chr;
my #sorted =
map { join '', #my_chr[ unpack 'W*', $_ ] } # "\x00\x01\x02\x03\x04" ⇒ "ʔʕbgd"
sort
map { pack 'W*', #my_ord{ split //, $_ } } # "ʔʕbgd" ⇒ "\x00\x01\x02\x03\x04"
#unsorted;
Optimized for long strings since it only scans a string up until a difference is found:
use utf8;
use List::Util qw( min );
my #my_chr = split //, "ʔʕbgd";
my %my_ord = map { $my_chr[$_] => $_ } 0..$#my_chr;
sub my_cmp($$) {
for ( 0 .. ( min map length($_), #_ ) - 1 ) {
my $cmp = $my_ord{substr($_[0], $_, 1)} <=> $my_ord{substr($_[1], $_, 1)};
return $cmp if $cmp;
}
return length($_[0]) <=> length($_[1]);
}
my #sorted = sort my_cmp #unsorted;
Both should be faster than Sobrique's. Theirs uses a compare callback, and it scans the entire strings being compared.
Yes.
sort can take any function that returns a relative sort position. All you need is a function that correctly looks up the 'sort value' of a string for comparing.
So all you need to do here is define a 'relative weight' of your extra letters, and then compare the two.
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
my #sort_order = qw ( B C A D );
my #array_to_sort = qw ( A B C D A B C D AB BB CCC ABC );
my $count = 0;
my %position_of;
$position_of{$_} = $count++ for #sort_order;
print Dumper \%position_of;
sub sort_by_pos {
my #a = split //, $a;
my #b = split //, $b;
#iterate one letter at a time, using 'shift' to take it off the front
#of the array.
while ( #a and #b ) {
my $result = $position_of{shift #a} <=> $position_of{shift #b};
#result is 'true' if it's "-1" or "1" which indicates relative position.
# 0 is false, and that'll cause the next loop iteration to test the next
#letter-pair
return $result if $result;
}
#return a value based on remaining length - longest 'string' will sort last;
#That's so "AAA" comparing with "AA" comparison actually work,
return scalar #a <=> scalar #b;
}
my #new = sort { sort_by_pos } #array_to_sort;
print Dumper \#new;
Bit of a simple case, but it sorts our array into:
$VAR1 = [
'B',
'B',
'BB',
'C',
'C',
'CCC',
'A',
'A',
'AB',
'ABC',
'D',
'D'
];
I have three hashes that I would like to explode each one into a Hash of Arrays. Each one could be passed into a subroutine with a split function, but this requires the same subroutine to be called 3 times. I have tried to iterate through each hash and them split it but without success. Is it possible to do this without calling a subroutine? The code I have tried is:
#!/usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
my %hash1 = (
A => "AAAAAAAAAA",
B => "AAAAANAAAA",
C => "AAAAAXAAAA",
D => "AAXAAAAAAA",
E => "AAAAAAAAAX",
);
my %hash2 = (
F => "BBBBBBBBBB",
G => "BBBBBBBBBB",
H => "BBXBBBBBBB",
I => "BBBBBBBBBB",
J => "AAAAANAAAA",
);
foreach my $ref ( \%hash1, \%hash2 ) {
while ( my ($key, $value) = each %$ref) {
#{ $ref->{$key} } = split (//, $value );
}
}
print Dumper %hash1;
but gives the warning:
Can't use string ("AAAAAAAAAA") as an ARRAY ref while "strict refs" in use
Thanks in advance.
Your mistake is that you are taking the value of the key $ref->{$key} which is AAAAAAA etc, and using it as a reference with the #{ ... } braces. You need to assign to $ref->{$key} directly.
Since you are splitting the original value into a list, you need an array to store it. You can do this either by using a named array, lexically scoped, or an anonymous array. My choice would be to use an anonymous array, but a named array might appear more readable:
foreach my $ref ( \%hash1, \%hash2 ) {
while ( my ($key, $value) = each %$ref) {
$ref->{$key} = [ split (//, $value ) ]; # using anonymous array
# my #list = split (//, $value );
# $ref->{$key} = \#list; # using named array
}
}
This process would overwrite the original values, but I assume that is what you want.
A more direct way to achieve the same thing would be this:
$_ = [ split // ] for values %hash1, values %hash2;
Here we are (ab)using the fact that the elements in a for loop are aliased to the original variables, and simply overwriting the values the same way we did above.
If the compressed format is unwanted, a more verbose alternative would be:
for my $value (values %hash1, values %hash2) {
$value = [ split //, $value ];
}
Assuming I correctly understood what you want to do, you can use hash slicing and do something like this:
#!/usr/bin/perl
use warnings;
use strict;
use Data::Dumper;
my %hash1 = (
A => "AAAAAAAAAA",
B => "AAAAANAAAA",
C => "AAAAAXAAAA",
D => "AAXAAAAAAA",
E => "AAAAAAAAAX",
);
my %hash2 = (
F => "BBBBBBBBBB",
G => "BBBBBBBBBB",
H => "BBXBBBBBBB",
I => "BBBBBBBBBB",
J => "AAAAANAAAA",
);
foreach my $ref ( \%hash1, \%hash2 ) {
my #keys = keys %$ref;
#$ref{#keys} = map { [split //, $ref->{$_}] } #keys;
}
print Dumper \%hash1;
print Dumper \%hash2;
This is like writing:
foreach my $ref ( \%hash1, \%hash2 ) {
my #keys = keys %$ref;
foreach my $key (#keys) {
my #arr = split //, $ref->{$key};
$ref->{$key} = \#arr;
}
}
just simpler.
my %hash1 = (
a=>192.168.0.1,
b=>192.168.0.1,
c=>192.168.2.2,
d=>192.168.2.3,
e=>192.168.3.4,
f=>192.168.3.4
);
i have a perl hash like given above. keys are device names and values are ip addresses.How do i create a hash with no duplicate ip addresses (like %hash2) using %hash1? (devices that have same ips are removed)
my %hash2 = ( c=>192.168.2.2, d=>192.168.2.3 );
First of all you need to quote your IP addresses, because 192.168.0.1 is V-String in perl, means chr(192).chr(168).chr(0).chr(1).
And my variant is:
my %t;
$t{$_}++ for values %hash1; #count values
my #keys = grep
{ $t{ $hash1{ $_ } } == 1 }
keys %hash1; #find keys for slice
my %hash2;
#hash2{ #keys } = #hash1{ #keys }; #hash slice
How about:
my %hash1 = (
a=>'192.168.0.1',
b=>'192.168.0.1',
c=>'192.168.2.2',
d=>'192.168.2.3',
e=>'192.168.3.4',
f=>'192.168.3.4',
);
my (%seen, %out);
while( my ($k,$v) = each %hash1) {
if ($seen{$v}) {
delete $out{$seen{$v}};
} else {
$seen{$v} = $k;
$out{$k} = $v;
}
}
say Dumper\%out;
output:
$VAR1 = {
'c' => '192.168.2.2',
'd' => '192.168.2.3'
};
A solution using the CPAN module List::Pairwise:
use strict;
use warnings;
use List::Pairwise qw( grep_pairwise );
use Data::Dumper;
my %hash1 = (
a => '192.168.0.1',
b => '192.168.0.1',
c => '192.168.2.2',
d => '192.168.2.3',
e => '192.168.3.4',
f => '192.168.3.4'
);
my %count;
for my $ip ( values %hash1 ) { $count{ $ip }++ }
my %hash2 = grep_pairwise { $count{ $b } == 1 ? ( $a => $b ) : () } %hash1;
print Dumper \%hash2;
It's pretty straightforward. First you count the IPs in an auxiliary hash. And then you select only those IPs with a count of one using grep_pairwise from List::Pairwise. The syntax of grep_pairwise is like grep:
my #result = grep_pairwise { ... } #list;
The idea of grep_pairwise is to select the elements of #list two by two, with $a representing the first element of the pair, and $b the second (in this case the IP). (Remember that a hash evaluates to a list of ($key1, $value1, $key2, $value2, ...) pairs in list context).
I am looking for search implementation on hash using perl. I have following data in my hash
%hash = {0 => "Hello", 1=> "world"}.
Now i want to search the hash using the values (means world and hello) and return corresponding key.
Example: I want to search for world and the result should be 1
Iterate of the keys of the hash with a for ( keys %hash ) ... statement and check the values as you go. If you find what you are looking for, return
my $hash = { 0 => "World", 1 => "Hello" };
for ( keys %$hash ) {
my $val = $hash->{$_};
return $_ if $val eq 'World'; # or whatever you are looking for
}
another option would be to use while ( ... each ... )
my $hash = { 0 => "World", 1 => "Hello" };
while (($key, $val) = each %$hash) {
return $key if $val eq 'World'; # or whatever you are looking for
}
the use of { } literal creates a hash reference and not a hash
$h = { a => 'b', c => 'd' };
to create a literal hash you use ( )
%h = ( a => 'b', c => 'd' );
execution of while ... each on hashref
$h = { a => 'b', c => 'd' };
print "$k :: $v\n" while (($k, $v) = each %$h );
c :: d
a :: b
If:
The hash isn't very large, and
The values are unique
You can simply create a lookup hash with reverse:
my %lookup = reverse %hash;
my $key = $lookup{'world'}; # key from %hash or undef
use strict;
use warnings;
my %hash = (0 => "Hello", 1=> "world");
my $val = 'world';
my #keys = grep { $hash{$_} eq $val } keys %hash;
print "Keys: ", join(", ", #keys), "\n";
This will return all keys i.e. If the value is same for multiple keys.
I would like to make the value the key, and the key the value. What is the best way to go about doing this?
Adapted from http://www.dreamincode.net/forums/topic/46400-swap-hash-values/:
Assuming your hash is stored in $hash:
while (($key, $value) = each %hash) {
$hash2{$value}=$key;
}
%hash=%hash2;
Seems like much more elegant solution can be achieved with reverse (http://www.misc-perl-info.com/perl-hashes.html#reverseph):
%nhash = reverse %hash;
Note that with reverse, duplicate values will be overwritten.
Use reverse:
use Data::Dumper;
my %hash = ('month', 'may', 'year', '2011');
print Dumper \%hash;
%hash = reverse %hash;
print Dumper \%hash;
As mentioned, the simplest is
my %inverse = reverse %original;
It "fails" if multiple elements have the same value. You could create an HoA to handle that situation.
my %inverse;
push #{ $inverse{ $original{$_} } }, $_ for keys %original;
So you want reverse keys & vals in a hash? So use reverse... ;)
%hash2 = reverse %hash;
reverting (k1 => v1, k2 => v2) - yield (v2=>k2, v1=>k1) - and that is what you want. ;)
my %orig_hash = (...);
my %new_hash;
%new_hash = map { $orig_hash{$_} => $_ } keys(%orig_hash);
The map-over-keys solution is more flexible. What if your value is not a simple value?
my %forward;
my %reverse;
#forward is built such that each key maps to a value that is a hash ref:
#{ a => 'something', b=> 'something else'}
%reverse = map { join(',', #{$_}{qw(a b)}) => $_ } keys %forward;
Here is a way to do it using Hash::MultiValue.
use experimental qw(postderef);
sub invert {
use Hash::MultiValue;
my $mvh = Hash::MultiValue->from_mixed(shift);
my $inverted;
$mvh->each( sub { push $inverted->{ $_[1] }->#* , $_[0] } ) ;
return $inverted;
}
To test this we can try the following:
my %test_hash = (
q => [qw/1 2 3 4/],
w => [qw/4 6 5 7/],
e => ["8"],
r => ["9"],
t => ["10"],
y => ["11"],
);
my $wow = invert(\%test_hash);
my $wow2 = invert($wow);
use DDP;
print "\n \%test_hash:\n\n" ;
p %test_hash;
print "\n \%test_hash inverted as:\n\n" ;
p $wow ;
# We need to sort the contents of the multi-value array reference
# for the is_deeply() comparison:
map {
$test_hash{$_} = [ sort { $a cmp $b || $a <=> $b } #{ $test_hash{$_} } ]
} keys %test_hash ;
map {
$wow2->{$_} = [ sort { $a cmp $b || $a <=> $b } #{ $wow2->{$_} } ]
} keys %$wow2 ;
use Test::More ;
is_deeply(\%test_hash, $wow2, "double inverted hash == original");
done_testing;
Addendum
Note that in order to pass the gimmicky test here, the invert() function relies on %test_hash having array references as values. To work around this if your hash values are not array references, you can "coerce" the regular/mixed hash into a multi-value hash thatHash::MultiValue can then bless into an object. However, this approach means even single values will appear as array references:
for ( keys %test_hash ) {
if ( ref $test_hash{$_} ne 'ARRAY' ) {
$test_hash{$_} = [ $test_hash{$_} ]
}
}
which is longhand for:
ref($_) or $_ = [ $_ ] for values %test_hash ;
This would only be needed to get the "round trip" test to pass.
Assuming all your values are simple and unique strings, here is one more easy way to do it.
%hash = ( ... );
#newhash{values %hash} = (keys %hash);
This is called a hash slice. Since you're using %newhash to produce a list of keys, you change the % to a #.
Unlike the reverse() method, this will insert the new keys and values in the same order as they were in the original hash. keys and values always return their values in the same order (as does each).
If you need more control over it, like sorting it so that duplicate values get the desired key, use two hash slices.
%hash = ( ... );
#newhash{ #hash{sort keys %hash} } = (sort keys %hash);