No such thing as a small change PerlMonks

### One out of three ain't bad

by saintmike (Vicar)
 on Oct 22, 2005 at 05:19 UTC 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?

Replies are listed 'Best First'.
Re: One out of three ain't bad (order)
by tye (Sage) 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 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 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 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
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 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.

\$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 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);

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.
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)
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

Create A New User
Node Status?
node history
Node Type: perlquestion [id://502181]
Approved by planetscape
Front-paged by monkfan
help
Chatterbox?
 [LanX]: oh 9 downvotes ... Nick must be back :) [Discipulus]: marto apotropaic gestures in Eataly.. [Discipulus]: LanX nick does not downvote peoples, just content he dislike (I think) [Discipulus]: anyway I have my Tk toys running at 50%!! someone want to see it?

How do I use this? | Other CB clients
Other Users?
Others avoiding work at the Monastery: (8)
As of 2018-04-26 10:21 GMT
Sections?
Information?
Find Nodes?
Leftovers?
Voting Booth?
My travels bear the most uncanny semblance to ...

Results (95 votes). Check out past polls.

Notices?