Perl: How does hash assignment with 'map' work? - perl

I have trouble understanding how to assign to a hash using the map function.
Why does
my %a = map {$_=>1 if $_>=2} (1..4);
give me an Odd number of elements in hash assignment error while
my %a = map {$_=>1 if $_>2} (1..4);
gives me
$VAR1 = {
'' => '',
'4' => 1,
'3' => 1
};
and why is there only one empty string in the hash? If I assign to an array
my #a = map {$_ if $_>2} (1..4);
$VAR1 = [
'',
'',
3,
4
];
I get two empty strings, which makes more sense to me.
Is there a possibility to return no empty string if the condition is not met?

Although map is not the best way to do this job (grep as pointed out would be better), it is still possible just using map with the ? comparison:
#!/usr/bin/perl
use strict ;
use warnings ;
use Data::Dumper ;
my %a = map { $_>2 ? ( $_ => 1 ) : () } (1..4) ;
print Dumper( \%a ) ;
Returning the empty list makes map behave like grep when condition is not met.
>perl test.pl
$VAR1 = {
'4' => 1,
'3' => 1
};

map transforms a list into another list. In the first case, your input list is 1, 2, 3, 4. For each member, you return a tuple if the member is >= 2, but otherwise, you return just a single value. The single value is returned for 1 only and causes the "odd number of elements".
In the second case, the transformation works as follows:
input | output
------+-------
1 | ''
2 | ''
3 | 3 => 1
4 | 4 => 1
If you make a hash from it, you take the first empty string as the key, the second empty string as the value, which creates "one empty string in the hash" - there are in fact two.

Related

How to print specific key in an array (Perl) [duplicate]

This question already has answers here:
Simple hash search by value
(5 answers)
Closed 5 years ago.
I recently started learning Perl, so I'm not too familiar with the functions and syntax.
If I have a Perl array and some variables,
#!/usr/bin/perl
use strict;
use warnings;
my #numbers = (a =>1, b=> 2, c => 3, d =>4, e => 5);
my $x;
my $range = 5;
$x = int(rand($range));
print "$x";
to generate a random number between 1-5, how can I get the program to print the actual key (a, b, c, etc.) instead of just the number (1, 2, 3, 4, 5)?
It seems that you want to do a reverse lookup, key-by-value, opposite to what we get from a hash. Since a hash is a list you can reverse it and use the resulting hash to look up by number.
A couple of corrections: you need a hash variable (not an array), and you need to add 1 to your rand integer generator so to have the desired 1..5 range
use warnings;
use strict;
use feature 'say';
my %numbers = (a => 1, b => 2, c => 3, d => 4, e => 5);
my %lookup_by_number = reverse %numbers; # values need be unique
my $range = 5;
my $x = int(rand $range) + 1;
say $lookup_by_number{$x};
Without reversing the hash you'd need to iterate the hash %numbers over values, testing each against $x so to find its key.
If there are same values for various keys in your original hash then you have to do it by hand since reverse-ing would attempt to create a hash with duplicate keys, in which case only the last one assigned remains. So you'd lose some values. One way
my #at_num = grep { $x == $numbers{$_} } keys %numbers;
as in the post that this was marked as duplicate of.
But then you should build a data structure for reverse lookup so to not search through the list every time information is needed. This can be a hash where keys are the list of unique numbers while their values are then array references (arrayrefs) with corresponding keys from the original hash
use warnings;
use strict;
my %num = (a => 1, b => 2, c => 1, d => 3, e => 2); # with duplicate values
my %lookup_by_num;
foreach my $key (keys %num) {
push #{ $lookup_by_num{$num{$key}} }, $key;
}
say "$_ => [ #{$lookup_by_num{$_}} ]" for keys %lookup_by_num;
This prints
1 => [ c a ]
3 => [ d ]
2 => [ e b ]
A nice way to display complex data structures is via Data::Dumper, or Data::Dump (or others).
The expression #{ $lookup_by_num{ $num{$key} } } extracts the value of %lookup_by_num for the key $num{$key}and dereferences it #{ ... }, so that it can then push the $key to it. The critical part of this is that the first time it encounters $num{$key} it autovivifies the arrayref and its corresponding key. See this post with its references for details.
There's many ways to do it. For example, declare "numbers" as a hash rather than an array. Note that the keys come first in each key-value pair, and here you want to use your random int as the key:
my %numbers = ( 0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd', 4 => 'e' );
Then you can look up the "key" as you call it using:
my $key = $numbers{$x};
Note that rand( $x ); returns a number greater than or equal to zero and less than $x. So if you want integers in the range 1-5, you must add 1 in your code: at the moment you'll get 0-4, not 1-5.
Firstly, arrays don't have keys (well, they kind of do, but they're integers and not the values you want). So I think you want a hash, not an array.
my %numbers = (a =>1, b=> 2, c => 3, d =>4, e => 5);
And if you want to get the letter, given the integer then you need the reverse of this hash:
my %rev_numbers = %numbers;
Note that reversing a hash like this only works if the values in your original hash are unique (because reversing a hash makes the values into keys and hash keys are always unique).
Then, you can just look up an integer in your %rev_hash to get its associated letter.
my $integer = 3;
say $rev_numbers{$integer}; # prints 'c'

Perl backticks in hash ref giving different results

I have a file called a.gz which is a gzipped file which contains the following lines when unzipped:
a
b
Below are two blocks of perl code which I think "should" give the same results but they don't.
Code #1:
use Data::Dumper;
my $s = {
status => 'ok',
msg => `zcat a.gz`
};
print Dumper($s),"\n";
Code #2:
use Data::Dumper;
my $content = `zcat a.gz`;
my $s = {
status => 'ok',
msg => $content
};
print Dumper($s), "\n";
Code #1 gives the following result:
Odd number of elements in anonymous hash at ./x.pl line 8.
$VAR1 = {
'msg' => 'a
',
'b
' => undef,
'status' => 'ok'
};
Code #2 returns the following result:
$VAR1 = {
'msg' => 'a
b
',
'status' => 'ok'
};
I'm using perl 5.10.1 running in Linux
perldoc perlop:
In scalar context, it comes back as a single (potentially multi-line) string, or undef if the command failed. In list context, returns a list of lines (however you've defined lines with $/ or $INPUT_RECORD_SEPARATOR), or an empty list if the command failed.
Assigning to a scalar puts `` in scalar context; using it in { ... } puts it in list context.
{ LIST } takes a list and interprets its contents alternating between keys and values, i.e. key1, value1, key2, value2, key3, value3, .... If the number of elements is odd, you get a warning (and the missing value is taken to be undef).
LIST , LIST (the comma operator in list context) concatenates two lists.
=> works just like , but automatically quotes the identifier to its left (if there is one).

Perl: Get position and length of element in a string

Say I have a string like:
my $refseq="CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
I want to detect the positions where "-" occurs and the number of contiguous "-". I want to end up with a hash with "-" position as key, and extension length as value, for this example above:
%POSLENGTH = (5 => 1, 8 => 3, 14 => 2, 19 => 1, 27 => 20);
Note that the positions should be given based on the string without "-".
Check for #- array in perlval
my $refseq = "CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
my %POSLENGTH;
$POSLENGTH{ $-[0] +1 } = length($1) while $refseq =~ s/(-+)//;
use Data::Dumper; print Dumper \%POSLENGTH;
output
$VAR1 = {
'14' => 2,
'8' => 3,
'27' => 20,
'19' => 1,
'5' => 1
};
You can do this using the built-in #- and #+ arrays. Together they hold the start and end offsets of the last successful pattern match in element 0 (and of any captures in elements 1 onwards) so clearly the length of the last match is $+[0] - $-[0].
They're documented under Variables related to regular expressions in perldoc perlvar.
I've used Data::Dump here just to display the contents of the hash that is built
On a side note, I'm very doubtful that a hash is a useful structure for this information as I can't imagine a situation where you know the start position of a substring and need to know its length. I would have thought it was better represented as just an array of pairs
use strict;
use warnings;
use Data::Dump;
my $refseq="CCCC-TGA---ATAAAC--TCCAT-GCTCCCCC--------------------AAGC";
my %pos_length;
while ( $refseq =~ /-+/g ) {
my ($pos, $len) = ( $-[0] + 1, $+[0] - $-[0] );
$pos_length{$pos} = $len;
}
dd \%pos_length;
output
{ 5 => 1, 9 => 3, 18 => 2, 25 => 1, 34 => 20 }

Find duplicates in a hash, store grouped in a new hash

I have the following hash, and I need to find the duplicates between the top most hash values 6 and 4. I've tried a few solutions to no avail, and am not too familiar with Perl syntax to make it work.
The Hash I Have
$VAR1 = {
'6' => [ '1000', '2000', '4000' ],
'4' => [ '1000', '2000', '3000' ]
};
The Hash I Need
$VAR1 = {
'6' => ['4000'],
'4' => ['3000'],
'Both' => ['1000','2000']
}
Find all common elements, e.g. by deduplicating with a hash.
Find all elements that are not common.
Given two arrays #x, #y, this would mean:
use List::MoreUtils 'uniq';
# find all common elements
my %common;
$common{$_}++ for uniq(#x), uniq(#y); # count all elements
$common{$_} == 2 or delete $common{$_} for keys %common;
# remove entries from #x, #y that are common:
#x = grep { not $common{$_} } #x;
#y = grep { not $common{$_} } #y;
# Put the common strings in an array:
my #common = keys %common;
Now all that is left is to do a bit of dereferencing and such, but that should be fairly trivial.
No need for other modules. perl hashes are really good for finding uniq or common values
my %both;
# count the number of times any element was seen in 4 and 6
$both{$_}++ for (#{$VAR1->{4}}, #{$VAR1->{6}});
for (keys %both) {
# if the count is one the element isn't in both 4 and 6
delete $both{$_} if( $both{$_} == 1 );
}
$VAR1->{Both} = [keys %both];

How can I find out if a hash has an odd number of elements in assignment?

How could I find out if this hash has an odd number of elements?
my %hash = ( 1, 2, 3, 4, 5 );
Ok, I should have written more information.
sub routine {
my ( $first, $hash_ref ) = #_;
if ( $hash_ref refers to a hash with odd numbers of elements ) {
"Second argument refers to a hash with odd numbers of elements.\nFalling back to default values";
$hash_ref = { option1 => 'office', option2 => 34, option3 => 'fast' };
}
...
...
}
routine( [ 'one', 'two', 'three' ], { option1 =>, option2 => undef, option3 => 'fast' );
Well, I suppose there is some terminological confusion in the question that should be clarified.
A hash in Perl always has the same number of keys and values - because it's fundamentally an engine to store some values by their keys. I mean, key-value pair should be considered as a single element here. )
But I guess that's not what was asked really. ) I suppose the OP tried to build a hash from a list (not an array - the difference is subtle, but it's still there), and got the warning.
So the point is to check the number of elements in the list which will be assigned to a hash. It can be done as simple as ...
my #list = ( ... there goes a list ... );
print #list % 2; # 1 if the list had an odd number of elements, 0 otherwise
Notice that % operator imposes the scalar context on the list variable: it's simple and elegant. )
UPDATE as I see, the problem is slightly different. Ok, let's talk about the example given, simplifying it a bit.
my $anhash = {
option1 =>,
option2 => undef,
option3 => 'fast'
};
See, => is just a syntax sugar; this assignment could be easily rewritten as...
my $anhash = {
'option1', , 'option2', undef, 'option3', 'fast'
};
The point is that missing value after the first comma and undef are not the same, as lists (any lists) are flattened automatically in Perl. undef can be a normal element of any list, but empty space will be just ignored.
Take note the warning you care about (if use warnings is set) will be raised before your procedure is called, if it's called with an invalid hash wrapped in reference. So whoever caused this should deal with it by himself, looking at his own code: fail early, they say. )
You want to use named arguments, but set some default values for missing ones? Use this technique:
sub test_sub {
my ($args_ref) = #_;
my $default_args_ref = {
option1 => 'xxx',
option2 => 'yyy',
};
$args_ref = { %$default_args_ref, %$args_ref, };
}
Then your test_sub might be called like this...
test_sub { option1 => 'zzz' };
... or even ...
test_sub {};
The simple answer is: You get a warning about it:
Odd number of elements in hash assignment at...
Assuming you have not been foolish and turned warnings off.
The hard answer is, once assignment to the hash has been done (and warning issued), it is not odd anymore. So you can't.
my %hash = (1,2,3,4,5);
use Data::Dumper;
print Dumper \%hash;
$VAR1 = {
'1' => 2,
'3' => 4,
'5' => undef
};
As you can see, undef has been inserted in the empty spot. Now, you can check for undefined values and pretend that any existing undefined values constitutes an odd number of elements in the hash. However, should an undefined value be a valid value in your hash, you're in trouble.
perl -lwe '
sub isodd { my $count = #_ = grep defined, #_; return ($count % 2) };
%a=(a=>1,2);
print isodd(%a);'
Odd number of elements in hash assignment at -e line 1.
1
In this one-liner, the function isodd counts the defined arguments and returns whether the amount of arguments is odd or not. But as you can see, it still gives the warning.
You can use the __WARN__ signal to "trap" for when a hash assignment is incorrect.
use strict ;
use warnings ;
my $odd_hash_length = 0 ;
{
local $SIG{__WARN__} = sub {
my $msg = shift ;
if ($msg =~ m{Odd number of elements in hash assignment at}) {
$odd_hash_length = 1 ;
}
} ;
my %hash = (1, 2, 3, 4, 5) ;
}
# Now do what you want based on $odd_hash_length
if ($odd_hash_length) {
die "the hash had an odd hash length assignment...aborting\n" ;
} else {
print "the hash was initialized correctly\n";
}
See also How to capture and save warnings in Perl.