Lets move above JavaScript 101
To OP, I first must address the quality of some parts of some of the given answers that concern me.
I am continually shocked at the misunderstanding of JavaScript here in Code Review, Sorry if I rub some the wrong way but please, we are professionals and our answers should reflect that fact.
Issues
The naming critique is nit picking and not worthy of a mention as the abstract is not in the realm of natural language.
The critique of the nested ternaries is a worry, particularly in this problem as it deals with JS strings and alternatives ignore the need to avoid string building concatenation overhead.
I would expect a first year JS101 student to be thrown by nested ternaries, but not professionals, the ongoing emphasize in this forum on avoiding them makes it look like an undergrad hangout.
No one noticed the precision problem!!!!
The review
I found your function to be of good quality, naming is kept simple and in line with what one would expect from a mathematician. Due to the nature of the problem I would not expect someone without that type of knowledge to modify or get near the function.
There are some simple style problems and can be DRY er
The only major issue is that there is no (implied or direct) accounting for the the precision issue,
Number precision
I am surprised that your function passes all the tests because the upper limit of the polynomial order is well above the floating point (Double) precision of JavaScript Numbers
Lets start with the most inefficient section of your code. The function you call fac
that generates the factorial sequence.
The size of value grows very quickly with f(n) = f(70) = 1.197857166996989e100
that is close enough to a google to be called a 1.2 google
If we consider that Number.MAX_SAFE_INTEGER = 9.007199254740991e15
is 85 orders of magnitude below the 71st factorial any integer calculations around this range are going to produce rounding errors.
Ambiguous results due to precedence
This becomes even more pronounced as integer math seriously breaks down when we pass the precision limit.
For example lets strip out some of your code and look at the second line in the for
loop. Substituting some constants to demonstrate the problem
const fac = n => n < 2 ? 1 : n * fac(n - 1);
const a = 3, b = 13, n = 23, i = 11;
const k = n - i;
const c = !b && k > 0 ? 0 : a**i * b**k * (k === 0 ? 1 : fac(n) / (fac(k) * fac(n - k)));
In this special case the last line assigning to c
has some redundancy that one would naturally reduce to the following line
const c1 = a**i * b**k * fac(n) / (fac(k) * fac(n - k));
We have not changed the equation, we have simply removed a brackets that separated the ternary with clause (k === 0 ? ...)
(BTW could have been (!k ? ...)
.
However due to the way the rounding error is propagated the two lines c
, c1
will return two different values
console.log(c) // 5.580277237278821e+24
console.log(c1) // 5.580277237278822e+24
console.log(c === c1) // false
The different is minor and inconsequential in the order we are using, ~1 billion apart, but as the result is a string and as there are many ways to correctly arrange the precedence of the calculations there are many results that are about correct yet would not pass a string comparison.
Why did your function pass?
For high order polynomials getting the correct answer, is either,
- Just pure luck,
- The test is inclusive of all rounding errors (seriously doubt that)
- The test expects that you use
BigInt
in the calculations to avoid loss of precision (maybe but as you have passed all tests, my guess is that this is not so)
- You are never passed a polynomial above the precision range of a double that can not be optimized. eg
"(x+0)^204"
does not suffer precision problem as all but the first coefficients are 0 and the first is 1 with the result "x^204"
Rewrite
If we consider that option 4 is the reality of the problem then we have a handy optimization available by using a lookup for the factorial sequence rather than calculating it of each coff. Limiting it to Number.MAX_SAFE_INTEGER
we can cover that range up to fac(24)
PLEASE NOTE that this is the upper limit and that as the order approaches 24 the chance of precision error increases depending on the size of the values of a and b. The order 24 represents the max order that may return reliable results, not the max order that will. eg (12345235423875623537345x+1)^1
will likely fail.
Rewriting your function with some changes to address style and performance
- Use
Number
rather than parseInt
- Use constants for values that do not change
- Use the name
p
(power) or o
(order) to replace n
. I opt for 'p' as o
is too near 0
for me (bad eyesight)
- Replace
x
with name
- Drop the unneeded assignment to
_
underscore in the destructure declaration. eg const [ _, a, b] = [1,2,3]
can also be written as const [ , a, b] = [1,2,3]
you do not need to define unwanted items
- Rearrange the declarations a little. Assign the
String.match
to an array and then destructure to constants rather than variables.
- Avoiding the string building overhead of concatenating strings by using an array, to hold the coefficients (as strings) that is joined on the return to avoid overhead due to the need to iterate and reassign each time you concat a string variable.
- To further avoid the string building overhead we need to create a coefficient string in one expression and push that to an array of strings. That means we have no choise but to use NESTED ternaries
- Use the less noisy
while
loop and move the top and bottom coefficient out of the loop and be handled as special cases.
- Add tests for early exits possible when
a = 0
, p = 1
(p
formaly n
), eg "(x+0)^203"
will return "x^203"
without iterating 203 pointless coefficient calculations
- And some minor repeated calculations (and now a lookup) as stored constants rather than calculated. eg
k - n === i
so fac(k - n)
can be fac(i)
and fac(n) is the same each iteration so assign that to a constant facN
before the loop.
- Add the function
signed
that returns the partial formatted coefficient string, with arguments to deal with leading sign.
- Add function
finalCoff
to handle the two cases when we tack on the last coff
Output change
One change to the output as it makes no sense as it stands. When simplifying, eg "0x^2 + 2"
becomes "2"
however your function returns an empty string for "0x^2 + 0"
becomes ""
. This is not what one would expect, at minimum a number is expected, thus the function will return "0" rather than ""
As you are running this in a test environment you will likely not be ablue to close over the array facSeq
so I have placed it inside the function.
function expandA(str) {
const facSeq = [0,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600,6227020800,87178291200,1307674368000,20922789888000,355687428096000,6402373705728000,121645100408832000,2432902008176640000,51090942171709440000,1.1240007277776077e21,2.585201673888498e22,6.204484017332394e23];
const finalCoff = n => n < 0 ? n : (n ? "+" + n : "");
const signed = (n, pow, plus = "+") => (
n < 0 ?
(pow ? (n === -1 ? "-" : n) : (!pow ? n : "-")) :
(n ? plus + (n > 1 ? n : "") : "")
) +
(pow > 1 ? name + "^" + pow : (pow === 1 ? name : ""));
const mt = str.match(/\((-?\d*)([a-z])([-+]\d+)\)\^(\d+)/)
const [ , a, name, b, p] = [,
mt[1] ? mt[1] == '-' ? -1 : Number(mt[1]) : 1,
mt[2], Number(mt[3]), Number(mt[4])];
if (a === 0) { return "" + b ** p }
if (p === 1) { return (a ? signed(a, p, "") : "") + finalCoff(b) }
const facN = facSeq[p];
var i = p, coffs = [signed(a ** p, p, "")];
while (i-- > 1) {
const pos = p - i, cof = a ** i * b ** pos * facN / (facSeq[pos] * facSeq[i]);
cof && coffs.push(signed(cof, i));
}
return coffs.join("") + finalCoff(b ** p);
}
If you find that this does not work, because the polynomials you need all coffs for are in orders greater than 24. You can reinstate the fac
function, modified to lookup first and calculate if needed. NOTE still returns "0" rather than ""
function expandA(str) {
const facSeq = [0,1,2,6,24,120,720,5040,40320,362880,3628800,39916800,479001600,6227020800,87178291200,1307674368000,20922789888000,355687428096000,6402373705728000,121645100408832000,2432902008176640000,51090942171709440000,1.1240007277776077e21,2.585201673888498e22,6.204484017332394e23];
const fac = n => facSeq[n] ? facSeq[n] : (n < 2 ? 1 : n * fac(n - 1));
const finalCoff = n => n < 0 ? n : (n ? "+" + n : "");
const signed = (n, pow, plus = "+") => (
n < 0 ?
(pow ? (n === -1 ? "-" : n) : (!pow ? n : "-")) :
(n ? plus + (n > 1 ? n : "") : "")
) +
(pow > 1 ? name + "^" + pow : (pow === 1 ? name : ""));
const mt = str.match(/\((-?\d*)([a-z])([-+]\d+)\)\^(\d+)/)
const [ , a, name, b, p] = [,
mt[1] ? mt[1] == '-' ? -1 : Number(mt[1]) : 1,
mt[2], Number(mt[3]), Number(mt[4])];
if (a === 0) { return "" + b ** p }
if (p === 1) { return (a ? signed(a, p, "") : "") + finalCoff(b) }
const facN = fac(p);
var i = p, coffs = [signed(a ** p, p, "")];
while (i-- > 1) {
const pos = p - i, cof = a ** i * b ** pos * facN / (fac(pos) * fac(i));
cof && coffs.push(signed(cof, i));
}
return coffs.join("") + finalCoff(b ** p);
}
Performance
using the second version function and testing for a even distribution of (nx+n)^m
where m is 1 <= m <= 300
and n is -1000 < n < 1000
there is a marginal performance benefit of 10%
If we avoid rounding errors by only passing zero for the first term "nx"
n = 0 when m > 20
and only making 5% of calls in the range m > 20
the performance increases is near 40%
The first version will not handle a full range but has an even better performance and may well pass the test suit you use. (excluding the "0")
Final
I expect this will get some down votes, and please do if you feel it warranted.
If you do please do provide a comment regarding the reasoning for the benefit of the OP and others.
I am not going to enter into debate and reply to comments of such nature unless specifically asked a question (collection of words ending with ?)