From 37c550dad9fa0c071964fec647277471c0ddd6cf Mon Sep 17 00:00:00 2001 From: Glen Whitney Date: Fri, 31 Jan 2025 03:40:03 -0800 Subject: [PATCH] fix: Uniformize bigint construction Adds options to the bigint constructor function `math.bigint()` to control whether/how non-integer inputs are rounded or errors thrown, and whether "unsafe" values outside the range where integers can be uniquely represented are converted. Changes `math.numeric(x, 'bigint')` to call the bigint constructor configured to allow unsafe values but throw on non-integers (closest approximation to its previous buggy behavior). Also restores documentation generation for constructor functions, and initiates History sections in function doc pages. Resolves #3366. Resolves #3368. Resolves #3341. --- src/core/function/import.js | 15 +- src/expression/parse.js | 26 +++- src/function/algebra/polynomialRoot.js | 10 +- src/function/arithmetic/log.js | 23 ++- src/function/matrix/map.js | 10 +- src/function/relational/compare.js | 5 + src/function/string/format.js | 9 +- src/function/utils/numeric.js | 13 +- src/type/bigint.js | 175 ++++++++++++++++++++--- src/type/bignumber/function/bignumber.js | 2 +- src/type/boolean.js | 12 +- src/type/complex/function/complex.js | 23 ++- src/type/fraction/function/fraction.js | 12 +- src/type/matrix/function/index.js | 10 +- src/type/matrix/function/matrix.js | 8 +- src/type/matrix/function/sparse.js | 6 +- src/type/string.js | 14 +- src/type/unit/function/unit.js | 19 ++- test/node-tests/doc.test.js | 34 ++--- test/unit-tests/type/bigint.test.js | 70 ++++++++- test/unit-tests/type/numeric.test.js | 24 ++++ tools/docgenerator.js | 53 +++++-- 22 files changed, 481 insertions(+), 92 deletions(-) diff --git a/src/core/function/import.js b/src/core/function/import.js index 30c525c768..94e7539d28 100644 --- a/src/core/function/import.js +++ b/src/core/function/import.js @@ -45,17 +45,26 @@ export function importFactory (typed, load, math, importedFactories) { * return 'hello, ' + name + '!' * } * }) - * - * // use the imported function and variable + * // use the imported function and variable * math.myvalue * 2 // 84 + * * math.hello('user') // 'hello, user!' * * // import the npm module 'numbers' * // (must be installed first with `npm install numbers`) * math.import(numbers, {wrap: true}) - * * math.fibonacci(7) // returns 13 * + * See also: + * + * create, all + * + * History: + * + * v0.2 Created + * v0.7 Changed second parameter to an options object + * v2 Dropped support for direct import of a module by name + * * @param {Object | Array} functions Object with functions to be imported. * @param {Object} [options] Import options. */ diff --git a/src/expression/parse.js b/src/expression/parse.js index 32faf04344..c0da494bfd 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -66,19 +66,35 @@ export const createParse = /* #__PURE__ */ factory(name, dependencies, ({ * node1.compile().evaluate() // 5 * * let scope = {a:3, b:4} - * const node2 = math.parse('a * b') // 12 + * const node2 = math.parse('a * b') + * node2.evaluate(scope) // 12 * const code2 = node2.compile() - * code2.evaluate(scope) // 12 + * scope.b = 5 + * code2.evaluate(scope) // 15 * scope.a = 5 - * code2.evaluate(scope) // 20 + * code2.evaluate(scope) // 25 * - * const nodes = math.parse(['a = 3', 'b = 4', 'a * b']) - * nodes[2].compile().evaluate() // 12 + * const nodes = math.parse(['a = 3', 'b = 2', 'a * b']) + * const newscope = {} + * nodes.map(node => node.compile().evaluate(newscope)) // [3, 2, 6] * * See also: * * evaluate, compile * + * History: + * + * v0.9 Created + * v0.13 Switched to one-based indices + * v0.14 Added `[1,2;3,4]` notation for matrices + * v0.18 Dropped the `function` keyword + * v0.20 Added ternary conditional + * v0.27 Allow multi-line expressions; allow functions that receive + * unevaluated parameters (`rawArgs`) + * v3 Add object notation; allow assignments internal to other + * expressions + * v7.3 Supported binary, octal, and hexadecimal notation + * * @param {string | string[] | Matrix} expr Expression to be parsed * @param {{nodes: Object}} [options] Available options: * - `nodes` a set of custom nodes diff --git a/src/function/algebra/polynomialRoot.js b/src/function/algebra/polynomialRoot.js index 2d331f5c18..98c684a26f 100644 --- a/src/function/algebra/polynomialRoot.js +++ b/src/function/algebra/polynomialRoot.js @@ -42,17 +42,17 @@ export const createPolynomialRoot = /* #__PURE__ */ factory(name, dependencies, * math.polynomialRoot(constant, linearCoeff, quadraticCoeff, cubicCoeff) * * Examples: - * // linear + * // linear * math.polynomialRoot(6, 3) // [-2] * math.polynomialRoot(math.complex(6,3), 3) // [-2 - i] * math.polynomialRoot(math.complex(6,3), math.complex(2,1)) // [-3 + 0i] - * // quadratic + * // quadratic * math.polynomialRoot(2, -3, 1) // [2, 1] * math.polynomialRoot(8, 8, 2) // [-2] * math.polynomialRoot(-2, 0, 1) // [1.4142135623730951, -1.4142135623730951] * math.polynomialRoot(2, -2, 1) // [1 + i, 1 - i] * math.polynomialRoot(math.complex(1,3), math.complex(-3, -2), 1) // [2 + i, 1 + i] - * // cubic + * // cubic * math.polynomialRoot(-6, 11, -6, 1) // [1, 3, 2] * math.polynomialRoot(-8, 0, 0, 1) // [-1 - 1.7320508075688774i, 2, -1 + 1.7320508075688774i] * math.polynomialRoot(0, 8, 8, 2) // [0, -2] @@ -61,6 +61,10 @@ export const createPolynomialRoot = /* #__PURE__ */ factory(name, dependencies, * See also: * cbrt, sqrt * + * History: + * + * v11.4 Created + * * @param {... number | Complex} coeffs * The coefficients of the polynomial, starting with with the constant coefficent, followed * by the linear coefficient and subsequent coefficients of increasing powers. diff --git a/src/function/arithmetic/log.js b/src/function/arithmetic/log.js index 3dadeaa40e..8654a91e37 100644 --- a/src/function/arithmetic/log.js +++ b/src/function/arithmetic/log.js @@ -13,6 +13,13 @@ export const createLog = /* #__PURE__ */ factory(name, dependencies, ({ typed, t * To avoid confusion with the matrix logarithm, this function does not * apply to matrices. * + * Note that when the value is a Fraction, then the + * base must be specified as a Fraction, and the log() will only be + * returned when the result happens to be rational. When there is an + * attempt to take a log of Fractions that would result in an irrational + * value, a TypeError against implicit conversion of BigInt to Fraction + * is thrown. + * * Syntax: * * math.log(x) @@ -34,11 +41,25 @@ export const createLog = /* #__PURE__ */ factory(name, dependencies, ({ typed, t * * exp, log2, log10, log1p * + * History: + * + * v0.0.2 Created + * v0.2 Add optional base argument + * v0.3 Handle Array input + * v0.5 Handle Matrix input + * v0.16 Handle BigNumber input + * v0.21 Support negative BigNumbers + * v11 Drop Array/Matrix support in favor of explicit map of + * the scalar log function, to avoid confusion with the log + * of a matrix + * v14 Allow value and base to be Fractions, when the log is rational + * * @param {number | BigNumber | Fraction | Complex} x * Value for which to calculate the logarithm. * @param {number | BigNumber | Fraction | Complex} [base=e] * Optional base for the logarithm. If not provided, the natural - * logarithm of `x` is calculated. + * logarithm of `x` is calculated, unless x is a Fraction, in + * which case an error is thrown. * @return {number | BigNumber | Fraction | Complex} * Returns the logarithm of `x` */ diff --git a/src/function/matrix/map.js b/src/function/matrix/map.js index d8ff940431..089c01f663 100644 --- a/src/function/matrix/map.js +++ b/src/function/matrix/map.js @@ -38,13 +38,19 @@ export const createMap = /* #__PURE__ */ factory(name, dependencies, ({ typed }) * // callback(value, index, Array) * // If you want to call with only one argument, use: * math.map([1, 2, 3], x => math.format(x)) // returns ['1', '2', '3'] - * // It can also be called with 2N + 1 arguments: for N arrays - * // callback(value1, value2, index, BroadcastedArray1, BroadcastedArray2) + * // It can also be called with 2N + 1 arguments: for N arrays + * // callback(value1, value2, index, BroadcastedArray1, BroadcastedArray2) * * See also: * * filter, forEach, sort * + * History: + * + * v0.13 Created + * v1.1 Clone the indices on each callback in case callback mutates + * v13.1 Support multiple inputs to the callback + * * @param {Matrix | Array} x The input to iterate on. * @param {Function} callback * The function to call (as described above) on each entry of the input diff --git a/src/function/relational/compare.js b/src/function/relational/compare.js index 39886c3920..74bb74309b 100644 --- a/src/function/relational/compare.js +++ b/src/function/relational/compare.js @@ -58,6 +58,11 @@ export const createCompare = /* #__PURE__ */ factory(name, dependencies, ({ type * * equal, unequal, smaller, smallerEq, larger, largerEq, compareNatural, compareText * + * History: + * + * v0.19 Created + * v4 Changed to compare strings by numerical value + * * @param {number | BigNumber | bigint | Fraction | Unit | string | Array | Matrix} x First value to compare * @param {number | BigNumber | bigint | Fraction | Unit | string | Array | Matrix} y Second value to compare * @return {number | BigNumber | bigint | Fraction | Array | Matrix} Returns the result of the comparison: diff --git a/src/function/string/format.js b/src/function/string/format.js index 69e337ce62..69968a1a97 100644 --- a/src/function/string/format.js +++ b/src/function/string/format.js @@ -109,7 +109,6 @@ export const createFormat = /* #__PURE__ */ factory(name, dependencies, ({ typed * function formatCurrency(value) { * // return currency notation with two digits: * return '$' + value.toFixed(2) - * * // you could also use math.format inside the callback: * // return '$' + math.format(value, {notation: 'fixed', precision: 2}) * } @@ -119,6 +118,14 @@ export const createFormat = /* #__PURE__ */ factory(name, dependencies, ({ typed * * print * + * History: + * + * v0.4 Created + * v0.7 Round to a consistent number of digits (rather than decimals) + * v0.15 Added multiple number notations and configurable precision + * v3 Added support for JSON objects + * v9 Added binary, hexadecimal, and octal notations + * * @param {*} value Value to be stringified * @param {Object | Function | number} [options] Formatting options * @return {string} The formatted value diff --git a/src/function/utils/numeric.js b/src/function/utils/numeric.js index e9cb5a24f2..4858361f8b 100644 --- a/src/function/utils/numeric.js +++ b/src/function/utils/numeric.js @@ -3,12 +3,13 @@ import { factory } from '../../utils/factory.js' import { noBignumber, noFraction } from '../../utils/noop.js' const name = 'numeric' -const dependencies = ['number', '?bignumber', '?fraction'] +const dependencies = ['number', 'bigint', '?bignumber', '?fraction'] -export const createNumeric = /* #__PURE__ */ factory(name, dependencies, ({ number, bignumber, fraction }) => { +export const createNumeric = /* #__PURE__ */ factory(name, dependencies, ({ number, bigint, bignumber, fraction }) => { const validInputTypes = { string: true, number: true, + bigint: true, BigNumber: true, Fraction: true } @@ -19,7 +20,7 @@ export const createNumeric = /* #__PURE__ */ factory(name, dependencies, ({ numb BigNumber: bignumber ? (x) => bignumber(x) : noBignumber, - bigint: (x) => BigInt(x), + bigint: (x) => bigint(x, { round: 'throw', safe: false }), Fraction: fraction ? (x) => fraction(x) : noFraction @@ -46,6 +47,12 @@ export const createNumeric = /* #__PURE__ */ factory(name, dependencies, ({ numb * * number, fraction, bignumber, bigint, string, format * + * History: + * + * v6 Created + * v13 Added `bigint` support + * v14.2.1 Prefer mathjs `bigint()` to built-in `BigInt()` + * * @param {string | number | BigNumber | bigint | Fraction } value * A numeric value or a string containing a numeric value * @param {string} outputType diff --git a/src/type/bigint.js b/src/type/bigint.js index 2b5e90625c..9ea619e58e 100644 --- a/src/type/bigint.js +++ b/src/type/bigint.js @@ -2,29 +2,75 @@ import { factory } from '../utils/factory.js' import { deepMap } from '../utils/collection.js' const name = 'bigint' -const dependencies = ['typed'] +const dependencies = ['typed', 'isInteger', 'typeOf', 'round', 'floor', 'ceil', 'fix', '?bignumber'] -export const createBigint = /* #__PURE__ */ factory(name, dependencies, ({ typed }) => { +export const createBigint = /* #__PURE__ */ factory(name, dependencies, ({ typed, isInteger, typeOf, round, floor, ceil, fix, bignumber }) => { /** - * Create a bigint or convert a string, boolean, or unit to a bigint. + * Create a bigint or convert a string, boolean, or numeric type to a bigint. * When value is a matrix, all elements will be converted to bigint. * * Syntax: * + * math.bigint() * math.bigint(value) + * math.bigint(value, options) + * + * Where: + * + * - `value: *` + * The value to be converted to bigint. If omitted, defaults to 0. + * - `options: Object` + * A plain object with conversion options, including: + * - `safe: boolean` + * If true and _value_ is outside the range in which its type can + * uniquely represent each integer, the conversion throws an error. + * (Note that converting NaN or Infinity throws a RangeError in any + * case.) Defaults to false. + * - `round: string` + * How to handle non-integer _value_. Choose from: + * - `'throw'` -- if _value_ does not nominally represent an integer, + * throw a RangeError + * - `'round'` -- convert to the nearest bigint, rounding halves per + * the default behavior of `math.round`. This is the default value + * for `round`. + * - `'floor'` -- convert to the largest bigint less than _value_ + * - `'ceil'` -- convert to the smallest bigint greater than _value_ + * - `'fix'` -- convert to the nearest bigint closer to zero + * than _value_ * * Examples: * - * math.bigint(2) // returns 2n - * math.bigint('123') // returns 123n - * math.bigint(true) // returns 1n - * math.bigint([true, false, true, true]) // returns [1n, 0n, 1n, 1n] + * math.bigint(2) // returns 2n + * math.bigint('123') // returns 123n + * math.bigint(true) // returns 1n + * math.bigint([true, false, true, true]) // returns [1n, 0n, 1n, 1n] + * math.bigint(3**50) // returns 717897987691852578422784n + * // note inexactness above from number precision; actual 3n**50n is + * // 717897987691852588770249n + * math.bigint(3**50, {safe: true}) // throws RangeError + * math.bigint(math.pow(math.bignumber(11), 64)) // returns 4457915684525902395869512133369841539490161434991526715513934826000n + * // similarly inaccurate; last three digits should be 241 + * math.bigint( + * math.pow(math.bignumber(11), 64), + * {safe: true}) // throws RangeError + * math.bigint(math.fraction(13, 2)) // returns 7n + * math.bigint(6.5, {round: 'throw'}) // throws RangeError + * math.bigint(6.5, {round: 'floor'}) // returns 6n + * math.bigint(-6.5, {round: 'ceil'}) // returns -6n + * math.bigint(6.5, {round: 'fix'}) // returns 6n * * See also: * * number, bignumber, boolean, complex, index, matrix, string, unit + * round, floor, ceil, fix + * + * History: + * + * v13 Created + * v14.2.1 Added conversion options * * @param {string | number | BigNumber | bigint | Fraction | boolean | Array | Matrix | null} [value] Value to be converted + * @param {Object} [options] Conversion options with keys `safe` and/or `round` * @return {bigint | Array | Matrix} The created bigint */ const bigint = typed('bigint', { @@ -32,30 +78,34 @@ export const createBigint = /* #__PURE__ */ factory(name, dependencies, ({ typed return 0n }, - bigint: function (x) { - return x + null: function (x) { + return 0n }, - - number: function (x) { - return BigInt(x.toFixed()) + 'null, Object': function (x) { + return 0n }, - BigNumber: function (x) { - return BigInt(x.round().toString()) + bigint: function (x) { + return x }, - - Fraction: function (x) { - return BigInt(x.valueOf().toFixed()) + 'bigint, Object': function (x) { + // Options irrelevant because always safe and no rounding needed + return x }, - 'string | boolean': function (x) { + boolean: function (x) { return BigInt(x) }, - - null: function (x) { - return 0n + 'boolean, Object': function (x) { + return BigInt(x) }, + string: stringToBigint, + 'string, Object': stringToBigint, + + 'number | BigNumber | Fraction': numericToBigint, + 'number | BigNumber | Fraction, Object': numericToBigint, + 'Array | Matrix': typed.referToSelf(self => x => deepMap(x, self)) }) @@ -68,5 +118,88 @@ export const createBigint = /* #__PURE__ */ factory(name, dependencies, ({ typed return BigInt(json.value) } + const rounders = { round, floor, ceil, fix } + + function numericToBigint (value, options = {}) { + // fill in defaults + options = Object.assign({ safe: false, round: 'round' }, options) + const valType = typeOf(value) + if (options.safe) { + let upper = Number.MAX_SAFE_INTEGER + let lower = Number.MIN_SAFE_INTEGER + let unsafe = valType === 'number' && (value < lower || value > upper) + if (bignumber && valType === 'BigNumber') { + const digits = value.precision() + upper = bignumber(`1e${digits}`) + lower = bignumber(`-1e${digits}`) + if (value.lessThan(lower) || value.greaterThan(upper)) unsafe = true + } + if (unsafe) { + throw new RangeError( + `${valType} ${value} outside of safe range [${lower}, ${upper}] ` + + 'for conversion to bigint.') + } + } + if (!isInteger(value)) { + if (options.round === 'throw') { + throw new RangeError(`${value} is not an integer.`) + } + value = rounders[options.round](value) + } + // Now we have an integer that we are comfortable converting to bigint + if (valType === 'number') return BigInt(value) + if (valType === 'Fraction') return value.n * value.s + // Currently only BigNumbers left + return BigInt(value.toFixed()) + } + + function stringToBigint (value, options = {}) { + // safe option is irrelevant for string: + const round = options.round ?? 'round' + value = value.trim() + // Built in constructor works for integers in other bases: + if (/^0[box]/.test(value)) return BigInt(value) + + // Otherwise, have to parse ourselves, because BigInt() doesn't allow + // rounding; it throws on all decimals. + const match = value.match(/^([+-])?(\d*)([.,]\d*)?([eE][+-]?\d+)?$/) + if (!match) { + throw new SyntaxError('invalid BigInt syntax') + } + const sgn = match[1] === '-' ? -1n : 1n + let intPart = match[2] + let fracPart = match[3] ? match[3].substr(1) : '' + let expn = match[4] ? parseInt(match[4].substr(1)) : 0 + if (expn >= fracPart.length) { + intPart += fracPart + expn -= fracPart.length + intPart += '0'.repeat(expn) + } else if (expn > 0) { + intPart += fracPart.substr(0, expn) + fracPart = fracPart.substr(expn) + } else if (-expn > intPart.length) { + fracPart = intPart + fracPart + expn += intPart.length + fracPart = '0'.repeat(-expn) + fracPart + } else { // negative exponent smaller in magnitude than length of intPart + fracPart = intPart.substr(expn) + fracPart + intPart = intPart.substr(0, intPart.length + expn) + } + // Now expn is irrelevant, number is intPart.fracPart + if (/^0*$/.test(fracPart)) fracPart = '' + if (round === 'throw' && fracPart) { + throw new RangeError(`${value} is not an integer`) + } + const intVal = sgn * BigInt(intPart) + if (round === 'fix' || !fracPart) return intVal + const flr = sgn > 0 ? intVal : intVal - 1n + if (round === 'floor') return flr + if (round === 'ceil') return flr + 1n + // OK, round is 'round'. We proceed by the first digit of fracPart. + // 0-4 mean 'fix'; 5-9 'fix' + sgn. This is the half-round rule "away". + if (/[0-4]/.test(fracPart[0])) return intVal + return intVal + sgn + } + return bigint }) diff --git a/src/type/bignumber/function/bignumber.js b/src/type/bignumber/function/bignumber.js index a38a84ae28..3107fefe1b 100644 --- a/src/type/bignumber/function/bignumber.js +++ b/src/type/bignumber/function/bignumber.js @@ -16,7 +16,7 @@ export const createBignumber = /* #__PURE__ */ factory(name, dependencies, ({ ty * Examples: * * 0.1 + 0.2 // returns number 0.30000000000000004 - * math.bignumber(0.1) + math.bignumber(0.2) // returns BigNumber 0.3 + * math.add(math.bignumber(0.1), math.bignumber(0.2)) // returns BigNumber 0.3 * * * 7.2e500 // returns number Infinity diff --git a/src/type/boolean.js b/src/type/boolean.js index 7ac890c994..bda994aaec 100644 --- a/src/type/boolean.js +++ b/src/type/boolean.js @@ -27,7 +27,13 @@ export const createBoolean = /* #__PURE__ */ factory(name, dependencies, ({ type * * See also: * - * bignumber, complex, index, matrix, string, unit + * bigint, bignumber, complex, index, matrix, string, unit + * + * History: + * + * v0.11 Created + * v0.16 Added conversion from BigNumber + * v14.2.1 Added conversion from bigint * * @param {string | number | boolean | Array | Matrix | null} value A value of any type * @return {boolean | Array | Matrix} The boolean value @@ -45,6 +51,10 @@ export const createBoolean = /* #__PURE__ */ factory(name, dependencies, ({ type return !!x }, + bigint: function (x) { + return x !== 0n + }, + null: function (x) { return false }, diff --git a/src/type/complex/function/complex.js b/src/type/complex/function/complex.js index b94327e2b4..25d9130071 100644 --- a/src/type/complex/function/complex.js +++ b/src/type/complex/function/complex.js @@ -28,16 +28,25 @@ export const createComplex = /* #__PURE__ */ factory(name, dependencies, ({ type * * Examples: * - * const a = math.complex(3, -4) // a = Complex 3 - 4i - * a.re = 5 // a = Complex 5 - 4i - * const i = a.im // Number -4 - * const b = math.complex('2 + 6i') // Complex 2 + 6i - * const c = math.complex() // Complex 0 + 0i - * const d = math.add(a, b) // Complex 5 + 2i + * const a = math.complex(3, -4) + * a // Complex 3 - 4i + * a.re = 5 + * a // Complex 5 - 4i + * a.im // Number -4 + * const b = math.complex('2 + 6i') + * b // Complex 2 + 6i + * math.complex() // Complex 0 + 0i + * math.add(a, b) // Complex 7 + 2i * * See also: * - * bignumber, boolean, index, matrix, number, string, unit + * bigint, bignumber, boolean, index, matrix, number, string, unit + * + * History: + * + * v0.5 Created + * v0.16 Added conversion from BigNumber + * v6 Added conversion from Fraction * * @param {* | Array | Matrix} [args] * Arguments specifying the real and imaginary part of the complex number diff --git a/src/type/fraction/function/fraction.js b/src/type/fraction/function/fraction.js index e97e155a5d..eff4cd7dc1 100644 --- a/src/type/fraction/function/fraction.js +++ b/src/type/fraction/function/fraction.js @@ -30,12 +30,20 @@ export const createFraction = /* #__PURE__ */ factory(name, dependencies, ({ typ * math.fraction(1, 3) // returns Fraction 1/3 * math.fraction('2/3') // returns Fraction 2/3 * math.fraction({n: 2, d: 3}) // returns Fraction 2/3 - * math.fraction([0.2, 0.25, 1.25]) // returns Array [1/5, 1/4, 5/4] + * math.fraction([0.2, 0.25, 1.25]) // returns Array [fraction(1,5), fraction(1,4), fraction(5,4)] * math.fraction(4, 5.1) // throws Error: Parameters must be integer * * See also: * - * bignumber, number, string, unit + * bigint, bignumber, number, string, unit + * + * History: + * + * v2 Created + * v3 Added conversion from BigNumber + * v11.8 Added conversion from Unit + * v13 Added conversion from bigint + * v14 Move to bigint-based fraction.js@5; construct from two bigints * * @param {number | string | Fraction | BigNumber | bigint | Unit | Array | Matrix} [args] * Arguments specifying the value, or numerator and denominator of diff --git a/src/type/matrix/function/index.js b/src/type/matrix/function/index.js index a5540527f0..61a80398bf 100644 --- a/src/type/matrix/function/index.js +++ b/src/type/matrix/function/index.js @@ -31,12 +31,20 @@ export const createIndex = /* #__PURE__ */ factory(name, dependencies, ({ typed, * * const a = math.matrix([[1, 2], [3, 4]]) * a.subset(math.index(0, 1)) // returns 2 - * a.subset(math.index(0, [false, true])) // returns 2 + * a.subset(math.index(0, [false, true])) // returns [2] * * See also: * * bignumber, boolean, complex, matrix, number, string, unit * + * History: + * + * v? Created + * v2 Dropped support for `[start, end, step]` arguments in favor + * of lists of arbitrary values; added support for Range + * objects. + * v11.10 Added support for arrays of booleans as indices. + * * @param {...*} ranges Zero or more ranges or numbers. * @return {Index} Returns the created index */ diff --git a/src/type/matrix/function/matrix.js b/src/type/matrix/function/matrix.js index 385301fc3d..cc42f63285 100644 --- a/src/type/matrix/function/matrix.js +++ b/src/type/matrix/function/matrix.js @@ -29,7 +29,13 @@ export const createMatrix = /* #__PURE__ */ factory(name, dependencies, ({ typed * * See also: * - * bignumber, boolean, complex, index, number, string, unit, sparse + * bigint, bignumber, boolean, complex, index, number, string, unit, sparse + * + * History: + * + * v0.5 Created + * v1.5 Support dense or sparse Matrix types; allow construction + * from string, Array, or another Matrix. * * @param {Array | Matrix} [data] A multi dimensional array * @param {string} [format] The Matrix storage format, either `'dense'` or `'sparse'` diff --git a/src/type/matrix/function/sparse.js b/src/type/matrix/function/sparse.js index 05941b8cc1..f9f14391a4 100644 --- a/src/type/matrix/function/sparse.js +++ b/src/type/matrix/function/sparse.js @@ -24,7 +24,7 @@ export const createSparse = /* #__PURE__ */ factory(name, dependencies, ({ typed * m.size() // Array [2, 2] * m.resize([3, 2], 5) * m.valueOf() // Array [[1, 2], [3, 4], [5, 5]] - * m.get([1, 0]) // number 3 + * m.get([1, 0]) // number 3 * let v = math.sparse([0, 0, 1]) * v.size() // Array [3, 1] * v.get([2, 0]) // number 1 @@ -33,6 +33,10 @@ export const createSparse = /* #__PURE__ */ factory(name, dependencies, ({ typed * * bignumber, boolean, complex, index, number, string, unit, matrix * + * History: + * + * v1.5 Created + * * @param {Array | Matrix} [data] A two dimensional array * * @return {Matrix} The created matrix diff --git a/src/type/string.js b/src/type/string.js index 85f57ff81c..18c6f8a5c1 100644 --- a/src/type/string.js +++ b/src/type/string.js @@ -16,17 +16,21 @@ export const createString = /* #__PURE__ */ factory(name, dependencies, ({ typed * * Examples: * - * math.string(4.2) // returns string '4.2' - * math.string(math.complex(3, 2) // returns string '3 + 2i' + * math.string(4.2) // returns string '4.2' + * math.string(math.complex(3, 2)) // returns string '3 + 2i' * * const u = math.unit(5, 'km') - * math.string(u.to('m')) // returns string '5000 m' + * math.string(u.to('m')) // returns string '5000 m' * - * math.string([true, false]) // returns ['true', 'false'] + * math.string([true, false]) // returns ['true', 'false'] * * See also: * - * bignumber, boolean, complex, index, matrix, number, unit + * bigint, bignumber, boolean, complex, index, matrix, number, unit + * + * History: + * + * v0.9 Created * * @param {* | Array | Matrix | null} [value] A value to convert to a string * @return {string | Array | Matrix} The created string diff --git a/src/type/unit/function/unit.js b/src/type/unit/function/unit.js index bae05a6b79..3930ad1645 100644 --- a/src/type/unit/function/unit.js +++ b/src/type/unit/function/unit.js @@ -19,15 +19,24 @@ export const createUnitFunction = /* #__PURE__ */ factory(name, dependencies, ({ * * Examples: * - * const kph = math.unit('km/h') // returns Unit km/h (valueless) - * const v = math.unit(25, kph) // returns Unit 25 km/h - * const a = math.unit(5, 'cm') // returns Unit 50 mm - * const b = math.unit('23 kg') // returns Unit 23 kg + * math.unit('23 kg') // returns Unit 23 kg + * // Valueless Units can be used to specify the unit type: + * const kph = math.unit('km/h') + * math.unit(25, kph) // returns Unit 25 km/h + * const a = math.unit(5, 'cm') * a.to('m') // returns Unit 0.05 m * * See also: * - * bignumber, boolean, complex, index, matrix, number, string, createUnit + * bigint, bignumber, boolean, complex, index, matrix, number, string, createUnit + * + * History: + * + * v0.5 Created + * v0.16 Support conversion from BigNumber + * v2.5 Support BigNumber and Fraction values in units + * v2.6 Support Complex values in units + * v11.1 Allow the type of unit to be specifed by a unit (not just string) * * @param {* | Array | Matrix} args A number and unit. * @return {Unit | Array | Matrix} The created unit diff --git a/test/node-tests/doc.test.js b/test/node-tests/doc.test.js index b5995a42d6..beac2b7490 100644 --- a/test/node-tests/doc.test.js +++ b/test/node-tests/doc.test.js @@ -5,8 +5,14 @@ import { approxEqual, approxDeepEqual } from '../../tools/approx.js' import { collectDocs } from '../../tools/docgenerator.js' import { create, all } from '../../lib/esm/index.js' +// Really stupid mock of the numbers module, for the core import.js doc test: +const numbers = { + fibonacci: x => 13 +} +numbers.useItForLint = true + const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const math = create(all) +let math = create(all) const debug = process.argv.includes('--debug-docs') function extractExpectation (comment, optional = false) { @@ -40,7 +46,8 @@ function extractValue (spec) { } const keywords = { number: 'Number(_)', - BigNumber: 'math.bignumber(_)', + Number: 'Number(_)', + BigNumber: "math.bignumber('_')", Fraction: 'math.fraction(_)', Complex: "math.complex('_')", Unit: "math.unit('_')", @@ -217,9 +224,7 @@ const knownUndocumented = new Set([ 'off', 'once', 'emit', - 'config', 'expression', - 'import', 'create', 'factory', 'AccessorNode', @@ -228,16 +233,12 @@ const knownUndocumented = new Set([ 'atomicMass', 'avogadro', 'BigNumber', - 'bignumber', 'BlockNode', 'bohrMagneton', 'bohrRadius', 'boltzmann', - 'boolean', - 'chain', 'Chain', 'classicalElectronRadius', - 'complex', 'Complex', 'ConditionalNode', 'conductanceQuantum', @@ -256,7 +257,6 @@ const knownUndocumented = new Set([ 'fermiCoupling', 'fineStructure', 'firstRadiation', - 'fraction', 'Fraction', 'FunctionAssignmentNode', 'FunctionNode', @@ -267,7 +267,6 @@ const knownUndocumented = new Set([ 'Help', 'i', 'ImmutableDenseMatrix', - 'index', 'Index', 'IndexNode', 'Infinity', @@ -280,7 +279,6 @@ const knownUndocumented = new Set([ 'loschmidt', 'magneticConstant', 'magneticFluxQuantum', - 'matrix', 'Matrix', 'molarMass', 'molarMassC12', @@ -291,12 +289,9 @@ const knownUndocumented = new Set([ 'Node', 'nuclearMagneton', 'null', - 'number', - 'bigint', 'ObjectNode', 'OperatorNode', 'ParenthesisNode', - 'parse', 'Parser', 'phi', 'pi', @@ -321,19 +316,15 @@ const knownUndocumented = new Set([ 'sackurTetrode', 'secondRadiation', 'Spa', - 'sparse', 'SparseMatrix', 'speedOfLight', 'splitUnit', 'stefanBoltzmann', - 'string', 'SymbolNode', 'tau', 'thomsonCrossSection', 'true', - 'typed', 'Unit', - 'unit', 'E', 'PI', 'vacuumImpedance', @@ -371,6 +362,7 @@ describe('Testing examples from (jsdoc) comments', function () { describe('category: ' + category, function () { for (const doc of byCategory[category]) { it('satisfies ' + doc.name, function () { + math = create(all) if (debug) { console.log(` Testing ${doc.name} ...`) // can remove once no known failures; for now it clarifies "PLEASE RESOLVE" } @@ -399,6 +391,7 @@ describe('Testing examples from (jsdoc) comments', function () { expectation = extractExpectation(expectationFrom) parts[1] = '' } + let clearAccumulation = false if (accumulation && !accumulation.includes('console.log(')) { // note: we ignore examples that contain a console.log to keep the output of the tests clean let value @@ -406,10 +399,11 @@ describe('Testing examples from (jsdoc) comments', function () { value = eval(accumulation) // eslint-disable-line no-eval } catch (err) { value = err.toString() + clearAccumulation = true } maybeCheckExpectation( doc.name, expectation, expectationFrom, value, accumulation) - accumulation = '' + if (clearAccumulation) accumulation = '' } expectationFrom = parts[1] expectation = extractExpectation(expectationFrom, 'requireSignal') @@ -417,6 +411,8 @@ describe('Testing examples from (jsdoc) comments', function () { if (line !== '') { if (accumulation) { accumulation += '\n' } accumulation += line + } else { + accumulation = '' } } } diff --git a/test/unit-tests/type/bigint.test.js b/test/unit-tests/type/bigint.test.js index 3a5d536f77..456962f98c 100644 --- a/test/unit-tests/type/bigint.test.js +++ b/test/unit-tests/type/bigint.test.js @@ -20,6 +20,9 @@ describe('bigint', function () { it('should convert a BigNumber to a bigint', function () { assert.strictEqual(bigint(math.bignumber('123')), 123n) assert.strictEqual(bigint(math.bignumber('2.3')), 2n) + const bigString = '123456789012345678901234567890' + const bigi = BigInt(bigString) + assert.strictEqual(bigint(math.bignumber(bigString)), bigi) }) it('should convert a number to a bigint', function () { @@ -29,11 +32,22 @@ describe('bigint', function () { it('should convert a Fraction to a bigint', function () { assert.strictEqual(bigint(math.fraction(7, 3)), 2n) + assert.strictEqual(bigint(math.fraction(27.5)), 28n) + assert.strictEqual( + bigint(math.fraction('123456789012345678901234567890123456789/2')), + 61728394506172839450617283945061728395n + ) + assert.strictEqual( + bigint(math.fraction('1234567890123456789012345678901234567890/2')), + 617283945061728394506172839450617283945n + ) }) it('should accept a bigint as argument', function () { assert.strictEqual(bigint(3n), 3n) assert.strictEqual(bigint(-3n), -3n) + const big = 12345678901234567890n + assert.strictEqual(bigint(big), big) }) it('should parse the string if called with a valid string', function () { @@ -41,14 +55,65 @@ describe('bigint', function () { assert.strictEqual(bigint(' -2100 '), -2100n) assert.strictEqual(bigint(''), 0n) assert.strictEqual(bigint(' '), 0n) + assert.strictEqual(bigint('2.3'), 2n) + assert.strictEqual(bigint('-237503.6437e3'), -237503644n) }) it('should throw an error if called with an invalid string', function () { - assert.throws(function () { bigint('2.3') }, SyntaxError) + assert.throws( + function () { bigint('2.3', { round: 'throw' }) }, + RangeError + ) assert.throws(function () { bigint('2.3.4') }, SyntaxError) assert.throws(function () { bigint('23a') }, SyntaxError) }) + it('should respect the safe option', function () { + const bigsafe = val => bigint(val, { safe: true }) + assert.throws(() => bigsafe(3 ** 50), RangeError) + assert.throws(() => bigsafe((-5) ** 49), RangeError) + assert.throws(() => bigsafe(math.bignumber(11).pow(64)), RangeError) + assert.throws(() => bigsafe(math.bignumber(-12).pow(63)), RangeError) + assert.strictEqual( + bigsafe(Number.MAX_SAFE_INTEGER - 1), + BigInt(Number.MAX_SAFE_INTEGER) - 1n) + assert.strictEqual( + bigsafe(Number.MIN_SAFE_INTEGER + 1), + BigInt(Number.MIN_SAFE_INTEGER) + 1n) + const bigPosString = '9'.repeat(63) + assert.strictEqual( + bigsafe(math.bignumber(bigPosString)), BigInt(bigPosString)) + const bigNegString = '-' + bigPosString + assert.strictEqual( + bigsafe(math.bignumber(bigNegString)), BigInt(bigNegString)) + }) + + it('should respect round: throw', function () { + const bigthrow = val => bigint(val, { round: 'throw' }) + assert.throws(() => bigthrow(27.5), RangeError) + assert.throws( + () => bigthrow(math.bignumber(3).pow(32).dividedBy(2)), RangeError) + assert.throws(() => { + return bigthrow( + math.fraction('123456789012345678901234567890123456789/2')) + }, RangeError) + assert.strictEqual(bigthrow(2 ** 60), 2n ** 60n) + assert.strictEqual(bigthrow(math.bignumber('1e70')), 10n ** 70n) + assert.strictEqual( + bigthrow(math.fraction('1234567890123456789012345678901234567890/2')), + 617283945061728394506172839450617283945n + ) + }) + + it('should allow different rounding modes', function () { + assert.strictEqual(bigint(math.fraction(37, 2), { round: 'floor' }), 18n) + assert.strictEqual(bigint(-27.5, { round: 'ceil' }), -27n) + assert.strictEqual( + bigint(math.bignumber('-12345678901234567890.5'), { round: 'fix' }), + -12345678901234567890n) + assert.strictEqual(bigint(math.fraction(-37, 2), { round: 'round' }), -18n) + }) + it('should convert the elements of a matrix to numbers', function () { assert.deepStrictEqual(bigint(math.matrix(['123', true])), math.matrix([123n, 1n])) }) @@ -58,7 +123,8 @@ describe('bigint', function () { }) it('should throw an error if called with a wrong number of arguments', function () { - assert.throws(function () { bigint(1, 2, 3) }, /TypeError: Too many arguments/) + assert.throws(function () { bigint(1, 2, 3) }, TypeError) + assert.throws(function () { bigint(1, {}, 3) }, /TypeError: Too many arguments/) }) it('should throw an error if called with a complex number', function () { diff --git a/test/unit-tests/type/numeric.test.js b/test/unit-tests/type/numeric.test.js index 5b3eb82424..04fed9ee4f 100644 --- a/test/unit-tests/type/numeric.test.js +++ b/test/unit-tests/type/numeric.test.js @@ -85,6 +85,30 @@ describe('numeric', function () { assert.throws(function () { numeric(math.complex(2, 3), 'number') }, TypeError) }) + it('should convert various types to bigint', function () { + const tobi = x => numeric(x, 'bigint') + const big = 12345678901234567890n + const bigs = big.toString() + assert.strictEqual(tobi('-5723'), -5723n) + assert.strictEqual(tobi(bigs), big) + assert.strictEqual(tobi(2e10), 20000000000n) + assert.strictEqual(tobi(big), big) + assert.strictEqual(tobi(math.bignumber(bigs)), big) + assert.strictEqual( + tobi(math.bignumber('123456789012345678901234567890')), + 123456789012345678901234567890n) + assert.strictEqual(tobi(math.fraction(18, -3)), -6n) + assert.strictEqual( + tobi(math.fraction('1234567890123456789012345678901234567890/2')), + 617283945061728394506172839450617283945n + ) + + assert.throws(() => tobi(math.bignumber(27.4)), RangeError) + assert.throws( + () => tobi(math.fraction('123456789012345678901234567890123456789/2')), + RangeError) + }) + it('should LaTeX numeric', function () { const expr1 = math.parse('numeric(3.14, "number")') const expr2 = math.parse('numeric("3.141592653589793238462643383279501", "BigNumber")') diff --git a/tools/docgenerator.js b/tools/docgenerator.js index de962797c9..c4f672f1b9 100644 --- a/tools/docgenerator.js +++ b/tools/docgenerator.js @@ -191,6 +191,29 @@ export function generateDoc (name, code) { return false } + function parseHistory () { + if (/^history/i.test(line)) { + next() + skipEmptyLines() + + while (exists() && !empty()) { + let [_all, indent, version, entry] = line.match(/^(\s*)(\S*)\s+(.*)$/) + const moreIndent = indent + ' ' + next() + while (exists() && !empty() && line.startsWith(moreIndent)) { + entry += ' ' + line.trim() + next() + } + doc.history[version] = entry + } + + skipEmptyLines() + + return true + } + return false + } + function parseExamples () { if (/^example/i.test(line)) { next() @@ -338,6 +361,7 @@ export function generateDoc (name, code) { description: '', syntax: [], where: [], + history: {}, examples: [], seeAlso: [], parameters: [], @@ -354,6 +378,7 @@ export function generateDoc (name, code) { const handled = parseSyntax() || parseWhere() || + parseHistory() || parseExamples() || parseSeeAlso() || parseParameters() || @@ -518,6 +543,15 @@ export function generateMarkdown (doc, functions) { '\n' } + if (Object.keys(doc.history).length > 0) { + text += '## History\n\n' + + 'Version | Comment\n' + + '------- | -------\n' + for (const version in doc.history) { + text += `${version} | ${doc.history[version]}\n` + } + } + return text } @@ -567,19 +601,22 @@ export function collectDocs (functionNames, inputPath) { if (!path.includes('docs') && functionIndex !== -1) { if (path.includes('expression')) { category = 'expression' - } else if (/\/lib\/cjs\/type\/[a-zA-Z0-9_]*\/function/.test(fullPath)) { - // for type/bignumber/function/bignumber.js, type/fraction/function/fraction.js, etc - category = 'construction' - } else if (/\/lib\/cjs\/core\/function/.test(fullPath)) { - category = 'core' + } else if (functionIndex == path.length - 1) { + if (path[functionIndex - 1] === 'core') { + category = 'core' + } else { + // for type/bignumber/function/bignumber.js, type/fraction/function/fraction.js, etc + category = 'construction' + } } else { + // typical case, e.g. src/function/algebra/lsolve.js category = path[functionIndex + 1] } - } else if (fullPath.endsWith('/lib/cjs/expression/parse.js')) { + } else if (path[path.length - 1] === 'expression' && name === 'parse') { // TODO: this is an ugly special case category = 'expression' - } else if (path.join('/').endsWith('/lib/cjs/type')) { - // for boolean.js, number.js, string.js + } else if (path[path.length - 1] === 'type') { + // for boolean.js, number.js, string.js, bigint.js category = 'construction' }