tags:

views:

510

answers:

4

I have an array @test. What's the best way to check if each element of the array is the same string?

I know I can do it with a foreach loop but is there a better way to do this? I checked out the map function but I'm not sure if that's what I need.

+1  A: 

I use List::Util::first for all similar purposes.

# try #0: $ok = !first { $_ ne $string } @test;
# try #1: $ok = !first { (defined $_ != defined $string) || !/\A\Q$string\E\z/ } @test;

# final solution
use List::Util 'first';
my $str = shift @test;
my $ok = !first { defined $$_ != defined $str || defined $str && $$_ ne $str } map \$_, @test;

I used map \$_, @test here to avoid problems with values that evaluate to false.

Note. As cjm noted fairly, using map defeats the advantage of first short-circuiting. So I tip my hat to Sinan with his first_index solution.

codeholic
But what if `@test` contains the empty string (or 0, or `undef`)? Your test will set `$ok` to true when it should be false.
cjm
Ok. I'll correct.
codeholic
But it's not the test that's wrong. It's the fact that `first` returns the element of `@test` that passed the test. There's no way to distinguish between an `undef` in `@test` and failure to find a match.
cjm
I've fixed everything. And a regular expression is not required. Though the solution is not very laconic.
codeholic
Oh, c'mon... Now what's wrong with my answer?
codeholic
ysth
What's wrong? Well, it kind of showcases the limitations of first() :)
ysth
@ysth Okay, `first` has some limitations. But it shortcuts the loop as soon as mismatch is found as opposed to `grep`. `map \$_, @test` fixes all limitations about `first`. And the heavy condition inside the braces has nothing about `first`. Other answers are just wrong because they don't take `undef` into account.
codeholic
The only problem now is that using `map` kind of defeats the advantage of `first` short-circuiting. But the code actually works, so I removed my downvote.
cjm
+4  A: 

Use grep.

if (@test == grep { $_ eq $string } @test) {
 # all equal
}

If you don't know what the string would be, use hash:

my %string = map { $_, 1 } @test;
if (keys %string == 1) {
 # all equal
}

or even shorter:

if (keys %{{ map {$_, 1} @test }} == 1) {
 # all equal
}

NOTE: The undef value behaves like the empty string ("") when used as a string in perl. Therefore, the checks will return true if you have only empty strings and undefs in your array.

If this is not acceptable, you can code a loop like this:

my $is_equal = 0;
my $string   = $test[0]; # the first element

for my $i (0..$#test) {
    last unless defined $string == defined $test[$i];
    last if defined $test[$i] && $test[$i] ne $string;
    $is_equal = 1 if $i == $#test;
}
eugene y
Bad. You'll always need to traverse the whole array even if there's mismatch in the first element.
codeholic
What I did before I saw this answer was sort the array and check if the first and last elements are the same.Thanks for the code with the hash. I'm still learning to use map properly.
somebody
@Quick Joe Smith: It's not that my current solution is lacking. I was just looking for different ways to do the same thing, and basically use the shortest code so it looks good :P
somebody
Your solution won't work right if `@test` contains `undef` and empty strings.
codeholic
@codeholic: If you use an undef and perl expected a string, perl uses the empty string, "", in its place.
eugene y
@codeholic: "Bad" only if the array may be unusually large. For shorter arrays, grep is the way to go.
ysth
@eugene y: yes, s/he means `grep $_ eq $string, @array` will return a count of elements that are either "" or undef when $string is either "" or undef.
ysth
@eugene You don't know what array size Wireless has.
codeholic
@ysth If you can't see my bristle on the avatar, don't call me or anyone s/he anyway. Use the nickname instead. "S/he" is very annoying.
codeholic
+6  A: 

Both methods in the accepted post give you the wrong answer if @test = (undef, ''). That is, they declare an undefined value to be equal to the empty string.

That might be acceptable. In addition, using grep goes through all elements of the array even if a mismatch is found early on and using the hash more than doubles the memory used by elements of array. Neither of these would be a problem if you have small arrays. And, grep is likely to be fast enough for reasonable list sizes.

However, here is an alternative that 1) returns false for (undef, '') and (undef, 0), 2) does not increase the memory footprint of your program and 3) short-circuits as soon as a mismatch is found:

#!/usr/bin/perl

use strict; use warnings;

# Returns true for an empty array as there exist
# no elements of an empty set that are different
# than each other (see
# http://en.wikipedia.org/wiki/Vacuous_truth)

sub all_the_same {
    my ($ref) = @_;
    return 1 unless @$ref;
    my $cmpv = \ $ref->[-1];
    for my $i (0 .. $#$ref - 1)  {
        my $this = \ $ref->[$i];
        return unless defined $$cmpv == defined $$this;
        return if defined $$this
            and ( $$cmpv ne $$this );
    }
    return 1;
}

However, using List::MoreUtils::first_index is likely to be faster:

use List::MoreUtils qw( first_index );

sub all_the_same {
    my ($ref) = @_;
    my $first = \ $ref->[0];
    return -1 == first_index {
        (defined $$first != defined)
            or (defined and $_ ne $$first)
    } @$ref;
}
Sinan Ünür
+1 Interesting answer. I posted a variant on your first method (but please let me know if I overlooked something).
FM
+3  A: 

TIMTOWTDI, and I've been reading a lot of Mark Jason Dominus lately.

use strict;
use warnings;

sub all_the_same {
    my $ref = shift;
    return 1 unless @$ref;
    my $cmp = $ref->[0];
    my $equal = defined $cmp ?
        sub { defined($_[0]) and $_[0] eq $cmp } :
        sub { not defined $_[0] };
    for my $v (@$ref){
        return unless $equal->($v);
    }
    return 1;
}

my @tests = (
    [ qw(foo foo foo) ],
    [ '', '', ''],
    [ undef, undef, undef ],
    [ qw(foo foo bar) ],
    [ '', undef ],
    [ undef, '' ]
);

for my $i (0 .. $#tests){
    print "$i. ", all_the_same($tests[$i]) ? 'equal' : '', "\n";
}
FM