my @array = qw(foo bar foo bar baz foo baz bar foo);
my %counts = ();
for (@array) {
$counts{$_}++;
}
foreach my $keys (keys %counts) {
print "$keys = $counts{$keys}\n";
}
Wednesday, October 22, 2014
Unique values in an array in Perl
Unique values in an array in Perl
In this part of the Perl tutorial we are going to see how to
make sure we only have distinct values in an array.
Perl 5 does not have a built in function to filter out duplicate values from an array, but there are several solutions to the problem.
In order to use this module you'll have to install it from CPAN.
We start with an empty hash so when we encounter the first "foo", $seen{"foo"} does not exist and thus its value is undef which is considered false in Perl. Meaning we have not seen this value yet. We push the value to the end of the new @uniq array where we are going to collect the distinct values.
We also set the value of $seen{"foo"} to 1. Actually any value would do as long as it is considered "true" by Perl.
The next time we encounter the same string we already have that key in the %seen hash and its value is true, so the if condition will fail, and we won't push the duplicate value in the resulting array.
It is basically a filter. You provide an array on the right hand side and an expression in the block. The grep function will take each value of the array one-by-one, put it in $_, the default scalar variable of Perl and then execute the block. If the block evaluates to TRUE, the value can pass. If the block evaluates to FALSE the current value is filtered out.
That's how we got to this expression:
Inline:
map has a similar syntax to grep: a block and an array (or a list of values). It goes over all the elements of the array, executes the block and passes the result to the left.
In our case, for every value in the array it will pass the value itself followed by the number 1. Remember =>, aka. fat comma, is just a comma. Assuming @data has ('a', 'b', 'a') in it, this expression will return ('a', 1, 'b', 1, 'a', 1).
Finally, starting from perl version 5.14, we can call the keys function on hash references as well. Thus we can write:
input.txt:
expected output:
Perl 5 does not have a built in function to filter out duplicate values from an array, but there are several solutions to the problem.
List::MoreUtils
Depending on your situation, probably the simplest way is to use the uniq function of the List::MoreUtils module from CPAN.A full example is this:
- use List::MoreUtils qw(uniq);
- my @words = qw(foo bar baz foo zorg baz);
- my @unique_words = uniq @words;
The result is:
- use strict;
- use warnings;
- use 5.010;
- use List::MoreUtils qw(uniq);
- use Data::Dumper qw(Dumper);
- my @words = qw(foo bar baz foo zorg baz);
- my @unique_words = uniq @words;
- say Dumper \@unique_words;
$VAR1 = [ 'foo', 'bar', 'baz', 'zorg' ];For added fun the same module also provides a function called distinct, which is just an alias of the uniq function.
In order to use this module you'll have to install it from CPAN.
Home made uniq
If you cannot install the above module for whatever reason, or if you think the overhead of loading it is too big, there is a very short expression that will do the same:This, of course can look cryptic to someone who does not know it already, so it is recommended to define your own uniq subroutine, and use that in the rest of the code:
- my @unique = do { my %seen; grep { !$seen{$_}++ } @data };
- use strict;
- use warnings;
- use 5.010;
- use Data::Dumper qw(Dumper);
- my @words = qw(foo bar baz foo zorg baz);
- my @unique = uniq( @words );
- say Dumper \@unique_words;
- sub uniq {
- my %seen;
- return grep { !$seen{$_}++ } @_;
- }
Home made uniq explained
I can't just throw this example here and leave it like that. I'd better explain it. Let's start with an easier version:Here we are using a regular foreach loop to go over the values in the original array, one by one. We use a helper hash called %seen. The nice thing about the hashes is that their keys are unique.
- my @unique;
- my %seen;
- foreach my $value (@words) {
- if (! $seen{$value}) {
- push @unique, $value;
- $seen{$value} = 1;
- }
- }
We start with an empty hash so when we encounter the first "foo", $seen{"foo"} does not exist and thus its value is undef which is considered false in Perl. Meaning we have not seen this value yet. We push the value to the end of the new @uniq array where we are going to collect the distinct values.
We also set the value of $seen{"foo"} to 1. Actually any value would do as long as it is considered "true" by Perl.
The next time we encounter the same string we already have that key in the %seen hash and its value is true, so the if condition will fail, and we won't push the duplicate value in the resulting array.
Shortening the home made unique function
First of all we replace the assignment of 1 $seen{$value} = 1; by the post-increment operator $seen{$value}++. This does not change the behavior of the previous solution - any positive number is going to be evaluated as TRUE, but it will allow us to include the setting of the "seen flag" within the if condition. It is important that this is a postfix increment (and not a prefix increment) as this means the increment only takes place after the boolean expression was evaluated. The first time we encounter a value the expression will be TRUE and the rest of the times it will be FALSE.This is shorter, but we can do even better.
- my @unique;
- my %seen;
- foreach my $value (@data) {
- if (! $seen{$value}++ ) {
- push @unique, $value;
- }
- }
Filtering duplicate values using grep
The grep function in Perl is a generalized form of the well known grep command of Unix.It is basically a filter. You provide an array on the right hand side and an expression in the block. The grep function will take each value of the array one-by-one, put it in $_, the default scalar variable of Perl and then execute the block. If the block evaluates to TRUE, the value can pass. If the block evaluates to FALSE the current value is filtered out.
That's how we got to this expression:
- my %seen;
- my @unique = grep { !$seen{$_}++ } @words;
Wrapping it in 'do' or in 'sub'
The last little thing we have to do, is wrapping the above two statements in either a do blockor, better yet, in a function with an expressive name:
- my @unique = do { my %seen; grep { !$seen{$_}++ } @words };
- sub uniq {
- my %seen;
- return grep { !$seen{$_}++ } @_;
- }
Home made uniq - round 2
Prakash Kailasa suggested an even shorted version of implementing uniq, for perl version 5.14 and above, if there is no requirement to preserve the order of elements.Inline:
or within a subroutine:
- my @unique = keys { map { $_ => 1 } @data };
Let's take this expression apart:
- my @unique = uniq(@data);
- sub uniq { keys { map { $_ => 1 } @_ } };
map has a similar syntax to grep: a block and an array (or a list of values). It goes over all the elements of the array, executes the block and passes the result to the left.
In our case, for every value in the array it will pass the value itself followed by the number 1. Remember =>, aka. fat comma, is just a comma. Assuming @data has ('a', 'b', 'a') in it, this expression will return ('a', 1, 'b', 1, 'a', 1).
If we assigned that expression to a hash, we would get the original data as keys, and the number 1-es as values. Try this:
- map { $_ => 1 } @data
and you will get:
- use strict;
- use warnings;
- use Data::Dumper;
- my @data = qw(a b a);
- my %h = map { $_ => 1 } @data;
- print Dumper \%h;
$VAR1 = { 'a' => 1, 'b' => 1 };If, instead of assigning it to an array we wrap the above expression in curly braces, we will get a reference to an anonymous hash.
Let's see it in action:
- { map { $_ => 1 } @data }
Will print the same output as the previous one, barring any change in order in the dumping of the hash.
- use strict;
- use warnings;
- use Data::Dumper;
- my @data = qw(a b a);
- my $hr = { map { $_ => 1 } @data };
- print Dumper $hr;
Finally, starting from perl version 5.14, we can call the keys function on hash references as well. Thus we can write:
and we'll get back the unique values from @data
- my @unique = keys { map { $_ => 1 } @data };
Exercise
Given the following file print out the unique values:input.txt:
foo Bar bar first second Foo foo another fooexpected output:
foo Bar bar first second Foo another
Exercise 2
This time filter out duplicates regardless of case.expected output:
foo Bar first second another
Written by
Gabor Szabo
Gabor Szabo
Do you want to improve your Perl?
In order to register to our newsletter, please type your e-mail here: Registered people will be notified when a new article is published on the Perl Maven web site.Published on 2012-09-20
Comments
In the comments, please wrap your code snippets within <pre> </pre> tags and use spaces for indentation.Do you want to improve your Perl?
Register to the FREE Perl Maven newsletter here. or to the Perl Weekly newletter over thereDo you want to become a Pro?
Check out the Perl Maven Pro.
Related articles
For previous articles check out the Archive,
or type in a keyword at the top of the site.
sort hash by value and return the associated key
If instead you want to sort by numeric hash values, you'd write:
foreach my $key (sort { $hash{$a} <=> $hash{$b} } keys %hashes) { do something; }
Subscribe to:
Posts (Atom)