nerdamer-prime
Version:
javascript light-weight symbolic math library
1,483 lines (1,440 loc) • 270 kB
JavaScript
/*
* Author : Martin Donk
* Website : http://www.nerdamer.com
* Email : martin.r.donk@gmail.com
* License : MIT
* Source : https://github.com/jiggzson/nerdamer
*/
// Type imports for JSDoc ======================================================
// These typedefs provide type aliases for the interfaces defined in index.d.ts.
// They enable proper type checking when working with the classes defined in this file.
//
// Usage patterns:
// - For return types: @returns {NerdamerSymbolType}
// - For parameters: @param {NerdamerSymbolType} symbol
// - For variable declarations: /** @type {NerdamerSymbolType} */
//
// Note: When casting local class instances to interface types, use the pattern:
// /** @type {InterfaceType} */ (/** @type {unknown} */ (localInstance))
// This is needed because TypeScript sees local classes and interfaces as separate types.
/**
* Core type aliases from index.d.ts
*
* @typedef {import('./index').NerdamerCore.NerdamerSymbol} NerdamerSymbolType
*
* @typedef {import('./index').NerdamerCore.Frac} FracType
*
* @typedef {import('./index').NerdamerCore.Vector} VectorType
*
* @typedef {import('./index').NerdamerCore.Matrix} MatrixType
*
* @typedef {NerdamerSymbolType | VectorType | MatrixType} ParseResultType Union type for parse results
*
* @typedef {import('./index').NerdamerCore.Parser} ParserType
*
* @typedef {import('./index').NerdamerCore.Collection} CollectionType
*
* @typedef {import('./index').NerdamerCore.Settings} SettingsType
*
* @typedef {import('./index').NerdamerExpression} ExpressionType
*
* @typedef {typeof import('./index')} NerdamerType
*
* @typedef {import('./index').NerdamerCore.Utils} UtilsInterface
*
* @typedef {import('./index').NerdamerCore.Math2} Math2Interface
*
* @typedef {import('./index').NerdamerCore.Core} CoreType
*
* @typedef {import('./index').ExpressionParam} ExpressionParam
*
* @typedef {import('./index').ArithmeticOperand} ArithmeticOperand
*
* @typedef {import('./index').ExpandOptions} ExpandOptions
*
* @typedef {import('./index').NerdamerCore.DecomposeResultObject} DecomposeResultType Constructor types
*
* @typedef {import('./index').NerdamerCore.FracConstructor} FracConstructor
*
* @typedef {import('./index').NerdamerCore.SymbolConstructor} SymbolConstructor
*
* @typedef {import('./index').NerdamerCore.VectorConstructor} VectorConstructor
*
* @typedef {import('./index').NerdamerCore.AlgebraModule} AlgebraModuleType
*
* @typedef {import('./index').NerdamerCore.Polynomial} Polynomial
*
* @typedef {import('./index').NerdamerCore.Factors} Factors
*
* @typedef {import('./index').NerdamerCore.FactorsLike} FactorsLike
*
* @typedef {import('./index').NerdamerCore.MVTerm} MVTerm
*
* @typedef {import('./index').NerdamerCore.FactorSubModule} FactorInterface
*
* @typedef {import('./index').NerdamerCore.SimplifySubModule} SimplifyInterface
*
* @typedef {import('./index').NerdamerCore.PartFracSubModule} PartFracInterface
*
* @typedef {new () => Factors} FactorsConstructor
*/
// Check if nerdamer exists globally (browser) or needs to be required (Node.js)
let nerdamer = typeof globalThis !== 'undefined' && globalThis.nerdamer ? globalThis.nerdamer : undefined;
if (typeof module !== 'undefined' && nerdamer === undefined) {
nerdamer = require('./nerdamer.core.js');
require('./Calculus.js');
}
(function initAlgebraModule() {
/* Shortcuts*/
/** @type {CoreType} */
const core = nerdamer.getCore();
/** @type {ParserType} */
const _ = core.PARSER;
const { N, P, S, EX, FN, PL, CP, CB } = core.groups;
const { keys, even, variables, format, round, isInt } = core.Utils;
const { Frac, NerdamerSymbol, Vector: _Vector, Expression: _Expression } = core;
const { CONST_HASH } = core.Settings;
/** @type {Record<string, Function>} */
const math = core.Utils.importFunctions();
const _evaluate = core.Utils.evaluate;
//* ************** CLASSES ***************//
/**
* Converts a symbol into an equivalent polynomial arrays of the form [[coefficient_1, power_1],[coefficient_2,
* power_2], ... ] Univariate polymials only.
*
* @class
* @this {Polynomial}
* @param {NerdamerSymbolType | number | string} [symbol]
* @param {string} [variable] The variable name of the polynomial
* @param {number} [order]
*/
function Polynomial(symbol, variable, order) {
/** @type {FracType[]} */
this.coeffs = [];
/** @type {string} */
this.variable = '';
if (core.Utils.isSymbol(symbol)) {
this.parse(/** @type {NerdamerSymbolType} */ (symbol));
this.variable ||= variable || '';
} else if (typeof symbol === 'number' && !isNaN(symbol)) {
order ||= 0;
if (variable === undefined) {
throw new core.exceptions.InvalidVariableNameError(
'Polynomial expects a variable name when creating using order'
);
}
this.coeffs = [];
this.coeffs[order] = new Frac(symbol);
this.fill(symbol);
} else if (typeof symbol === 'string') {
this.parse(_.parse(symbol));
}
}
/**
* Creates a Polynomial given an array of coefficients
*
* @param {FracType[]} arr
* @param {string} variable
* @returns {Polynomial}
*/
Polynomial.fromArray = function fromArray(arr, variable) {
if (typeof variable === 'undefined') {
throw new core.exceptions.InvalidVariableNameError(
'A variable name must be specified when creating polynomial from array'
);
}
/** @type {Polynomial} */
const p = new Polynomial();
p.coeffs = arr;
p.variable = variable;
return p;
};
/**
* @param {number} c1
* @param {number} c2
* @param {number} n
* @param {number} base
* @param {number} p
* @param {string} variable
* @returns {Polynomial | null}
*/
Polynomial.fit = function fit(c1, c2, n, base, p, variable) {
// After having looped through and mod 10 the number to get the matching factor
const terms = new Array(p + 1);
let t = n - c2;
terms[0] = c2; // The constants is assumed to be correct
// constant for x^p is also assumed know so add
terms[p] = c1;
t -= c1 * base ** p;
// Start fitting
for (let i = p - 1; i > 0; i--) {
const b = base ** i; // We want as many wholes as possible
const q = t / b;
const sign = Math.sign(q);
const c = sign * Math.floor(Math.abs(q));
t -= c * b;
terms[i] = c;
}
if (t !== 0) {
return null;
}
for (let i = 0; i < terms.length; i++) {
terms[i] = new Frac(terms[i]);
}
return Polynomial.fromArray(terms, variable);
};
Polynomial.prototype = {
/**
* Converts NerdamerSymbol to Polynomial
*
* @this {Polynomial}
* @param {NerdamerSymbolType} symbol
* @param {FracType[]} [c] - A collector array
* @returns {void}
*/
parse(symbol, c) {
this.variable = variables(symbol)[0];
if (!symbol.isPoly()) {
throw new core.exceptions.NerdamerTypeError(`Polynomial Expected! Received ${core.Utils.text(symbol)}`);
}
c ||= [];
if (!(/** @type {FracType} */ (symbol.power).absEquals(1))) {
symbol = /** @type {NerdamerSymbolType} */ (_.expand(symbol));
}
if (symbol.group === core.groups.N) {
c[0] = symbol.multiplier;
} else if (symbol.group === core.groups.S) {
c[Number(/** @type {FracType} */ (symbol.power).toDecimal())] = symbol.multiplier;
} else {
for (const x in symbol.symbols) {
if (!Object.hasOwn(symbol.symbols, x)) {
continue;
}
const sub = symbol.symbols[x];
const p = sub.power;
if (core.Utils.isSymbol(p)) {
throw new core.exceptions.NerdamerTypeError('power cannot be a NerdamerSymbol');
}
const pNum = sub.group === N ? 0 : Number(/** @type {FracType} */ (p).toDecimal());
if (sub.symbols) {
this.parse(sub, c);
} else {
c[pNum] = sub.multiplier;
}
}
}
this.coeffs = c;
this.fill();
},
/**
* Fills in the holes in a polynomial with zeroes
*
* @this {Polynomial}
* @param {number} [x] - The number to fill the holes with
* @returns {Polynomial}
*/
fill(x) {
x = Number(x) || 0;
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
if (this.coeffs[i] === undefined) {
this.coeffs[i] = new Frac(x);
}
}
return this;
},
/**
* Removes higher order zeros or a specific coefficient
*
* @this {Polynomial}
* @returns {Polynomial}
*/
trim() {
let l = this.coeffs.length;
while (l--) {
const c = this.coeffs[l];
const equalsZero = c.equals(0);
if (c && equalsZero) {
if (l === 0) {
break;
}
this.coeffs.pop();
} else {
break;
}
}
return this;
},
/**
* Returns polynomial mod p **currently fails**
*
* @this {Polynomial}
* @param {number} p
* @returns {Polynomial}
*/
modP(p) {
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
let c = this.coeffs[i];
let j;
if (c.lessThan(0)) {
// Go borrow
/** @type {FracType | undefined} */
let b; // A coefficient > 0
for (j = i; j < l; j++) {
// Starting from where we left off
if (this.coeffs[j].greaterThan(0)) {
b = this.coeffs[j];
break;
}
}
if (b) {
// If such a coefficient exists
for (; j > i; j--) {
// Go down the line and adjust using p
this.coeffs[j] = this.coeffs[j].subtract(new Frac(1));
this.coeffs[j - 1] = this.coeffs[j - 1].add(new Frac(p));
}
c = this.coeffs[i]; // Reset c
}
}
const d = c.mod(new Frac(p));
const w = c.subtract(d).divide(new Frac(p));
if (!w.equals(0)) {
const upOne = i + 1;
let next = this.coeffs[upOne] || new Frac(0);
next = next.add(w);
this.coeffs[upOne] = next;
this.coeffs[i] = d;
}
}
return this;
},
/**
* Adds together 2 polynomials
*
* @this {Polynomial}
* @param {Polynomial} poly
* @returns {Polynomial}
*/
add(poly) {
const l = Math.max(this.coeffs.length, poly.coeffs.length);
for (let i = 0; i < l; i++) {
const a = this.coeffs[i] || new Frac(0);
const b = poly.coeffs[i] || new Frac(0);
this.coeffs[i] = a.add(b);
}
return this;
},
/**
* Subtracts 2 polynomials
*
* @this {Polynomial}
* @param {Polynomial} poly
* @returns {Polynomial}
*/
subtract(poly) {
const l = Math.max(this.coeffs.length, poly.coeffs.length);
for (let i = 0; i < l; i++) {
const a = this.coeffs[i] || new Frac(0);
const b = poly.coeffs[i] || new Frac(0);
this.coeffs[i] = a.subtract(b);
}
return this;
},
/**
* Divides two polynomials
*
* @this {Polynomial}
* @param {Polynomial} poly
* @returns {[Polynomial, Polynomial]}
*/
divide(poly) {
const { variable } = this;
/** @type {FracType[]} */
const dividend = /** @type {FracType[]} */ (core.Utils.arrayClone(this.coeffs));
/** @type {FracType[]} */
const divisor = /** @type {FracType[]} */ (core.Utils.arrayClone(poly.coeffs));
const n = dividend.length;
const mp = divisor.length - 1;
/** @type {FracType[]} */
const quotient = [];
// Loop through the dividend
for (let i = 0; i < n; i++) {
const p = n - (i + 1);
// Get the difference of the powers
const d = p - mp;
// Get the quotient of the coefficients
const q = dividend[p].divide(divisor[mp]);
if (d < 0) {
break;
} // The divisor is not greater than the dividend
// place it in the quotient
quotient[d] = q;
for (let j = 0; j <= mp; j++) {
// Reduce the dividend
dividend[j + d] = dividend[j + d].subtract(divisor[j].multiply(q));
}
}
// Clean up
const p1 = Polynomial.fromArray(dividend, variable || 'x').trim(); // Pass in x for safety
const p2 = Polynomial.fromArray(quotient, variable || 'x');
return [p2, p1];
},
/**
* Multiplies two polynomials
*
* @this {Polynomial}
* @param {Polynomial} poly
* @returns {Polynomial}
*/
multiply(poly) {
const l1 = this.coeffs.length;
const l2 = poly.coeffs.length;
/** @type {FracType[]} */
const c = []; // Array to be returned
for (let i = 0; i < l1; i++) {
const x1 = this.coeffs[i];
for (let j = 0; j < l2; j++) {
const k = i + j; // Add the powers together
const x2 = poly.coeffs[j];
const e = c[k] || new Frac(0); // Get the existing term from the new array
c[k] = e.add(x1.multiply(x2)); // Multiply the coefficients and add to new polynomial array
}
}
this.coeffs = c;
return this;
},
/**
* Checks if a polynomial is zero
*
* @this {Polynomial}
* @returns {boolean}
*/
isZero() {
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
const e = this.coeffs[i];
if (!e.equals(0)) {
return false;
}
}
return true;
},
/**
* Substitutes in a number n into the polynomial p(n)
*
* @this {Polynomial}
* @param {number} n
* @returns {FracType}
*/
sub(n) {
let sum = new Frac(0);
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
const t = this.coeffs[i];
if (!t.equals(0)) {
sum = sum.add(t.multiply(new Frac(n ** i)));
}
}
return sum;
},
/**
* Returns a clone of the polynomial
*
* @this {Polynomial}
* @returns {Polynomial}
*/
clone() {
/** @type {Polynomial} */
const p = new Polynomial();
p.coeffs = this.coeffs.slice();
p.variable = this.variable;
return p;
},
/**
* Gets the degree of the polynomial
*
* @this {Polynomial}
* @returns {number}
*/
deg() {
this.trim();
return this.coeffs.length - 1;
},
/**
* Returns a lead coefficient
*
* @this {Polynomial}
* @returns {FracType}
*/
lc() {
return this.coeffs[this.deg()].clone();
},
/**
* Converts polynomial into a monic polynomial
*
* @this {Polynomial}
* @returns {Polynomial}
*/
monic() {
const lc = this.lc();
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
this.coeffs[i] = this.coeffs[i].divide(lc);
}
return this;
},
/**
* Returns the GCD of two polynomials
*
* @this {Polynomial}
* @param {Polynomial} poly
* @returns {Polynomial}
*/
gcd(poly) {
// Get the maximum power of each
const mp1 = this.coeffs.length - 1;
const mp2 = poly.coeffs.length - 1;
/** @type {[Polynomial, Polynomial]} */
let T;
// Swap so we always have the greater power first
if (mp1 < mp2) {
return poly.gcd(this);
}
/** @type {Polynomial} */
let a = this;
while (!poly.isZero()) {
const t = poly.clone();
a = a.clone();
T = a.divide(t);
poly = T[1];
a = t;
}
const gcd = core.Math2.QGCD.apply(null, a.coeffs);
if (!gcd.equals(1)) {
const l = a.coeffs.length;
for (let i = 0; i < l; i++) {
a.coeffs[i] = a.coeffs[i].divide(gcd);
}
}
return a;
},
/**
* Differentiates the polynomial
*
* @this {Polynomial}
* @returns {Polynomial}
*/
diff() {
/** @type {FracType[]} */
const newArray = [];
const l = this.coeffs.length;
for (let i = 1; i < l; i++) {
newArray.push(this.coeffs[i].multiply(new Frac(i)));
}
this.coeffs = newArray;
return this;
},
/**
* Integrates the polynomial
*
* @this {Polynomial}
* @returns {Polynomial}
*/
integrate() {
/** @type {FracType[]} */
const newArray = [new Frac(0)];
const l = this.coeffs.length;
for (let i = 0; i < l; i++) {
const c = new Frac(i + 1);
newArray[i + 1] = this.coeffs[i].divide(c);
}
this.coeffs = newArray;
return this;
},
/**
* Returns the Greatest common factor of the polynomial
*
* @this {Polynomial}
* @param {boolean} [toPolynomial] - True if a polynomial is wanted
* @returns {[FracType, number] | Polynomial}
*/
gcf(toPolynomial) {
// Get the first nozero coefficient and returns its power
/**
* @param {FracType[]} a
* @returns {number | undefined}
*/
const fnz = function (a) {
for (let i = 0; i < a.length; i++) {
if (!a[i].equals(0)) {
return i;
}
}
return undefined;
};
/** @type {FracType[]} */
const ca = [];
for (let i = 0; i < this.coeffs.length; i++) {
const c = this.coeffs[i];
if (!c.equals(0) && ca.indexOf(c) === -1) {
ca.push(c);
}
}
/** @type {[FracType, number] | Polynomial} */
let p = [core.Math2.QGCD.apply(undefined, ca), fnz(this.coeffs) || 0];
if (toPolynomial) {
const parr = [];
parr[p[1] - 1] = p[0];
p = Polynomial.fromArray(parr, this.variable).fill();
}
return p;
},
/**
* Raises a polynomial P to a power p -> P^p. e.g. (x+1)^2
*
* @this {Polynomial}
* @param {boolean} [inclImg] - Include imaginary numbers
* @returns {number[]}
*/
quad(inclImg) {
/** @type {number[]} */
const roots = [];
if (this.coeffs.length > 3) {
throw new Error(`Cannot calculate quadratic order of ${this.coeffs.length - 1}`);
}
if (this.coeffs.length === 0) {
throw new Error('Polynomial array has no terms');
}
const a = this.coeffs[2] ? Number(this.coeffs[2].toDecimal()) : 0;
const b = this.coeffs[1] ? Number(this.coeffs[1].toDecimal()) : 0;
const c = Number(this.coeffs[0].toDecimal());
const dsc = b * b - 4 * a * c;
if (dsc < 0 && !inclImg) {
return roots;
}
roots[0] = (-b + Math.sqrt(dsc)) / (2 * a);
roots[1] = (-b - Math.sqrt(dsc)) / (2 * a);
return roots;
},
/**
* Makes polynomial square free
*
* @this {Polynomial}
* @returns {[Polynomial, Polynomial, number]}
*/
squareFree() {
const a = this.clone();
let i = 1;
const b = a.clone().diff();
let c = a.clone().gcd(b);
let w = a.divide(c)[0];
let output = Polynomial.fromArray([new Frac(1)], a.variable);
while (!c.equalsNumber(1)) {
const y = w.gcd(c);
let z = w.divide(y)[0];
// One of the factors may have shown up since it's square but smaller than the
// one where finding
if (!z.equalsNumber(1) && i > 1) {
const t = z.clone();
for (let j = 1; j < i; j++) {
t.multiply(z.clone());
}
z = t;
}
output = output.multiply(z);
i++;
w = y;
c = c.divide(y)[0];
}
return [output, w, i];
},
/**
* Converts polynomial to NerdamerSymbol
*
* @this {Polynomial}
* @returns {NerdamerSymbolType}
*/
toSymbol() {
const l = this.coeffs.length;
const { variable } = this;
if (l === 0) {
return new NerdamerSymbol(0);
}
// Polynomials must have a variable
if (!variable) {
throw new core.exceptions.NerdamerTypeError(
'Polynomial.toSymbol requires a variable. Constants should not be converted to Polynomial.'
);
}
const terms = [];
for (let i = 0; i < l; i++) {
const e = this.coeffs[i];
if (!e.equals(0)) {
terms.push(`${e}*${variable}^${i}`);
}
}
if (terms.length === 0) {
return new NerdamerSymbol(0);
}
return _.parse(terms.join('+'));
},
/**
* Checks if polynomial is equal to a number
*
* @this {Polynomial}
* @param {number} x
* @returns {boolean}
*/
equalsNumber(x) {
this.trim();
return this.coeffs.length === 1 && this.coeffs[0].toDecimal() === String(x);
},
/**
* @this {Polynomial}
* @returns {string}
*/
toString() {
return this.toSymbol().toString();
},
};
/**
* # TODO
*
* # THIS METHOD HAS A NASTY HIDDEN BUG. IT HAS INCONSISTENT RETURN TYPES PRIMARILY DUE TO
*
* WRONG ASSUMPTIONS AT THE BEGINNING. THE ASSUMPTION WAS THAT COEFFS WERE ALWAYS GOING BE NUMBERS NOT TAKING INTO
* ACCOUNT THAT IMAGINARY NUMBERS. FIXING THIS BREAKS WAY TOO MANY TESTS AT THEM MOMENT WHICH I DON'T HAVE TO FIX
*
* If the symbols is of group PL or CP it will return the multipliers of each symbol as these are polynomial
* coefficients. CB symbols are glued together by multiplication so the symbol multiplier carries the coefficients
* for all contained symbols. For S it just returns it's own multiplier. This function doesn't care if it's a
* polynomial or not
*
* @this {NerdamerSymbolType}
* @param {Array} [c] The coefficient array
* @param {boolean} [withOrder]
* @returns {Array}
*/
NerdamerSymbol.prototype.coeffs = function coeffs(c, withOrder) {
if (withOrder && !this.isPoly(true)) {
_.error('Polynomial expected when requesting coefficients with order');
}
c ||= [];
const s = this.clone().distributeMultiplier();
if (s.isComposite()) {
for (const x in s.symbols) {
if (!Object.hasOwn(s.symbols, x)) {
continue;
}
const sub = s.symbols[x];
if (sub.isComposite()) {
sub.clone().distributeMultiplier().coeffs(c, withOrder);
} else if (withOrder) {
c[sub.isConstant() ? 0 : Number(/** @type {FracType} */ (sub.power).toDecimal())] = sub.multiplier;
} else {
c.push(sub.multiplier);
}
}
} else if (withOrder) {
c[s.isConstant(true) ? 0 : Number(/** @type {FracType} */ (s.power).toDecimal())] = s.multiplier;
} else if (s.group === CB && s.isImaginary()) {
let m = new NerdamerSymbol(s.multiplier);
s.each(x => {
// Add the imaginary part
if (x.isConstant(true) || x.imaginary) {
m = /** @type {NerdamerSymbolType} */ (_.multiply(m, x));
}
});
c.push(m);
} else {
c.push(s.multiplier);
}
// Fill the holes
if (withOrder) {
for (let i = 0; i < c.length; i++) {
if (c[i] === undefined) {
c[i] = new NerdamerSymbol(0);
}
}
}
return c;
};
/**
* @this {NerdamerSymbolType}
* @param {Record<string, number> & { length: number }} map
* @returns {MVTerm[]}
*/
NerdamerSymbol.prototype.tBase = function tBase(map) {
if (typeof map === 'undefined') {
throw new Error('NerdamerSymbol.tBase requires a map object!');
}
/** @type {MVTerm[]} */
const terms = [];
const symbols = /** @type {NerdamerSymbolType[]} */ (this.collectSymbols(null, null, null, true));
const l = symbols.length;
for (let i = 0; i < l; i++) {
const symbol = symbols[i];
const g = symbol.group;
/** @type {MVTerm} */
const nterm = new MVTerm(symbol.multiplier, [], map);
if (g === CB) {
for (const x in symbol.symbols) {
if (!Object.hasOwn(symbol.symbols, x)) {
continue;
}
const sym = symbol.symbols[x];
nterm.terms[map[x]] = /** @type {FracType} */ (sym.power);
}
} else {
nterm.terms[map[symbol.value]] = /** @type {FracType} */ (symbol.power);
}
terms.push(nterm.fill());
nterm.updateCount();
}
return terms;
};
/**
* @this {NerdamerSymbolType}
* @param {string} x
* @returns {string}
*/
NerdamerSymbol.prototype.altVar = function altVar(x) {
const m = this.multiplier.toString();
const p = this.power.toString();
return (m === '1' ? '' : `${m}*`) + x + (p === '1' ? '' : `^${p}`);
};
/**
* Checks to see if the symbols contain the same variables
*
* @this {NerdamerSymbolType}
* @param {NerdamerSymbolType} symbol
* @returns {boolean}
*/
NerdamerSymbol.prototype.sameVars = function sameVars(symbol) {
if (!(this.symbols || this.group === symbol.group)) {
return false;
}
for (const x in this.symbols) {
if (!Object.hasOwn(this.symbols, x)) {
continue;
}
const a = this.symbols[x];
const b = symbol.symbols[x];
if (!b) {
return false;
}
if (a.value !== b.value) {
return false;
}
}
return true;
};
/**
* Groups the terms in a symbol with respect to a variable For instance the symbol {a_b^2_x^2+a_b_x^2+x+6} returns
* [6,1,a_b+a_b^2]
*
* @this {NerdamerSymbolType}
* @param {string} x
* @returns {NerdamerSymbolType[]}
*/
NerdamerSymbol.prototype.groupTerms = function groupTerms(x) {
x = String(x);
/** @type {DecomposeResultType | undefined} */
let f;
/** @type {number} */
let p;
/** @type {NerdamerSymbolType[] | undefined} */
let egrouped;
/** @type {NerdamerSymbolType[]} */
const grouped = [];
this.each(e => {
if (e.group === PL) {
egrouped = e.groupTerms(x);
for (let i = 0; i < egrouped.length; i++) {
const el = egrouped[i];
if (el) {
grouped[i] = el;
}
}
} else {
f = /** @type {DecomposeResultType} */ (core.Utils.decompose_fn(e, x, true));
p =
/** @type {NerdamerSymbolType} */ (f.x).value === x
? Number(/** @type {NerdamerSymbolType} */ (f.x).power)
: 0;
// Check if there's an existing value
grouped[p] = /** @type {NerdamerSymbolType} */ (_.add(grouped[p] || new NerdamerSymbol(0), f.a));
}
});
return grouped;
};
/**
* Use this to collect Factors
*
* @this {NerdamerSymbolType}
* @returns {NerdamerSymbolType[]}
*/
NerdamerSymbol.prototype.collectFactors = function collectFactors() {
/** @type {NerdamerSymbolType[]} */
const factors = [];
if (this.group === CB) {
this.each(x => {
factors.push(x.clone());
});
} else {
factors.push(this.clone());
}
return factors;
};
/**
* A container class for factors
*
* @class
* @this {Factors}
*/
function Factors() {
/** @type {Record<string, NerdamerSymbolType>} */
this.factors = {};
/** @type {number} */
this.length = 0;
/** @type {((s: NerdamerSymbolType) => NerdamerSymbolType) | undefined} */
this.preAdd = undefined;
/** @type {number | string | undefined} */
this.pFactor = undefined;
}
/**
* @this {Factors}
* @returns {number}
*/
Factors.prototype.getNumberSymbolics = function getNumberSymbolics() {
let n = 0;
this.each(x => {
if (!x.isConstant(true)) {
n++;
}
});
return n;
};
/**
* Adds the factors to the factor object
*
* @this {Factors}
* @param {NerdamerSymbolType} s
* @returns {Factors}
*/
Factors.prototype.add = function add(s) {
if (s.equals(0)) {
return this;
} // Nothing to add
// we don't want to carry -1 as a factor. If a factor already exists,
// then add the minus one to that factor and return.
if (s.equals(-1) && this.length > 0) {
const fo = core.Utils.firstObject(this.factors, null, true);
const newObj = /** @type {NerdamerSymbolType} */ (
_.symfunction(core.Settings.PARENTHESIS, [fo.obj]).negate()
);
delete this.factors[fo.key];
this.add(newObj);
this.length--;
return this;
}
if (s.group === CB) {
const factors = this;
if (!s.multiplier.equals(1)) {
factors.add(new NerdamerSymbol(s.multiplier));
}
s.each(x => {
factors.add(x);
});
} else {
if (this.preAdd) // If a preAdd function was defined call it to do prep
{
s = this.preAdd(s);
}
if (this.pFactor) // If the symbol isn't linear add back the power
{
s = /** @type {NerdamerSymbolType} */ (_.pow(s, new NerdamerSymbol(this.pFactor)));
}
const isConstant = s.isConstant();
if (isConstant && s.equals(1)) {
return this;
} // Don't add 1
const v = isConstant ? s.value : s.text();
if (v in this.factors) {
this.factors[v] = /** @type {NerdamerSymbolType} */ (_.multiply(this.factors[v], s));
// Did the addition cancel out the existing factor? If so remove it and decrement the length
if (this.factors[v].equals(1)) {
delete this.factors[v];
this.length--;
}
} else {
this.factors[v] = s;
this.length++;
}
}
return this;
};
/**
* Converts the factor object to a NerdamerSymbol
*
* @this {Factors}
* @returns {NerdamerSymbolType}
*/
Factors.prototype.toSymbol = function toSymbol() {
/** @type {NerdamerSymbolType} */
let factored = new NerdamerSymbol(1);
const factors = Object.values(this.factors).sort((a, b) => (a.group > b.group ? 1 : -1));
for (let i = 0, l = factors.length; i < l; i++) {
const f = factors[i];
// Don't wrap group S or FN
const factor =
f.power.equals(1) && f.fname !== '' /* Don't wrap it twice */
? _.symfunction(core.Settings.PARENTHESIS, [f])
: f;
factored = /** @type {NerdamerSymbolType} */ (_.multiply(factored, factor));
}
if (factored.fname === '') {
factored = NerdamerSymbol.unwrapPARENS(factored);
}
return factored;
};
/**
* Merges 2 factor objects into one
*
* @this {Factors}
* @param {Record<string, NerdamerSymbolType>} o
* @returns {Factors}
*/
Factors.prototype.merge = function merge(o) {
for (const x in o) {
if (x in this.factors) {
this.factors[x] = /** @type {NerdamerSymbolType} */ (_.multiply(this.factors[x], o[x]));
} else {
this.factors[x] = o[x];
}
}
return this;
};
/**
* The iterator for the factor object
*
* @this {Factors}
* @param {(factor: NerdamerSymbolType, key: string) => void} f - Callback
* @returns {Factors}
*/
Factors.prototype.each = function each(f) {
for (const x in this.factors) {
if (!Object.hasOwn(this.factors, x)) {
continue;
}
let factor = this.factors[x];
if (factor.fname === core.Settings.PARENTHESIS && factor.isLinear()) {
factor = factor.args[0];
}
f.call(this, factor, x);
}
return this;
};
/**
* Return the number of factors contained in the factor object
*
* @this {Factors}
* @returns {number}
*/
Factors.prototype.count = function count() {
return keys(this.factors).length;
};
/**
* Cleans up factors from -1
*
* @this {Factors}
* @returns {void}
*/
Factors.prototype.clean = function clean() {
try {
const h = core.Settings.CONST_HASH;
if (this.factors[h].lessThan(0)) {
if (this.factors[h].equals(-1)) {
delete this.factors[h];
} else {
this.factors[h].negate();
}
this.each(x => {
x.negate();
});
}
} catch (e) {
if (/** @type {Error} */ (e).message === 'timeout') {
throw e;
}
}
};
/**
* @this {Factors}
* @returns {string}
*/
Factors.prototype.toString = function toString() {
return this.toSymbol().toString();
};
/**
* A wrapper for performing multivariate division
*
* @class
* @this {MVTerm}
* @param {FracType} coeff
* @param {FracType[]} [terms]
* @param {Record<string, number> & { length: number }} [map]
*/
function MVTerm(coeff, terms, map) {
/** @type {FracType[]} */
this.terms = terms || [];
/** @type {FracType} */
this.coeff = coeff;
/** @type {(Record<string, number> & { length: number }) | undefined} */
this.map = map; // Careful! all maps are the same object
/** @type {FracType} */
this.sum = new Frac(0);
/** @type {string | undefined} */
this.image = undefined;
/** @type {Record<number, string> | undefined} */
this.revMap = undefined;
/** @type {number | undefined} */
this.count = undefined;
}
/**
* @this {MVTerm}
* @returns {MVTerm}
*/
MVTerm.prototype.updateCount = function updateCount() {
this.count ||= 0;
for (let i = 0; i < this.terms.length; i++) {
if (!this.terms[i].equals(0)) {
this.count++;
}
}
return this;
};
/**
* @this {MVTerm}
* @returns {string}
*/
MVTerm.prototype.getVars = function getVars() {
/** @type {string[]} */
const vars = [];
for (let i = 0; i < this.terms.length; i++) {
const term = this.terms[i];
this.getRevMap();
if (!term.equals(0) && this.revMap) {
vars.push(this.revMap[i]);
}
}
return vars.join(' ');
};
/**
* @this {MVTerm}
* @returns {number}
*/
MVTerm.prototype.len = function len() {
if (typeof this.count === 'undefined') {
this.updateCount();
}
return this.count || 0;
};
/**
* @this {MVTerm}
* @param {Record<number, string>} [revMap]
* @returns {NerdamerSymbolType}
*/
MVTerm.prototype.toSymbol = function toSymbol(revMap) {
revMap ||= this.getRevMap();
/** @type {NerdamerSymbolType} */
let symbol = new NerdamerSymbol(this.coeff);
for (let i = 0; i < this.terms.length; i++) {
const v = revMap[i];
const t = this.terms[i];
if (t.equals(0) || v === CONST_HASH) {
continue;
}
const mapped = new NerdamerSymbol(v);
mapped.power = t;
symbol = /** @type {NerdamerSymbolType} */ (_.multiply(symbol, mapped));
}
return symbol;
};
/**
* @this {MVTerm}
* @returns {Record<number, string>}
*/
MVTerm.prototype.getRevMap = function getRevMap() {
if (this.revMap) {
return this.revMap;
}
/** @type {Record<number, string>} */
const o = {};
if (this.map) {
for (const x in this.map) {
if (!Object.hasOwn(this.map, x)) {
continue;
}
o[this.map[x]] = x;
}
}
this.revMap = o;
return o;
};
/**
* @this {MVTerm}
* @returns {MVTerm}
*/
MVTerm.prototype.generateImage = function generateImage() {
this.image = this.terms.join(' ');
return this;
};
/**
* @this {MVTerm}
* @returns {string}
*/
MVTerm.prototype.getImg = function getImg() {
if (!this.image) {
this.generateImage();
}
return this.image || '';
};
/**
* @this {MVTerm}
* @returns {MVTerm}
*/
MVTerm.prototype.fill = function fill() {
const l = this.map ? this.map.length : 0;
for (let i = 0; i < l; i++) {
if (typeof this.terms[i] === 'undefined') {
this.terms[i] = new Frac(0);
} else {
this.sum = this.sum.add(this.terms[i]);
}
}
return this;
};
/**
* @this {MVTerm}
* @param {MVTerm} mvterm
* @returns {MVTerm}
*/
MVTerm.prototype.divide = function divide(mvterm) {
const c = this.coeff.divide(mvterm.coeff);
const l = this.terms.length;
/** @type {MVTerm} */
const newMvterm = new MVTerm(c, [], this.map);
for (let i = 0; i < l; i++) {
newMvterm.terms[i] = this.terms[i].subtract(mvterm.terms[i]);
newMvterm.sum = newMvterm.sum.add(newMvterm.terms[i]);
}
return newMvterm;
};
/**
* @this {MVTerm}
* @param {MVTerm} mvterm
* @returns {MVTerm}
*/
MVTerm.prototype.multiply = function multiply(mvterm) {
const c = this.coeff.multiply(mvterm.coeff);
const l = this.terms.length;
/** @type {MVTerm} */
const newMvterm = new MVTerm(c, [], this.map);
for (let i = 0; i < l; i++) {
newMvterm.terms[i] = this.terms[i].add(mvterm.terms[i]);
newMvterm.sum = newMvterm.sum.add(newMvterm.terms[i]);
}
return newMvterm;
};
/**
* @this {MVTerm}
* @returns {boolean}
*/
MVTerm.prototype.isZero = function isZero() {
return this.coeff.equals(0);
};
/**
* @this {MVTerm}
* @returns {string}
*/
MVTerm.prototype.toString = function toString() {
return `{ coeff: ${this.coeff.toString()}, terms: [${this.terms.join(
','
)}]: sum: ${this.sum.toString()}, count: ${this.count}}`;
};
/**
* @param {string[]} arr
* @returns {Record<string, number> & { length: number }}
*/
core.Utils.toMapObj = function toMapObj(arr) {
let c = 0;
/** @type {Record<string, number> & { length: number }} */
const o = /** @type {Record<string, number> & { length: number }} */ ({ length: 0 });
for (let i = 0; i < arr.length; i++) {
const v = arr[i];
if (typeof o[v] === 'undefined') {
o[v] = c;
c++;
}
}
o.length = c;
return o;
};
/**
* @template T
* @param {T} v
* @param {number} n
* @param {new (v: T) => T} [Clss]
* @returns {T[]}
*/
core.Utils.filledArray = function filledArray(v, n, Clss) {
const a = [];
while (n--) {
a[n] = Clss ? new Clss(v) : v;
}
return a;
};
/**
* @param {number[]} arr
* @returns {number}
*/
core.Utils.arrSum = function arrSum(arr) {
let sum = 0;
const l = arr.length;
for (let i = 0; i < l; i++) {
sum += arr[i];
}
return sum;
};
/**
* Determines if 2 arrays have intersecting elements.
*
* @template T
* @param {T[]} a
* @param {T[]} b
* @returns {boolean} True if a and b have intersecting elements.
*/
core.Utils.haveIntersection = function haveIntersection(a, b) {
if (b.length > a.length) {
[a, b] = [b, a]; // IndexOf to loop over shorter
}
return a.some(e => b.indexOf(e) > -1);
};
/**
* Substitutes out functions as variables so they can be used in regular algorithms
*
* @param {NerdamerSymbolType} symbol
* @param {Record<string, string>} [map]
* @returns {string} The expression string
*/
core.Utils.subFunctions = function subFunctions(symbol, map) {
map ||= {};
/** @type {string[]} */
const subbed = [];
const vars = new Set(variables(symbol));
symbol.each(x => {
if (x.group === FN || x.previousGroup === FN) {
// We need a new variable name so why not use one of the existing
const val = core.Utils.text(x, 'hash');
const tvar = map[val];
if (tvar) {
subbed.push(x.altVar(tvar));
} else {
// Generate a unique enough name
// GM make sure it's not the name of an existing variable
let i = 0;
let t;
do {
t = x.fname + keys(map).length + (i > 0 ? String(i) : '');
i++;
} while (vars.has(t));
map[val] = t;
subbed.push(x.altVar(t));
}
} else if (x.group === CB || x.group === PL || x.group === CP) {
subbed.push(core.Utils.subFunctions(x, map));
} else {
subbed.push(x.text());
}
});
if (symbol.group === CP || symbol.group === PL) {
return symbol.altVar(core.Utils.inBrackets(subbed.join('+')));
}
if (symbol.group === CB) {
return symbol.altVar(core.Utils.inBrackets(subbed.join('*')));
}
return symbol.text();
};
/**
* @param {Record<string, string>} map
* @returns {Record<string, NerdamerSymbolType>}
*/
core.Utils.getFunctionsSubs = function getFunctionsSubs(map) {
/** @type {Record<string, NerdamerSymbolType>} */
const subs = {};
// Prepare substitutions
for (const x in map) {
if (!Object.hasOwn(map, x)) {
continue;
}
subs[map[x]] = _.parse(x);
}
return subs;
};
/** @type {AlgebraModuleType} */
const __ = (core.Algebra = {
version: '1.4.6',
/**
* @param {NerdamerSymbolType | Array} symbol
* @param {number} [decp]
* @returns {(string | number)[]}
*/
proots(symbol, decp) {
// The roots will be rounded up to 7 decimal places.
// if this causes trouble you can explicitly pass in a different number of places
// rarr for polynomial of power n is of format [n, coeff x^n, coeff x^(n-1), ..., coeff x^0]
decp ||= 7;
const zeros = 0;
/** @type {(string | number)[]} */
const knownRoots = [];
/**
* @param {FracType[]} rarr
* @param {(string | number)[]} powers
* @param {number} max
* @returns {(string | number)[]}
*/
const getRoots = function (rarr, powers, max) {
const roots = calcroots(rarr, powers, max).concat(knownRoots);
for (let i = 0; i < zeros; i++) {
roots.unshift(0);
}
return /** @type {string[]} */ (roots);
};
if (core.Utils.isSymbol(symbol) && /** @type {NerdamerSymbolType} */ (symbol).isPoly()) {
let sym = /** @type {NerdamerSymbolType} */ (symbol);
sym.distributeMultiplier();
// Make it so the symbol has a constants as the lowest term
if (sym.group === PL) {
const lowestPow = core.Utils.arrayMin(
/** @type {number[]} */ (/** @type {unknown} */ (keys(sym.symbols)))
);
const lowestSymbol = sym.symbols[lowestPow].clone().toUnitMultiplier();
sym = /** @type {NerdamerSymbolType} */ (_.expand(_.divide(sym, lowestSymbol)));
knownRoots.push(0); // Add zero since this is a known root
}
if (sym.group === core.groups.S) {
return [/** @type {string} */ ('0')];
}
if (sym.group === core.groups.PL) {
const powers = keys(sym.symbols);
const minpower = core.Utils.arrayMin(/** @type {number[]} */ (/** @type {unknown} */ (powers)));
sym = /** @type {NerdamerSymbolType} */ (
core.PARSER.divide(sym, core.PARSER.parse(`${sym.value}^${minpower}`))
);
}
const variable = keys(sym.symbols).sort().pop();
const subSym = sym.group === core.groups.PL ? sym.symbols : sym.symbols[variable || ''];
const g = subSym.group;