Given the following instructions I have created a function that achieves the requirements. I feel my function is a bit too complex, though it does what it's supposed to. The difficult part for myself was avoiding the equality operators. The only way I could think to solve this is with a bit of math and a comparison operator. How can I simplify this function and save some editing time?
Write a function
onlyOne
that accepts three arguments of any type.
onlyOne
should return true only if exactly one of the three arguments are truthy. Otherwise, it should return false.Do not use the equality operators (
==
and===
) in your solution.
My function:
function onlyOne(input1, input2, input3){
output = false;
let truthy = 0;
let falsey = 0;
if (!!input1) {
truthy++;
} else falsey++;
if (!!input2) {
truthy++;
} else falsey++;
if (!!input3) {
truthy++;
} else falsey++;
if (falsey > truthy) {
output = true;
}
if (falsey > 2) {
output = false;
}
return output;
}
(originally asked on StackOverflow and was directed here.)
9 Answers 9
You don't need to count the falsy arguments. The challenge only asks you to count the truthy ones. Since true
is treated as 1
by the +
operator, you can use that to perform the count.
As for the prohibition on using equality operators, you can easily work around that using two inequalities.
You forgot to declare output
, so that was treated as a global variable.
function onlyOne(a, b, c) {
let truthyCount = !!a + !!b + !!c;
// Workaround for the prohibited test truthyCount == 1
return 0 < truthyCount && truthyCount < 2;
}
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
Based on @Blindman67's suggestion, here's another solution, which involves fewer operations. How it works is less obvious from inspection, though. Basically, it tests whether the number of falsy parameters is 2.
function onlyOne(a, b, c) {
return !((!a + !b + !c) - 2);
}
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
-
2\$\begingroup\$ Would be simpler as
const onlyOne = (a, b, c) => !a + !b + !c - 2 ? false : true;
the!!a
requires two test, while the!a
one \$\endgroup\$Blindman67– Blindman672018年07月19日 22:02:05 +00:00Commented Jul 19, 2018 at 22:02 -
1\$\begingroup\$ @Blindman67 Thanks! I've incorporated a variant of your suggestion. More efficient, yes, but also less readable. \$\endgroup\$200_success– 200_success2018年07月19日 22:09:52 +00:00Commented Jul 19, 2018 at 22:09
-
\$\begingroup\$ This is great and super helpful. Thank you so much for taking the time to explain this! \$\endgroup\$CalamityAdam– CalamityAdam2018年07月19日 22:55:19 +00:00Commented Jul 19, 2018 at 22:55
-
\$\begingroup\$ Unless this were being called a gazillion times I would consider the readability penalty far more important than the efficiency gain.. \$\endgroup\$Loren Pechtel– Loren Pechtel2018年07月22日 04:11:26 +00:00Commented Jul 22, 2018 at 4:11
-
\$\begingroup\$ Other ways to check for equality are
!(a !=b)
,a in [b]
, andMath.abs(a-b)<1
(ifa
andb
are integers). \$\endgroup\$Acccumulation– Acccumulation2018年07月24日 15:03:37 +00:00Commented Jul 24, 2018 at 15:03
So, it can get a bit confusing, but it's doable with xor
ing your values:
const xor = (a, b) => !a ^ !b;
const onlyOne = (a, b, c) => xor(xor(xor(a, b), c), a && b && c)
Or alternatively, since xor
is associative:
const onlyOne = (a, b, c) => !!a ^ !!b ^ !!c ^ (a && b && c)
Can be optimized even further by removing some redundant computations:
const onlyOne = (a, b, c) => !a ^ !b ^ !c ^ !(a && b && c)
Doing a straight xor
on all 3 values leaves room for them all to be true, but then we just xor
that value in to get our results.
Also, the reason xor(a, b) != a ^ b
is to convert those values to boolean beforehand. It's nice that with booleans a ^ b === !a ^ !b
.
Edit: I didn't heed my own advice, with the last statement. Removed 2 !
(not) computations by removing the double negation to coerce into boolean.
-
\$\begingroup\$ I would like to add that this is extendable to odd number parameters only. Separate logic would need to be made for even number parameters. \$\endgroup\$Sunny Patel– Sunny Patel2018年07月20日 19:31:11 +00:00Commented Jul 20, 2018 at 19:31
I'd prefer one of these two versions for readability. They seem easy enough to reason about, and don't hide the logic for choosing a result behind some counter variables, whose state the reader would need to keep track of.
Just keep it simple.
function onlyOne(a, b, c){
if(a && !b && !c) return true;
if(!a && b && !c) return true;
if(!a && !b && c) return true;
return false;
}
function onlyOne(a, b, c){
if(a || b || c) {
if(!b && !c) return true;
if(!a && !c) return true;
if(!a && !b) return true;
}
return false;
}
-
2\$\begingroup\$ I don't believe double negations
!!
are necessary since the result would be the same. \$\endgroup\$Sunny Patel– Sunny Patel2018年07月19日 20:07:32 +00:00Commented Jul 19, 2018 at 20:07 -
2\$\begingroup\$ @SunnyPatel: You're right, fixed that. \$\endgroup\$hoffmale– hoffmale2018年07月19日 20:09:35 +00:00Commented Jul 19, 2018 at 20:09
-
1\$\begingroup\$ I'd say this is the best solution. There aren't too many options. Maybe avoid repetition or the word "input" by using a,b,c instead, like
return (a && !b && !c) || (!a && b && !c) || (!a && !b && c)
(with line breaks before the||
s) \$\endgroup\$JollyJoker– JollyJoker2018年07月20日 09:34:26 +00:00Commented Jul 20, 2018 at 9:34 -
\$\begingroup\$ I actually find this hard to read. I have to check all the
!
s and all the numeric suffixes are correct, make sure every combination is covered... the explicit counting solutions match the spec much more precisely. \$\endgroup\$Ben Millwood– Ben Millwood2018年07月22日 07:17:21 +00:00Commented Jul 22, 2018 at 7:17
You can use filter
on an array of arguments to filter out false
ones and then count the remaining true
arguments. Added the !(... - 1)
so it will return true only if length == 1
(because using ==
was prohibited).
const onlyOne = (a, b, c) => !([a, b, c].filter(Boolean).length - 1);
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
-
\$\begingroup\$ That's both clever and readable, +1 \$\endgroup\$JollyJoker– JollyJoker2018年07月20日 09:39:39 +00:00Commented Jul 20, 2018 at 9:39
-
\$\begingroup\$ Oh, my. Just noticed you've already posted
array
solution.. Nice one (: \$\endgroup\$sineemore– sineemore2018年07月20日 12:38:35 +00:00Commented Jul 20, 2018 at 12:38
This feels like a fun time to use the XOR operator. If you're not familiar with it, it's basically the "either or" operator. It returns true if-and-only-if exactly one of the two operands is true.
JavaScript does not have a logical xor operator but it does have a bitwise xor operator: ^
. It also has other bitwise operators that you can read more about on the MDN Reference page. If we coerce our truthy and falsy values into true
and false
, we can utilize the bitwise operators. We can perform this coercion by prepending a value with !!
(if we want to retain its truthiness) or !!
(if we are okay with negating the value).
I started with !a ^ !b ^ !!c
, which covers all but one case (all true) on its own. I wrapped it in parentheses and prepended !!
to ensure the resulting value was a boolean rather than 1 or 0. Note that I'm using 200_Success's tests in the following code:
const onlyOne = (a, b, c) => {
return !!(!a ^ !b ^ !!c);
}
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
As you can see, the only case this does not handle is when all three are true. This is because the xor of the first two will return false and then the xor of false with the third one's value will return true.
It is easy to handle this case specifically. (!a || !b || !c)
will be false only if all three values are true. This leads to my final solution, which follows:
const onlyOne = (a, b, c) => {
return !!((!a ^ !b ^ !!c) && (!a | !b | !c));
};
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
A slight rewrite of hoffmale's solution
const onlyOne = (a,b,c) => (a && !b && !c)
|| (!a && b && !c)
|| (!a && !b && c);
I think enumerating all the combinations is short enough to be the easiest-to-read way of doing this.
Wow, there are tons of nice solutions among the answers!
Note: it is not a good solution, but you might learn something new, so it worth posting it.
Here is some generic one, that uses new (at least to me) spread syntax. It is not that efficient but it works nicely with any number of arguments:
const onlyOne = (...a) => !(a.reduce(((a, b) => a + !!b), 0) - 1)
// Tests based on @200_success answer
console.log("These should be true:");
console.log(onlyOne(true, null, undefined));
console.log(onlyOne(false, '', '0'));
console.log(onlyOne(false, 0, '0'));
console.log(onlyOne(false, [], null));
console.log(onlyOne(1/0 /* Infinity */));
console.log("These should be false:");
console.log(onlyOne(1, 'a', Infinity));
console.log(onlyOne(false, null, undefined));
console.log(onlyOne(false, NaN, null));
console.log(onlyOne(0));
console.log(onlyOne());
const onlyOne = (...a) => !(a.reduce(((a, b) => a + !!b), 0) - 1)
The spread operator ...a
will put all provided arguments to array a
. We can use reduce method to count truthy entries:
a.reduce(((a, b) => a + !!b), 0)
The return value will be number in range [0,a.length)
: 0 when no truthy arguments and a.length
when all of them are.
But we only need the 1
. So the trick is to subtract 1
from reduce result and !
it to boolean:
!(1 - 1) === true
Two or Three max.
There are three input thus there need be only 3 tests max, any more and the are working on information already known.
The best is when you find two inputs that are true
, that means you can return false
without testing the last value.
The current given answers all need more than three tests in the worst cases and at best three tests.
The flowing will do three test worst case and 2 best
function onlyOne(a, b, c) {
if (a) {
if (b) { return false }
return c ? false : true;
}
if (b) { return c ? false : true }
return c ? true : false;
}
Or as
function onlyOne(a, b, c) {
if (a) { return b ? false : c ? false : true }
return b ? (c ? false : true) : (c ? true : false);
}
There is also a time invariant solution that always has the same performance, though its best and worst is 3 tests. Both use lookup tables
const T = true, F = false, look = [F,T,T,F,T,F,F,F];
const onlyOne = (a, b, c) => look[(a ? 1 : 0) + (b ? 2 : 0) + (c ? 4 : 0)];
Or
const T = true, F = false, look = [F,F,T,F];
const onlyOne = (a, b, c) => look[!a + !b + !c];
Update
I would like to clarify some common misunderstanding about Javascript. Dogma and the fanaticism that enforces it has no place in the coding community. Hence the update.
(Ternaries have multiple paths) Wrong.
There are two types of code in Javascript Expressions and Statement
- Expression evaluate to a value. eg
"a"
,0
,a = b
,(a,b)
,foo()
- Statements do not have a value. eg
if
,else if
,while
,do ... while
,for
,switch
,return
Ternaries are expressions, they are not statements. Ternaries are expression selectors and can be thought of as lookup index rather than a conditional branch.
The following is another way of writing a ternary like expression in javascript.
var c = true;
var res = ([100, 200])[+c]; // res is assign 200
c = false;
res = ([100, 200])[+c]; // res is assign 100
Because ternaries do not split code flow you can not use ternaries to introduce statement that cause code flow to branch.
// Code flow is not split thus the next to can not work
a ? a++ : return; // Error unexpected token
a ? continue : break; // Error unexpected token
// need to diverge code flow to do the following
if (a) a++; else return;
if (a) continue; else break;
(Branching means low javascript performance) Wrong.
Modern CPUs use look ahead instruction pipelines that fetch instruction from memory while at the same time performing operations. They do this to improve performance. However conditional branching can mean the wrong path is fetched. When this happens the pipeline needs to rollback and fetch the correct path, meanwhile the CPU must wait until at least the first instruction has loaded. This results in downgraded performance.
This problem can be reduced using various method such as branch prediction. The CPU uses past results to guess which way to go next time.
Many JS codes believe that diverging code flow is poor because it may trigger the CPU instruction pipeline to rollback. That it is better to use syntax that avoids diverging code flow so to avoid this penalty.
This is not true, the only way to avoid CPU instruction branching in JavaScript is by not using operators in expressions (which is pointless) or by using statements that don't require a type check eg return foo
;
Javascript is loosely typed, which means that every expression that contains a operator must have its type checked for the operator to be applied correctly. There is no way to avoid it. Even optimized code needs to have a test to make sure a type has not changed
You can not avoid CPU instruction pipeline rollback when writing Javascript expressions
Semantic and syntactic meaning
All code represents meaningful concepts, an abstraction of the zeros and ones that lay underneath, these abstractions are split into two types.
- Syntactic meaning
A language is comprised of building blocks and a set of rules that define how these blocks relate to each other. These blocks and rules are the syntax of a language. They are non contextual and have a fixed fixed meaning.
- Semantic meaning (abstraction)
Using the blocks (tokens) and rules of syntax we can create expressions and statements. This grouping along with the context have a semantic meaning. Semantic meaning is not fixed, and the same code have entirely different semantic meaning depending on the context.
Semantic meaning is a fundamental part of writing code, its how we abstract code into high and more meaningful representation of all those bits.
This is similar to the the Array.filter
solution already posted, but can expand to any number of arguments which I thought made it unique enough to add.
function onlyOne(/*a, b, b -- works for any # of args*/) {
return !(Array.prototype.filter.call(arguments, i => i).length-1);
}
// test
const T = [true, 1, false, null, '', undefined];
T.forEach(t1 => {
T.forEach(t2 => {
T.forEach(t3 => {
console.log([t1, t2, t3]);
console.log(onlyOne(t1, t2, t3));
console.log();
});
});
});
-
1\$\begingroup\$ You have presented an alternative solution, but haven't reviewed the code. Please explain your reasoning (how your solution works and why it is better than the original) so that the author and other readers can learn from your thought process. \$\endgroup\$2018年07月21日 11:36:15 +00:00Commented Jul 21, 2018 at 11:36
return input1 || input2 || input3
? \$\endgroup\$true
if more than one of the arguments are true. onlyOne should return true only if exactly one of the three arguments are truthy. Otherwise, it should return false. \$\endgroup\$