17
\$\begingroup\$

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

\$\endgroup\$
5
  • \$\begingroup\$ Welcome to Code Review. Couldn't you have your function use logical OR and just return input1 || input2 || input3 ? \$\endgroup\$
    – Phrancis
    Commented Jul 19, 2018 at 19:40
  • 4
    \$\begingroup\$ @Phrancis That will return 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\$ Commented Jul 19, 2018 at 19:42
  • \$\begingroup\$ @Phrancis that is the route I originally started to take, however I couldn't get it to work with the stipulation that "onlyOne should return true only if exactly one of the three arguments are truthy. Otherwise, it should return false". Thank you for your comment, though! \$\endgroup\$ Commented Jul 19, 2018 at 19:44
  • \$\begingroup\$ What Javascript version are you using? \$\endgroup\$ Commented Jul 19, 2018 at 21:04
  • \$\begingroup\$ @FreezePhoenix well, I'm not sure on all counts. Im writing and testing the code in Visual Studio Code version 1.25, I'm not sure which version of JavaScript it contains. However I check most problems in Chrome as well, which I do know uses version 1.7 \$\endgroup\$ Commented Jul 19, 2018 at 23:05

9 Answers 9

33
\$\begingroup\$

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

\$\endgroup\$
6
  • 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
    Commented 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\$ Commented 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\$ Commented 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\$ Commented Jul 22, 2018 at 4:11
  • \$\begingroup\$ Other ways to check for equality are !(a !=b), a in [b], and Math.abs(a-b)<1 (if a and b are integers). \$\endgroup\$ Commented Jul 24, 2018 at 15:03
10
\$\begingroup\$

So, it can get a bit confusing, but it's doable with xoring 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.

enter image description here

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.

\$\endgroup\$
1
  • \$\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\$ Commented Jul 20, 2018 at 19:31
9
\$\begingroup\$

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;
}
\$\endgroup\$
4
  • 2
    \$\begingroup\$ I don't believe double negations !! are necessary since the result would be the same. \$\endgroup\$ Commented Jul 19, 2018 at 20:07
  • 2
    \$\begingroup\$ @SunnyPatel: You're right, fixed that. \$\endgroup\$
    – hoffmale
    Commented 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
    Commented 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\$ Commented Jul 22, 2018 at 7:17
7
\$\begingroup\$

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

\$\endgroup\$
2
  • \$\begingroup\$ That's both clever and readable, +1 \$\endgroup\$
    – JollyJoker
    Commented Jul 20, 2018 at 9:39
  • \$\begingroup\$ Oh, my. Just noticed you've already posted array solution.. Nice one (: \$\endgroup\$
    – sineemore
    Commented Jul 20, 2018 at 12:38
5
\$\begingroup\$

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

\$\endgroup\$
5
\$\begingroup\$

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.

\$\endgroup\$
5
\$\begingroup\$

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
\$\endgroup\$
3
\$\begingroup\$

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.

\$\endgroup\$
0
-1
\$\begingroup\$

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();
  });
 });
});
\$\endgroup\$
1
  • 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\$
    – Mast
    Commented Jul 21, 2018 at 11:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.