Beefy Boxes and Bandwidth Generously Provided by pair Networks
laziness, impatience, and hubris
 
PerlMonks  

One out of three ain't bad

by saintmike (Vicar)
on Oct 22, 2005 at 05:19 UTC ( #502181=perlquestion: print w/ replies, xml ) Need Help??
saintmike has asked for the wisdom of the Perl Monks concerning the following question:

What's the best way of testing if exactly one out of three variables are set?

There's three variables $x, $y, and $z. If there's exactly one true value, as in

$x = 0; $y = 1; $z = 0;
the test should succeed. But if there's more (or less) than one true value, as in
$x = 5; $y = undef; $z = 5;
then I'd like the test to fail.

Here's a clunky solution:

my $count = 0; $x && $count++; $y && $count++; $z && $count++; if($count == 1) { print "Exactly one variable set\n"; }
Here's a more elegant one:
if((grep { $_ } $x, $y, $z) == 1) { print "Exactly one variable set\n"; }
But none of them is really intuitive. Is there a better way?

Comment on One out of three ain't bad
Select or Download Code
Re: One out of three ain't bad (order)
by tye (Cardinal) on Oct 22, 2005 at 05:49 UTC

    Avoid the obfuscating punctuation:

    if( 1 == grep $_, $x, $y, $z )

    Or, using a more versatile trick:

    if( 1 == !!$x + !!$y + !!$z )

    But note that some will kvetch that the fact of !0 == 1 is "not defined" in Perl. But that ship has already sailed; practicality actually prevents Perl from changing the value of "true" to something other than 1, even if this property was intentionally left undefined.

    - tye        

      I like this !! based solution the best. It's the most logical way to solve the problem - convert the arguments to boolean truth values, then sum them. You could do it for an arbitrarily long list of variables with:
      use List::Util qw( sum ); if( 1 == sum( map { !!$_ } @inputs) ) { ... }

        As long as you're going to go through the same code path for each element in the array anyway, why not de-obfuscate that a bit and use the ternary operator?

        if (1 == sum( map { $_ ? 1 : 0 } @inputs ))
        While tye may be right that, in practice, Perl can't change the value of !0, I just don't like relying on obscure implementation details when clear, well-defined implementation details are available to me to accomplish the same thing in a readable, understandable, and maintainable manner.

      Or, shorter:

      if (2 == !$x + !$y + !$z) {

        Which seems to work right now, since you've seen the progression of what came before. But in 2 months, I can imagine you thinking, "Now why in the world is there a two in there... I must want two of them to be true" on your first look while skimming your code.

            -Bryan

Re: One out of three ain't bad
by davido (Archbishop) on Oct 22, 2005 at 05:52 UTC

    I'm not convinced that you need an elegant solution to finding if one and only one of three elements evaluates to truth. But it's kind of a fun problem anyway, and if the number of elements grows, a sleek solution becomes more relevant. So here goes...

    Try cpan's List::MoreUtils:

    use strict; use warnings; use List::MoreUtils qw/ true /; my @lists = ( [ 1, 0, 1 ], [ 1, 0, 0 ], [ 1, 1, 1 ], [ 0, 0, 0 ] ); foreach my $aref ( @lists ) { print "Testing @{$aref}\n"; print "Total list Exclusive OR ", ( 1 == true { $_ } @{ $aref } ) ? '' : 'not ', "satisfied.\n\n"; }

    Here we're just using the true() function from List::MoreUtils. The function returns the number of true elements. Like one of your proposed solutions, we simply check to ensure that the number of true elements is exactly one. This one works almost identically to your grep solution, except that it uses an explicit function name (true()) instead of a generic one such as grep.

    My example snippet actually tests a list of lists to see if each sub-list individually satisfies your requirement of a single element of truth. The engine is this:

    1 == true { $_ } @array

    If that test evaluates to truth, you've got a list with a single element of truth.


    Dave

Re: One out of three ain't bad
by ikegami (Pope) on Oct 22, 2005 at 05:53 UTC
    You could hide the guts in a function to make it more readable:
    sub single_true { my $count = 0; $_ && $count++ foreach @_; return $count == 1; } if (single_true($x, $y, $z)) { ... }
Re: One out of three ain't bad
by strat (Canon) on Oct 22, 2005 at 05:53 UTC

    well, you can write it a little bit shorter...

    my $count = 0; $_ and $count++ for ($x,$y,$z); if (1 == $count) { print "Exactly one variable set\n"; }
    or or even pack the $count and ++ into a do:
    if (1 == do {my $cnt=0; $_ and $cnt++ for ($x,$y,$z); $cnt } { print "Exactly one variable set\n"; }

    But I doubt that this is more readable

    update: I'd prefer a grep solution as posted by tye

    if (1 == grep {$_} ($x,$y,$z)) { print "Exactly one variable set\n"; }

    Best regards,
    perl -e "s>>*F>e=>y)\*martinF)stronat)=>print,print v8.8.8.32.11.32"

Re: One out of three ain't bad
by EvanCarroll (Chaplain) on Oct 22, 2005 at 06:12 UTC

    I'm not sure what solution to offer. Don't shoot for concise, short for maintainablity. Short circuiting should be avoided in perl, it has better faculties.

    ## Instead of...
    $x && $count++;
    ## Consider...
    $count++ if $x;

    I would probably go with the slightly modified grep suggested in the post above. But see 'perldoc -q contain in'


    Evan Carroll
    www.EvanCarroll.com

      Short circuiting should be avoided in perl, it has better faculties.

      Tell that to the POD. The docs for open, for example, give at least a dozen examples of constructs similar to this:

      open FH, '>', 'filename' or die $!;

      ...and who could argue that use of short circuiting is inferior to this:

      die $! unless open FH, '>', 'filename';

      This is further discussed in the following quote from perlstyle:

      Here are some other more substantive style issues to think about:

      • Just because you CAN do something a particular way doesn't mean that you SHOULD do it that way. Perl is designed to give you several ways to do anything, so consider picking the most readable one. For instance

        open(FOO,$foo) || die "Can't open $foo: $!";

        is better than

        die "Can't open $foo: $!" unless open(FOO,$foo);

        because the second way hides the main point of the statement in a modifier.

      The point here is that you cannot make a sweeping statement such as "short circuiting should be avoided in Perl". Avoided why? There are several equally efficient ways to do things. The docs are right: "consider picking the most readable one." You might be right that short circuiting hides the intent in this case, but certanly it's going too far to say it should be avoided altogether.


      Dave

Re: One out of three ain't bad
by gloryhack (Deacon) on Oct 22, 2005 at 07:26 UTC
    "Intuitive" is subjective... to those who grok perl, your "more elegant" solution should make perfect sense, even if it's not the ultimate reality solution. If it satisfies all test cases (3! of them) then it works... if it's also readable, then let it be. Don't sweat the irrelevant.
Re: One out of three ain't bad
by JamesNC (Chaplain) on Oct 22, 2005 at 14:38 UTC
    Here is a simple example using the shift operator.
    my $x = -2; my $y = 0; my $z = 1; my $t = 1; for( $x,$y,$z){ $t = $t<<1 if $_; } print "none set" if $t == 1; print "only one set" if $t == 2; print "more than 1 set" if $t > 2;

    JamesNC
Reaped: Re: One out of three ain't bad
by NodeReaper (Curate) on Oct 22, 2005 at 14:42 UTC
Re: One out of three ain't bad
by Util (Priest) on Oct 22, 2005 at 19:55 UTC

    Your grep solution would be intuitive to many monks, but it becomes more maintainable when paired with well-named variables or subroutines. My (current, and PBP-influenced) preferences:

    • If you check only once in your program, then be descriptive with a variable.
      my $number_of_true_variables = grep { $_ } $x, $y, $z; if ( $number_of_true_variables == 1 ) { print "Exactly one variable set\n"; }
    • If you check more than once, sometimes comparing the count to values other than one, then use a sub named for the count it returns.
      sub number_of_true_variables { return scalar grep { $_ } @_; } if ( number_of_true_variables( $x, $y, $z ) == 1 ) { print "Exactly one variable set\n"; }
    • If you check more than once, always comparing to one, then use a sub named for that comparison.
      sub exactly_one_is_set { my $number_of_true_variables = grep { $_ } @_; return ( $number_of_true_variables == 1 ); } if ( exactly_one_is_set( $x, $y, $z ) ) { print "Exactly one variable set\n"; }

Re: One out of three ain't bad
by ambrus (Abbot) on Oct 22, 2005 at 20:10 UTC

    Whenever you feel there's no intuitive way to do it, write a subroutine.

    In this case, let's define, say,

    sub exactly_one { 1 == grep $_, @_; }
    Then you can do exactly_one($x, $y, $z) which is now cleaner than any other solution.
Re: One out of three ain't bad
by snowhare (Friar) on Oct 23, 2005 at 03:54 UTC
    Ok. The last one is best.
    my $only_one = 1 == ($x ? 1 : 0) + ($y ? 1 : 0) + ($z ? 1 :0); + my $only_one = 2 == (! $x) + (! $y) + (! $z); my $only_one = ($x || $y || $z) && (! ($x && $y)) && (! ($y && $z)) && + (! ($z && $x)); my $only_one = (! ($x && $y && $z)) && ($x ^ $y ^ $z);
      You probably want logical exclusive or (xor) not bitwise exclusive or (^).

        Good catch. My test framework had a couple of bugs that let me miss that. I've corrected the framework errors, corrected my logical vs bitwise xor error and added the two contributions by anon monks to the tests. It changed the rankings - snowhare4 moved from about 9th to 2nd behind anonmonk1 after fixing. However, anonmonk1 got 3 test case failures, so snowhare4 is now the fastest of the correct solutions. It's nice that the solution that is to me the most elegant is also the fastest correct solution.


        anonmonk1 : 3.26 secs 100% (3 errors) snowhare4 : 4.50 secs 138% (0 errors) snowhare6 : 4.66 secs 143% (0 errors) snowhare2 : 4.99 secs 153% (0 errors) tye2 : 5.13 secs 157% (0 errors) snowhare5 : 5.17 secs 159% (0 errors) snowhare3 : 5.17 secs 159% (0 errors) snowhare1 : 5.30 secs 163% (0 errors) saintmike1 : 6.36 secs 195% (0 errors) tye1 : 7.21 secs 221% (0 errors) saintmike2 : 7.54 secs 231% (0 errors) knom1 : 13.41 secs 411% (0 errors) strat1 : 13.41 secs 411% (0 errors) ikegami1 : 13.50 secs 414% (0 errors) jamesnc1 : 14.86 secs 456% (0 errors) strat2 : 15.05 secs 462% (0 errors) tanktalus1 : 15.49 secs 475% (0 errors) ph713_1 : 20.21 secs 620% (0 errors) davido1 : 21.96 secs 674% (0 errors)
      Just a comment on forms for this kind of problem.

      The first two forms explicitly list each variable only once:

      my $only_one = 1 == ($x ? 1 : 0) + ($y ? 1 : 0) + ($z ? 1 :0); + my $only_one = 2 == (! $x) + (! $y) + (! $z);
      The second two forms require each variable to be listed twice:
      my $only_one = ($x || $y || $z) && (! ($x && $y)) && (! ($y && $z)) && + (! ($z && $x)); my $only_one = (! ($x && $y && $z)) && ($x ^ $y ^ $z);
      Just based on this criterion, I'd avoid the 2nd pair of solutions. (Looking at other replies, those with sub, map, grep, or List:Utils will take a list -- easier to maintain.)

      -QM
      --
      Quantum Mechanics: The dreams stuff is made of

        I'll disagree here an say a form like...
        if( !$x && !$y && $z or !$x && $y && !$z or $x && !$y && !$z ) { print "Exactly one..."; }
        ...is easier to maintain because it is more explicit as to what is required for a truth value. Anyone versed in boolean algebra will easily recognize the sum-of-product form here and instantly grasp the intent.
Re: One out of three ain't bad (benchmarks)
by snowhare (Friar) on Oct 23, 2005 at 15:37 UTC

    In the interest of "if you are going to do it, overdo it", I took the various solutions presented (along with two additional variants; snowhare5 and snowhare6 I thought of) and ran precision benchmarks on them to see how they stack up performance wise in addition to 'elegance wise'. And to verify that they all actually produce correct results. The benchmarks were run with 500,000 loops to get good precision and I 'tare' compensated the scripts with a 'null script' that factored out the testing framework overhead time.

    The good news is all the solutions produce correct results. (Yay everyone!).

    The bad news is that they vary by about 400% performance wise from the fastest to the slowest.

    The ranked results were as follows:

    snowhare6 : 3.76 secs 100% (0 errors) snowhare2 : 3.90 secs 104% (0 errors) tye2 : 4.06 secs 108% (0 errors) snowhare3 : 4.09 secs 109% (0 errors) snowhare5 : 4.13 secs 110% (0 errors) snowhare1 : 4.73 secs 126% (0 errors) saintmike1 : 5.51 secs 147% (0 errors) tye1 : 5.62 secs 149% (0 errors) snowhare4 : 5.69 secs 151% (0 errors) saintmike2 : 6.37 secs 169% (0 errors) strat1 : 10.60 secs 282% (0 errors) ikegami1 : 11.14 secs 296% (0 errors) tanktalus1 : 11.68 secs 311% (0 errors) strat2 : 11.81 secs 314% (0 errors) jamesnc1 : 12.07 secs 321% (0 errors) ph713_1 : 15.60 secs 415% (0 errors) davido1 : 16.18 secs 430% (0 errors)

    snowhare5 and snowhare6 were algorithmically the same as the previously presented snowhare1 and snowhare2 respectively, except I added 'use integer;' to them.

    Among the 'trivially generalized to handle N values' scripts, tye1 is the fastest (all it would take is replacing the explict variable callouts with @_).

Re: One out of three ain't bad
by Anonymous Monk on Oct 23, 2005 at 19:00 UTC
    if($x && !$y && !$z or !$x && ($y xor $z)) { print "Exactly one variable set\n"; }
Re: One out of three ain't bad
by Anonymous Monk on Oct 24, 2005 at 09:21 UTC
    undef $count; foreach(($x, $y, $z)) { $count++ if($_); } print "Exactly one variable set\n" if($count == 1); --Knom

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://502181]
Approved by planetscape
Front-paged by monkfan
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others examining the Monastery: (8)
As of 2014-08-01 11:49 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    Who would be the most fun to work for?















    Results (10 votes), past polls