Hey guys, today we gonna practice with the "interesting" algorithm. It was my improvisation at work when I once again tried to rewrite some logical conditions. Let's imagine that you have some statement with logical operators like: !(foo || (bar && !baz))
and you have a task to somehow rewrite it.
So, it would be nice to have tests for this case, but you might haven't some. Your brain starts thinking: "how to automate it" and "how to check all possible boolean values after changes". 🤔 Let's try to write the function to check the original condition variant with the new possible one. Our function can get original and new conditions in the form of functions in which we can pass boolean combinations to check the result on equality.
Rules for the code (as usual):
- Choose the right names for variables
- Choose the right loop statements: for, while, forEach, reduce etc.
- Avoid excess conditions and comparisons for edge cases
- Escape the side effects in your algorithms function, because very often you need to do mutations to decrease space or time complexity
const testBoolConditions = (sourceFn, targetFn) => {
/**
* n^r - Permutation with Repetition Formula
* n - is alphabet length equals 2 (for false and true)
* r - is number of arguments
*/
const combinationsCount = 2 ** sourceFn.length;
let counter = 0;
while (counter < combinationsCount) {
/**
* @example
* n - 2
* r - 3
* combinationsCount = 8
* in binary system we will args:
* 0 - [0,0,0]
* 1 - [0,0,1]
* 2 - [0,1,0]
* ...
* 7 - [1,1,1]
*
* where boolArgs will be:
* 0 - [false,false,false]
* 1 - [false,false,true]
* ...
* 7 - [true,true,true]
*/
const args = counter.toString(2).padStart(sourceFn.length, '0').split('');
const boolArgs = args.map(arg => Boolean(Number(arg)));
const sourceFnResult = sourceFn(...boolArgs);
const targetFnResult = targetFn(...boolArgs);
if (sourceFnResult !== targetFnResult) {
throw new Error(
`Failed condition for [${boolArgs}] with results sourceFn: ${sourceFnResult}, targetFn: ${targetFnResult}`,
);
}
counter++;
}
return 'functions have the same conditions';
};
For the sourceFn
and targetFn
we are generating all possible variants of arguments via the "Permutation with Repetition Formula" - n^r
(n
to the r
power).
n
- is alphabet length (in our case, it is 2 for false
and true
)
r
- is number of arguments (it depends on how many arguments in our condition)
Let's look at how to use it:
Convert our condition !(foo || (bar && !baz))
to function:
(foo, bar, baz) => {
return !(foo || (bar && !baz));
}
Rewrite the original condition somehow, for instance like this foo || (bar && !baz) ? false : true;
Convert this to a function:
(foo, bar, baz) => {
return foo || (bar && !baz) ? false : true;
}
And now we are ready to use our testBoolConditions
console.assert(
testBoolConditions(
(foo, bar, baz) => {
return !(foo || (bar && !baz));
},
(foo, bar, baz) => {
return foo || (bar && !baz) ? false : true;
},
),
);
That's it, if we get different results for sourceFn
and targetFn
for some argument combinations it will throw an error. It might be useful for rewriting some complex conditions.
Let me know your thoughts in the comment section below! 😊