eXile has asked for the wisdom of the Perl Monks concerning the following question:
esteemed monks,
I'm overheating my brain on a function that checks if 2 values are equal, so i don't have to update a certain column in a SQL database. There are some things I think I need to take care of (like 0 vs "" vs undef, which are different in SQL).
I don't have the SQL-column definition available at compare time.
My 'equals' function currently looks like this:
sub equals {
my ($val1, $val2) = @_;
# first test for undef-ness
if ( ! defined $val1 || ! defined $val2 ) {
return 1 if (! defined $val1 && ! defined $val2 );
return undef; #case one is undef, other is def
}
# have to do next line this because strings evaluate to 0 in == co
+ntext ...
if ( ! $val1 && ! $val2 ) {
# then test for ""-ness
return 1 if ( ($val1 eq "") && ($val2 eq "") );
return undef if ( $val1 eq "" || $val1 eq "" );
#case one is "" other is 0
# then test for 0-ness
return 1 if ( ($val1 == 0) && ($val2 == 0) );
}
# then test for equality using 'eq'
return 1 if ( $val1 eq $val2 );
# then test for equality using '=='
if ( int($val1) && int($val2) ) {
# all other number cases are already covered by
# above tests (eg. both 0, or both the same string)
# this merely takes care of cases like:
# 1000.0000 == 1000
return 1 if ( $val1 == $val2 );
}
# couldn't find any, so i guess they are not equal
return undef;
}
This looks very bloated (and a candidate for thedailywtf ), and it feels like there must be bugs lurking in this.
I'm pretty sure this can be done a lot easier/better. Anybody got a better/smarter way to accomplish this type of comparing of 2 variables?
I'm sure somebody can come up with a nicer solution, that'll make me bang my head at my desk for at least an hour.
Re: check if 2 values are equal
by GrandFather (Saint) on Jan 24, 2006 at 20:45 UTC
|
use strict;
use warnings;
my @pairs =
(
[undef, undef], [0, undef], [0, 0], ['', undef], ['', ''], [1, und
+ef], [1, 1],
[0, '0'], [0, 0], ['0', '0'], ['0e0', '0'], ['x', '0'], ['x', 0],
);
for (@pairs)
{
my @pair = (@$_);
print defined $pair[0] ? ">$pair[0]<" : 'undef';
print ', ' . (defined $pair[1] ? ">$pair[1]<" : 'undef');
print ': ' . (equals (@$_) ? "match\n" : "different\n");
}
sub equals {
my ($lhs, $rhs) = @_;
#False if mix of defined and undef
return 0 if defined $lhs != defined $rhs;
# True if both undef
return 1 if ! defined $lhs && ! defined $rhs;
# False if different as numbers
no warnings "numeric";
return 0 if (0 + $lhs) != (0 + $rhs);
# False if different as strings
return 0 if ('' . $lhs) ne ('' . $rhs);
return 1;
}
Prints:
undef, undef: match
>0<, undef: different
>0<, >0<: match
><, undef: different
><, ><: match
>1<, undef: different
>1<, >1<: match
>0<, >0<: match
>0<, >0<: match
>0<, >0<: match
>0e0<, >0<: different
>x<, >0<: different
>x<, >0<: different
DWIM is Perl's answer to Gödel
| [reply] [Watch: Dir/Any] [d/l] [select] |
|
thanks very much, this does what i'd like it to do, and if i compare it to my orginal function it even takes care of the 'numeric 0' vs. 'empty string' inequality.
my @vals = (undef, 0, '0', 'aap', 1, 't','');
my @expl = ('undef','numeric 0','string 0', 'string "aap"', 'nu
+mber 1', 'string t','empty string');
my ($i,$j);
for ($i=0; $i <= scalar(@vals-1); $i++) {
for ($j=0; $j <= scalar(@vals-1); $j++) {
# equals 1
print "equals_org '$expl[$i]'\t'$expl[$j]'\t" . equals_org($va
+ls[$i],$vals[$j]) . "\n";
print "equals_new '$expl[$i]'\t'$expl[$j]'\t" . equals_new($va
+ls[$i],$vals[$j]) . "\n";
print "--\n";
}
}
sub equals_org {
my ($val1, $val2) = @_;
# first test for undef-ness
if ( ! defined $val1 || ! defined $val2 ) {
return 1 if (! defined $val1 && ! defined $val2 );
return undef; #case one is undef, other is def
}
# have to do next line this because strings evaluate to 0 in == co
+ntext ...
if ( ! $val1 && ! $val2 ) {
# then test for ""-ness
return 1 if ( ($val1 eq "") && ($val2 eq "") );
return undef if ( $val1 eq "" || $val1 eq "" );
#case one is "" other is 0
# then test for 0-ness
return 1 if ( ($val1 == 0) && ($val2 == 0) );
}
# then test for equality using 'eq'
return 1 if ( $val1 eq $val2 );
# then test for equality using '=='
if ( int($val1) && int($val2) ) {
# all other number cases are already covered by
# above tests (eg. both 0, or both the same string)
# this merely takes care of cases like:
# 1000.0000 == 1000
return 1 if ( $val1 == $val2 );
}
# couldn't find any, so i guess they are not equal
return undef;
}
sub equals_new {
my ($lhs, $rhs) = @_;
#False if mix of defined and undef
return 0 if defined $lhs != defined $rhs;
# True if both undef
return 1 if ! defined $lhs && ! defined $rhs;
# False if different as numbers
no warnings "numeric";
return 0 if (0 + $lhs) != (0 + $rhs);
# False if different as strings
return 0 if ('' . $lhs) ne ('' . $rhs);
return 1;
}
| [reply] [Watch: Dir/Any] [d/l] |
|
>0e0<, >0<: different
be a match?
| [reply] [Watch: Dir/Any] [d/l] |
|
| [reply] [Watch: Dir/Any] [d/l] |
|
| [reply] [Watch: Dir/Any] |
Re: check if 2 values are equal
by Corion (Patriarch) on Jan 24, 2006 at 18:52 UTC
|
I think the three rules you want for equality are:
Two items are regarded as equal if
- they are both undef
- they are string-equal
- they are numerically equal
By translating these three rules into code,
I come up with the following:
sub equals {
my ($val1,$val2) = @_;
return (!defined $val1 && !defined $val2)
|| ($val1 eq $val2)
|| ($val1 == $val2)
};
But in such cases it really pays off if you write a test suite that tests your current implementation, then write your routine from scratch again with the rules you have found, and then test the new routine against the old test cases. | [reply] [Watch: Dir/Any] [d/l] [select] |
|
The problem with this is that the numeric comparison could return false positives. Try doing a comparison of 'aa' == 'bb'. With -w on, this does issue warnings, however it will still return true.
That is why something like Fletcher's looks_like_number from Scalar::Util is needed.
I have not used it myself, so I can not attest to the accuracy of this function, however the code would be like this:
sub equals {
my ($val1,$val2) = @_;
return 1 if !defined $val1 && !defined $val2;
if (looks_like_number($val1) && looks_like_number($val2)) {
return $val1 == $val2;
} else {
return $val1 eq $val2;
}
}
| [reply] [Watch: Dir/Any] [d/l] [select] |
|
| [reply] [Watch: Dir/Any] |
|
I think you can do away with the third condition of ($val1 == $val2).
First, the eq should catch differences between numbers equally well as the ==. See Comparing values.
Secondly, the == would fail (under strict) if one of the values were a string.
For eq to work for numbers, you need to know that the values are being represented internally as numbers, for instance:
$a = 1000;
$b = 1e3;
print $a eq $b ? 1 : 0;
> 1
$b = "1e3";
print $a eq $b ? 1 : 0;
> 0
$b=$b+0;
print $a eq $b ? 1 : 0;
> 1
| [reply] [Watch: Dir/Any] [d/l] [select] |
|
$a = "01000";
$b = 1000;
printf "\$a eq \$b : %s\n", $a eq $b;
printf "\$a == \$b : %s\n", $a == $b;
use strict; has no bearing on comparisons. What you are thinking of is use warnings;, which will warn about non-numeric values being compared with ==. | [reply] [Watch: Dir/Any] [d/l] [select] |
|
|
| [reply] [Watch: Dir/Any] |
|
Your test function should return false if only one of the items is undef. You do not test for that.
| [reply] [Watch: Dir/Any] |
Re: check if 2 values are equal
by davido (Cardinal) on Jan 24, 2006 at 23:37 UTC
|
Perl already knows how to do all the work for you. You can just use a hash in almost the same way you would assure uniqueness in a list.:
sub isequal {
return 0 if ( grep{ !defined($_) } @_ ) != scalar @_;
no warnings qw/uninitialized/;
my %gadget;
@gadget{ @_ } = ();
return scalar keys %gadget == 1 ? 1 : 0;
}
This works by creating a hash whos keys are the items being compared. If after creating the keys you have one element, you've got equality. If you have more than one element, you have inequality. The only tricky part is to be sure to silence the warning you get for making a hash key out of 'undef', and to deal with the fact that undef stringifies to ''.
Here is a test example:
use strict;
use warnings;
use diagnostics;
sub isequal {
return 0 if ( grep { ! defined( $_ ) } @_ ) != scalar @_;
no warnings qw/uninitialized/;
my %gadget;
@gadget{ @_ } = ();
return scalar keys %gadget == 1 ? 1 : 0;
}
my @test = (
[ 0, 1 ],
[ "1", "one" ],
[ 0, "0" ],
[ 0, undef ],
[ 1, undef ],
[ "hello", undef ],
[ undef, undef ],
[ 1, 1 ],
[ "1", 1 ],
[ "hello", "hello" ]
);
foreach my $items ( @test ) {
my( $left, $right ) = @{ $items };
my $comp = isequal( $left, $right );
foreach( $left, $right ) {
! defined $_ and $_ = "undef";
}
print "Testing $left, $right == ", $comp ? "equal\n" : "not equal\
+n";
}
Update: Another advantage to this method that may not be immediately apparent is that it can be used to test a list of any number of items.
Enjoy!
| [reply] [Watch: Dir/Any] [d/l] [select] |
|
thanks, I eventually used your solution, with a minor modification:
sub isequal {
return 0 if ( grep { ! defined( $_ ) } @_ ) == scalar @_;
no warnings qw/uninitialized/;
my %gadget;
@gadget{ @_ } = ();
return scalar keys %gadget == 1 ? 1 : 0;
}
(changed the != into a == in the first line in the function) | [reply] [Watch: Dir/Any] [d/l] |
Re: check if 2 values are equal
by Fletch (Bishop) on Jan 24, 2006 at 18:53 UTC
|
| [reply] [Watch: Dir/Any] [d/l] |
Re: check if 2 values are equal
by CountZero (Bishop) on Jan 24, 2006 at 21:32 UTC
|
I was thinking of something along the following lines:
use strict;
use Scalar::Util qw/looks_like_number/;
sub equal {
my ($first, $second) = @_;
if (
(! defined $first and ! defined $second) or
(looks_like_number($first) and looks_like_number($second) and
+$first == $second) or
(defined $first and defined $second and $first eq $second)
) {
return 1;
}
else {
return 0;
}
}
while (<DATA>) {
my ($first, $second, $result) = split /,/;
chomp $result;
if ($result == equal($first, $second)) {
print "OK: >$first< >$second< >$result<\n";
}
else {
print "NOT OK: >$first< >$second< expected >$result<; got >",e
+qual($first, $second),"<\n";
}
}
__DATA__
undef,undef,1
undef,,0
undef,0,0
,,1
100,100,1
100,99,0
100,0100,1
100,1E2,1
100,100.00,1
1000,1_000,1
1000,1_000.00,1
1000,1 000,1
1000,1 000.00,1
string,string,1
string,another_string,0
It seems to work except for the "funny" numbers written with underscores or spaces.
CountZero "If you have four groups working on a compiler, you'll get a 4-pass compiler." - Conway's Law
| [reply] [Watch: Dir/Any] [d/l] |
|
|