diff --git a/docs/datatypes/index.md b/docs/datatypes/index.md index 328422ed11..d9bf90dfdc 100644 --- a/docs/datatypes/index.md +++ b/docs/datatypes/index.md @@ -9,6 +9,7 @@ The supported data types are: - Boolean - [Number](numbers.md) +- [BigInt](bigints.md) - [BigNumber](bignumbers.md) - [Complex](complex_numbers.md) - [Fraction](fractions.md) @@ -20,7 +21,75 @@ The supported data types are: Function [`math.typeOf(x)`](../reference/functions/typeOf.md) can be used to get the type of a variable. -Example usage: +## Type conversions + +### Implicit + +For convenience, mathjs will automatically attempt certain type conversions +on the actual arguments to its functions, in order to match the declared +parameter types of the function. For example, there is such an implicit +conversion of boolean values to number values, that takes `false` to 0 and +`true` to 1. Therefore, the invocation `math.add(3, true)` matches the mathjs +add function with two parameters of type `number`, and returns the value 4. + +Note that booleans will implicitly convert to any other scalar type, +and strings will implicitly convert to any other scalar type except boolean, +by interpreting the string as the printed representation of a numerical value. +And here is an ASCII art diagram of all other implicit conversions in effect +as of mathjs 14: + +``` + /------------> Fraction + / / \ + BigInt----------\/ \ + \ /-> BigNumber / + -> Number | / + / \ v L +Boolean \--> Complex + + Array <--> Matrix +``` + +(Note that the diagram is not "transitive", or in other words, even though +there is an implicit conversion from Boolean to Number and Number to Complex, +there is _not_ any implicit conversion from Boolean to Complex.) + +All of these implicit conversions are "safe" in that they will throw an +error if performing them would cause either a loss of precision (e.g., +losing significant digits when converting a 20-digit bigint to a number), +or an illusory gain in precision (e.g., converting a number with 16 decimal +places, the last one or two of which are likely numerically approximate, +into a bignumber that purports to have 64 digits of precision). + +### Explicit + +In addition, for each type, there is an explicit function that serves as +a way to "construct" instances of that type, or equivalently, request +explicit conversion of any other type into that type. The name of this +constructor/converter is always the name of the type with all letters +changed to lower case. + +Note that such an explicit conversion will throw an error if it would +discard some portion of the content of the value to be converted. So for +example, converting `'6.2831853'` to `number` will succeed, but +`'6.2831853 ~= tau'` to `number` will fail because the non-numeric +characters would be discarded. Similarly, `bigint(complex(-12, 0))` will +succeed and return `-12n` because there is no imaginary part to "-12 + 0i", +but `bigint(complex(-12, 3))` will throw an error because the imaginary +part "3i" would be discarded. + +Otherwise, the explicit conversions are by default "unsafe", in that they +will produce the value of the requested type that most closely approximates +the numeric value of the supplied argument, even if rounding must occur or +(apparent) precision is lost or gained. Thus, `bigint(6.283)` will return +`6n`. However, you may supply an options object as a final argument to the +conversion, and if it includes the key "safe" with a true value, a safe +conversion, equivalent to that used in implicit conversion, will be +performed instead. See the documentation pages for the individual +constructor functions for details on any other options available in +specific cases. + +## Examples of using types with mathjs functions: ```js // use numbers @@ -35,11 +104,11 @@ math.add(math.bignumber(0.1), math.bignumber(0.2)) // BigNumber, 0.3 math.add(300000000000000000n, 1n) // 300000000000000001n // use Fractions -math.add(math.fraction(1), math.fraction(3)) // Fraction, 0.(3) +math.divide(math.fraction(1), math.fraction(3)) // Fraction, 0.(3) // use strings -math.add('hello ', 'world') // 'hello world' -math.max('A', 'D', 'C') // 'D' +math.concat('hello ', 'world') // 'hello world' +math.sort(['A', 'D', 'C'], 'natural')[2] // 'D' // use complex numbers const a = math.complex(2, 3) // 2 + 3i @@ -52,11 +121,11 @@ math.sqrt(-4) // 2i // use arrays const array = [1, 2, 3, 4, 5] math.factorial(array) // Array, [1, 2, 6, 24, 120] -math.add(array, 3) // Array, [3, 5, 6, 7, 8] +math.add(array, 3) // Array, [4, 5, 6, 7, 8] // use matrices const matrix = math.matrix([1, 4, 9, 16, 25]) // Matrix, [1, 4, 9, 16, 25] -math.sqrt(matrix) // Matrix, [1, 2, 3, 4, 5] +math.map(matrix, math.sqrt) // Matrix, [1, 2, 3, 4, 5] // use units const a = math.unit(55, 'cm') // 550 mm @@ -67,4 +136,18 @@ math.add(a, b) // 0.65 m math.typeOf(2) // 'number' math.typeOf(math.unit('2 inch')) // 'Unit' math.typeOf(math.sqrt(-4)) // 'Complex' + +// bigints implicitly convert to numbers (for example): +math.add(6.283, 3n) // 9.283 +math.sqrt(20000n) // 141.42135623731 +// But they guard against accidentally losing precision: +math.sqrt(12345678901234567890n) // throws "value exceeds the max safe integer" + +// You can request explicit conversion +math.add(math.bigint(6.283), 3n) // 9n +// And such explicit requests are unsafe by default: +math.sqrt(math.number(12345678901234567890n)) // 3.5136418288201e+9 +// But you can turn safety back on: +math.sqrt(math.number(12345678901234567890n, {safe: true})) + // throws "value exceed the max safe integer" ``` diff --git a/src/core/function/import.js b/src/core/function/import.js index 20cd82173f..42a50e8d46 100644 --- a/src/core/function/import.js +++ b/src/core/function/import.js @@ -45,17 +45,20 @@ export function importFactory (typed, load, math, importedFactories) { * return 'hello, ' + name + '!' * } * }) - * * // 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 + * * @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 8eab0230db..01367003ec 100644 --- a/src/expression/parse.js +++ b/src/expression/parse.js @@ -66,14 +66,17 @@ 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: * diff --git a/src/function/algebra/rationalize.js b/src/function/algebra/rationalize.js index 4c901ee08f..f16e73d097 100644 --- a/src/function/algebra/rationalize.js +++ b/src/function/algebra/rationalize.js @@ -75,24 +75,24 @@ export const createRationalize = /* #__PURE__ */ factory(name, dependencies, ({ * Examples: * * math.rationalize('sin(x)+y') - * // Error: There is an unsolved function call - * math.rationalize('2x/y - y/(x+1)') - * // (2*x^2-y^2+2*x)/(x*y+y) - * math.rationalize('(2x+1)^6') - * // 64*x^6+192*x^5+240*x^4+160*x^3+60*x^2+12*x+1 + * // throws Error: There is an unsolved function call + * math.rationalize('2x/y - y/(x+1)') // (2*x^2-y^2+2*x)/(x*y+y) + * // math.rationalize('(2x+1)^6') // hangs, see #3351 + * // returns 64*x^6+192*x^5+240*x^4+160*x^3+60*x^2+12*x+1 * math.rationalize('2x/( (2x-1) / (3x+2) ) - 5x/ ( (3x+4) / (2x^2-5) ) + 3') - * // -20*x^4+28*x^3+104*x^2+6*x-12)/(6*x^2+5*x-4) - * math.rationalize('x/(1-x)/(x-2)/(x-3)/(x-4) + 2x/ ( (1-2x)/(2-3x) )/ ((3-4x)/(4-5x) )') = - * // (-30*x^7+344*x^6-1506*x^5+3200*x^4-3472*x^3+1846*x^2-381*x)/ - * // (-8*x^6+90*x^5-383*x^4+780*x^3-797*x^2+390*x-72) + * // returns -20*x^4+28*x^3+104*x^2+6*x-12)/(6*x^2+5*x-4) + * // math.rationalize('x/(1-x)/(x-2)/(x-3)/(x-4) + 2x/ ( (1-2x)/(2-3x) )/ ((3-4x)/(4-5x) )') // hangs, see #3351 + * // returns (-30*x^7+344*x^6-1506*x^5+3200*x^4-3472*x^3+1846*x^2-381*x)/(-8*x^6+90*x^5-383*x^4+780*x^3-797*x^2+390*x-72) * * math.rationalize('x+x+x+y',{y:1}) // 3*x+1 * math.rationalize('x+x+x+y',{}) // 3*x+y * - * const ret = math.rationalize('x+x+x+y',{},true) - * // ret.expression=3*x+y, ret.variables = ["x","y"] - * const ret = math.rationalize('-2+5x^2',{},true) - * // ret.expression=5*x^2-2, ret.variables = ["x"], ret.coefficients=[-2,0,5] + * const r = math.rationalize('x+x+x+y',{},true); + * [r.expression.toString(), r.variables] // ['3 * x + y', ['x', 'y']] + * + * const ret = math.rationalize('-2+5x^2',{},true); + * [ret.expression.toString(), ret.variables, ret.coefficients] + * // returns ['5 * x ^ 2 - 2', ['x'], [-2,0,5]] * * See also: * diff --git a/src/function/algebra/solver/lsolve.js b/src/function/algebra/solver/lsolve.js index 49396d0959..17e5255f5a 100644 --- a/src/function/algebra/solver/lsolve.js +++ b/src/function/algebra/solver/lsolve.js @@ -28,7 +28,7 @@ export const createLsolve = /* #__PURE__ */ factory(name, dependencies, ({ typed * * const a = [[-2, 3], [2, 1]] * const b = [11, 9] - * const x = lsolve(a, b) // [[-5.5], [20]] + * math.lsolve(a, b) // [[-5.5], [20]] * * See also: * diff --git a/src/function/algebra/solver/lsolveAll.js b/src/function/algebra/solver/lsolveAll.js index e956329695..964cc42f03 100644 --- a/src/function/algebra/solver/lsolveAll.js +++ b/src/function/algebra/solver/lsolveAll.js @@ -28,7 +28,7 @@ export const createLsolveAll = /* #__PURE__ */ factory(name, dependencies, ({ ty * * const a = [[-2, 3], [2, 1]] * const b = [11, 9] - * const x = lsolveAll(a, b) // [ [[-5.5], [20]] ] + * math.lsolveAll(a, b) // [ [[-5.5], [20]] ] * * See also: * diff --git a/src/function/algebra/solver/usolve.js b/src/function/algebra/solver/usolve.js index cf5bf8b589..79f28afd66 100644 --- a/src/function/algebra/solver/usolve.js +++ b/src/function/algebra/solver/usolve.js @@ -28,7 +28,7 @@ export const createUsolve = /* #__PURE__ */ factory(name, dependencies, ({ typed * * const a = [[-2, 3], [2, 1]] * const b = [11, 9] - * const x = usolve(a, b) // [[8], [9]] + * math.usolve(a, b) // [[8], [9]] * * See also: * diff --git a/src/function/algebra/solver/usolveAll.js b/src/function/algebra/solver/usolveAll.js index 2761afbefc..6625793b5f 100644 --- a/src/function/algebra/solver/usolveAll.js +++ b/src/function/algebra/solver/usolveAll.js @@ -28,7 +28,7 @@ export const createUsolveAll = /* #__PURE__ */ factory(name, dependencies, ({ ty * * const a = [[-2, 3], [2, 1]] * const b = [11, 9] - * const x = usolveAll(a, b) // [ [[8], [9]] ] + * math.usolveAll(a, b) // [ [[8], [9]] ] * * See also: * diff --git a/src/function/arithmetic/add.js b/src/function/arithmetic/add.js index b9bc0ae9af..34b6b41d60 100644 --- a/src/function/arithmetic/add.js +++ b/src/function/arithmetic/add.js @@ -45,7 +45,7 @@ export const createAdd = /* #__PURE__ */ factory( * * const c = math.unit('5 cm') * const d = math.unit('2.1 mm') - * math.add(c, d) // returns Unit 52.1 mm + * math.add(c, d) // returns Unit 5.21 cm * * math.add("2.3", "4") // returns number 6.3 * diff --git a/src/function/arithmetic/cbrt.js b/src/function/arithmetic/cbrt.js index ae71eca054..501348d482 100644 --- a/src/function/arithmetic/cbrt.js +++ b/src/function/arithmetic/cbrt.js @@ -37,11 +37,8 @@ export const createCbrt = /* #__PURE__ */ factory(name, dependencies, ({ config, * * const x = math.complex('8i') * math.cbrt(x) // returns Complex 1.7320508075689 + i - * math.cbrt(x, true) // returns Matrix [ - * // 1.7320508075689 + i - * // -1.7320508075689 + i - * // -2i - * // ] + * math.cbrt(x, true) + * // returns [1.7320508075689 + i, -1.7320508075689 + i, -2i] * * See also: * diff --git a/src/function/arithmetic/dotDivide.js b/src/function/arithmetic/dotDivide.js index 3587faf762..13cbfed6cf 100644 --- a/src/function/arithmetic/dotDivide.js +++ b/src/function/arithmetic/dotDivide.js @@ -37,9 +37,8 @@ export const createDotDivide = /* #__PURE__ */ factory(name, dependencies, ({ ty * * math.dotDivide(2, 4) // returns 0.5 * - * a = [[9, 5], [6, 1]] - * b = [[3, 2], [5, 2]] - * + * const a = [[9, 5], [6, 1]] + * const b = [[3, 2], [5, 2]] * math.dotDivide(a, b) // returns [[3, 2.5], [1.2, 0.5]] * math.divide(a, b) // returns [[1.75, 0.75], [-1.75, 2.25]] * diff --git a/src/function/arithmetic/dotMultiply.js b/src/function/arithmetic/dotMultiply.js index d49287bc6b..5c0502cd8b 100644 --- a/src/function/arithmetic/dotMultiply.js +++ b/src/function/arithmetic/dotMultiply.js @@ -31,9 +31,8 @@ export const createDotMultiply = /* #__PURE__ */ factory(name, dependencies, ({ * * math.dotMultiply(2, 4) // returns 8 * - * a = [[9, 5], [6, 1]] - * b = [[3, 2], [5, 2]] - * + * const a = [[9, 5], [6, 1]] + * const b = [[3, 2], [5, 2]] * math.dotMultiply(a, b) // returns [[27, 10], [30, 2]] * math.multiply(a, b) // returns [[52, 28], [23, 14]] * diff --git a/src/function/arithmetic/log.js b/src/function/arithmetic/log.js index 3dadeaa40e..c707da2341 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) @@ -38,7 +45,8 @@ export const createLog = /* #__PURE__ */ factory(name, dependencies, ({ typed, t * 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/logical/and.js b/src/function/logical/and.js index 827751d042..f1455b452d 100644 --- a/src/function/logical/and.js +++ b/src/function/logical/and.js @@ -35,10 +35,9 @@ export const createAnd = /* #__PURE__ */ factory(name, dependencies, ({ typed, m * * math.and(2, 4) // returns true * - * a = [2, 0, 0] - * b = [3, 7, 0] - * c = 0 - * + * const a = [2, 0, 0] + * const b = [3, 7, 0] + * const c = 0 * math.and(a, b) // returns [true, false, false] * math.and(a, c) // returns [false, false, false] * @@ -72,8 +71,8 @@ export const createAnd = /* #__PURE__ */ factory(name, dependencies, ({ typed, m 'SparseMatrix, any': typed.referToSelf(self => (x, y) => { // check scalar if (not(y)) { - // return zero matrix - return zeros(x.size(), x.storage()) + // return all false matrix + return matrix([], x.storage()).resize(x.size(), false) } return matAlgo11xS0s(x, y, self, false) }), @@ -81,8 +80,8 @@ export const createAnd = /* #__PURE__ */ factory(name, dependencies, ({ typed, m 'DenseMatrix, any': typed.referToSelf(self => (x, y) => { // check scalar if (not(y)) { - // return zero matrix - return zeros(x.size(), x.storage()) + // return all false matrix + return matrix().resize(x.size(), false) } return matAlgo14xDs(x, y, self, false) }), diff --git a/src/function/logical/not.js b/src/function/logical/not.js index 3706f00b4b..817e7f609f 100644 --- a/src/function/logical/not.js +++ b/src/function/logical/not.js @@ -20,7 +20,7 @@ export const createNot = /* #__PURE__ */ factory(name, dependencies, ({ typed }) * math.not(0) // returns true * math.not(true) // returns false * - * a = [2, -7, 0] + * const a = [2, -7, 0] * math.not(a) // returns [false, false, true] * * See also: diff --git a/src/function/logical/or.js b/src/function/logical/or.js index 12d6c5b48d..2e0495f7fe 100644 --- a/src/function/logical/or.js +++ b/src/function/logical/or.js @@ -32,10 +32,9 @@ export const createOr = /* #__PURE__ */ factory(name, dependencies, ({ typed, ma * * math.or(2, 4) // returns true * - * a = [2, 5, 0] - * b = [0, 22, 0] - * c = 0 - * + * const a = [2, 5, 0] + * const b = [0, 22, 0] + * const c = 0 * math.or(a, b) // returns [true, true, false] * math.or(b, c) // returns [false, true, false] * diff --git a/src/function/logical/xor.js b/src/function/logical/xor.js index 1ae3a05f5b..dbcded2367 100644 --- a/src/function/logical/xor.js +++ b/src/function/logical/xor.js @@ -32,10 +32,9 @@ export const createXor = /* #__PURE__ */ factory(name, dependencies, ({ typed, m * * math.xor(2, 4) // returns false * - * a = [2, 0, 0] - * b = [2, 7, 0] - * c = 0 - * + * const a = [2, 0, 0] + * const b = [2, 7, 0] + * const c = 0 * math.xor(a, b) // returns [false, true, false] * math.xor(a, c) // returns [true, false, false] * diff --git a/src/function/matrix/concat.js b/src/function/matrix/concat.js index 6d8fe0f44b..b04ec3de0e 100644 --- a/src/function/matrix/concat.js +++ b/src/function/matrix/concat.js @@ -26,7 +26,6 @@ export const createConcat = /* #__PURE__ */ factory(name, dependencies, ({ typed * * const A = [[1, 2], [5, 6]] * const B = [[3, 4], [7, 8]] - * * math.concat(A, B) // returns [[1, 2, 3, 4], [5, 6, 7, 8]] * math.concat(A, B, 0) // returns [[1, 2], [5, 6], [3, 4], [7, 8]] * math.concat('hello', ' ', 'world') // returns 'hello world' diff --git a/src/function/matrix/mapSlices.js b/src/function/matrix/mapSlices.js index 1c86948f7a..ccdc68aa1e 100644 --- a/src/function/matrix/mapSlices.js +++ b/src/function/matrix/mapSlices.js @@ -24,7 +24,6 @@ export const createMapSlices = /* #__PURE__ */ factory(name, dependencies, ({ ty * * const A = [[1, 2], [3, 4]] * const sum = math.sum - * * math.mapSlices(A, 0, sum) // returns [4, 6] * math.mapSlices(A, 1, sum) // returns [3, 7] * diff --git a/src/function/relational/compareNatural.js b/src/function/relational/compareNatural.js index 17239331fa..26e51a6737 100644 --- a/src/function/relational/compareNatural.js +++ b/src/function/relational/compareNatural.js @@ -53,7 +53,7 @@ export const createCompareNatural = /* #__PURE__ */ factory(name, dependencies, * math.compareNatural('Answer: 10', 'Answer: 2') // returns 1 * math.compareText('Answer: 10', 'Answer: 2') // returns -1 * math.compare('Answer: 10', 'Answer: 2') - * // Error: Cannot convert "Answer: 10" to a number + * // throws Error: Cannot convert "Answer: 10" to a number * * const a = math.unit('5 cm') * const b = math.unit('40 mm') diff --git a/src/function/string/format.js b/src/function/string/format.js index 69e337ce62..0fc5619c91 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}) * } diff --git a/src/function/utils/numeric.js b/src/function/utils/numeric.js index e9cb5a24f2..c9d6f756e1 100644 --- a/src/function/utils/numeric.js +++ b/src/function/utils/numeric.js @@ -3,12 +3,14 @@ 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 = { + boolean: true, string: true, number: true, + bigint: true, BigNumber: true, Fraction: true } @@ -19,7 +21,7 @@ export const createNumeric = /* #__PURE__ */ factory(name, dependencies, ({ numb BigNumber: bignumber ? (x) => bignumber(x) : noBignumber, - bigint: (x) => BigInt(x), + bigint: (x) => bigint(x, { safe: false }), Fraction: fraction ? (x) => fraction(x) : noFraction diff --git a/src/type/bigint.js b/src/type/bigint.js index 2b5e90625c..c78cc51529 100644 --- a/src/type/bigint.js +++ b/src/type/bigint.js @@ -2,59 +2,110 @@ 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 + * // the bigint 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(math.complex(2.5, -0.3)) // returns 3n + * math.bigint(math.complex(-17, 1)) // throws RangeError + * 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 * * @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', { '': function () { return 0n }, + null: function (x) { + return 0n + }, + 'null, Object': function (x) { + return 0n + }, bigint: function (x) { return x }, - - number: function (x) { - return BigInt(x.toFixed()) - }, - - BigNumber: function (x) { - return BigInt(x.round().toString()) + 'bigint, Object': function (x) { + // Options irrelevant because always safe and no rounding needed + return x }, - Fraction: function (x) { - return BigInt(x.valueOf().toFixed()) + boolean: function (x) { + return BigInt(x) }, - - 'string | boolean': function (x) { + 'boolean, Object': function (x) { return BigInt(x) }, - null: function (x) { - return 0n - }, + string: stringToBigint, + 'string, Object': stringToBigint, + + 'number | BigNumber | Fraction': numericToBigint, + 'number | BigNumber | Fraction, Object': numericToBigint, + + Complex: complexToBigint, + 'Complex, Object': complexToBigint, 'Array | Matrix': typed.referToSelf(self => x => deepMap(x, self)) }) @@ -68,5 +119,100 @@ 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. We also can't use parseFloat + // because it will go through the `number` type with its potential loss + // of accuracy. + 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 + } + + function complexToBigint (z, options = {}) { + if (numericToBigint(z.im, options) !== 0n) { + throw new RangeError( + `Complex number with nonzero imaginary part ${z.im} cannot ` + + 'be converted to bigint.' + ) + } + return numericToBigint(z.re, options) + } + 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..35c284bed7 100644 --- a/src/type/boolean.js +++ b/src/type/boolean.js @@ -27,7 +27,7 @@ export const createBoolean = /* #__PURE__ */ factory(name, dependencies, ({ type * * See also: * - * bignumber, complex, index, matrix, string, unit + * bigint, bignumber, complex, index, matrix, string, unit * * @param {string | number | boolean | Array | Matrix | null} value A value of any type * @return {boolean | Array | Matrix} The boolean value @@ -45,6 +45,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..d02c4a6e09 100644 --- a/src/type/complex/function/complex.js +++ b/src/type/complex/function/complex.js @@ -28,16 +28,19 @@ 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 * * @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..3657e25a45 100644 --- a/src/type/fraction/function/fraction.js +++ b/src/type/fraction/function/fraction.js @@ -30,12 +30,12 @@ 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 * * @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..38c541bee1 100644 --- a/src/type/matrix/function/index.js +++ b/src/type/matrix/function/index.js @@ -31,7 +31,7 @@ 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: * diff --git a/src/type/matrix/function/matrix.js b/src/type/matrix/function/matrix.js index 385301fc3d..7c35289c6a 100644 --- a/src/type/matrix/function/matrix.js +++ b/src/type/matrix/function/matrix.js @@ -29,7 +29,7 @@ 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 * * @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..df474694ed 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 diff --git a/src/type/string.js b/src/type/string.js index 85f57ff81c..a06d6e662b 100644 --- a/src/type/string.js +++ b/src/type/string.js @@ -16,17 +16,17 @@ 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 * * @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..59e1aafde0 100644 --- a/src/type/unit/function/unit.js +++ b/src/type/unit/function/unit.js @@ -19,15 +19,16 @@ 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 * * @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..fb6401929b 100644 --- a/test/node-tests/doc.test.js +++ b/test/node-tests/doc.test.js @@ -4,9 +4,16 @@ import { fileURLToPath } from 'node:url' import { approxEqual, approxDeepEqual } from '../../tools/approx.js' import { collectDocs } from '../../tools/docgenerator.js' import { create, all } from '../../lib/esm/index.js' +import { isNode } from '../../src/utils/is.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 +47,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('_')", @@ -87,17 +95,16 @@ function extractValue (spec) { } const knownProblems = new Set([ - 'setUnion', 'unequal', 'equal', 'deepEqual', 'compareNatural', 'randomInt', + 'setUnion', 'unequal', 'equal', 'deepEqual', 'randomInt', 'random', 'pickRandom', 'kldivergence', - 'parser', 'compile', 're', 'im', + 'parser', 'compile', 'im', 'subset', 'squeeze', 'rotationMatrix', 'rotate', 'reshape', 'partitionSelect', 'matrixFromFunction', - 'matrixFromColumns', 'getMatrixDataType', 'eigs', 'diff', + 'getMatrixDataType', 'eigs', 'diff', 'nthRoots', 'nthRoot', - 'mod', 'floor', 'fix', 'expm1', 'exp', - 'ceil', 'cbrt', 'add', 'slu', - 'rationalize', 'qr', 'lusolve', 'lup', 'derivative', - 'symbolicEqual', 'schur', 'sylvester', 'freqz', 'round' + 'mod', 'expm1', 'exp', + 'slu', 'lusolve', 'derivative', + 'symbolicEqual', 'schur', 'sylvester', 'freqz' ]) let issueCount = 0 @@ -122,11 +129,18 @@ function maybeCheckExpectation (name, expected, expectedFrom, got, gotFrom) { function checkExpectation (want, got) { if (Array.isArray(want)) { if (!Array.isArray(got)) { - got = want.valueOf() + got = got.valueOf() } return approxDeepEqual(got, want, 1e-9) } if (want instanceof math.Unit && got instanceof math.Unit) { + if (got.skipAutomaticSimplification !== want.skipAutomaticSimplification) { + issueCount++ + if (debug) { + console.log(' Note: Ignoring different skipAutomaticSimplification') + } + got.skipAutomaticSimplification = want.skipAutomaticSimplification + } if (got.fixPrefix !== want.fixPrefix) { issueCount++ if (debug) { @@ -146,6 +160,9 @@ function checkExpectation (want, got) { } return approxEqual(got, want, 1e-9) } + if (typeof want === 'string' && isNode(got)) { + return got.toString() === want + } if ( typeof want === 'string' && typeof got === 'string' && @@ -217,9 +234,7 @@ const knownUndocumented = new Set([ 'off', 'once', 'emit', - 'config', 'expression', - 'import', 'create', 'factory', 'AccessorNode', @@ -228,16 +243,12 @@ const knownUndocumented = new Set([ 'atomicMass', 'avogadro', 'BigNumber', - 'bignumber', 'BlockNode', 'bohrMagneton', 'bohrRadius', 'boltzmann', - 'boolean', - 'chain', 'Chain', 'classicalElectronRadius', - 'complex', 'Complex', 'ConditionalNode', 'conductanceQuantum', @@ -256,7 +267,6 @@ const knownUndocumented = new Set([ 'fermiCoupling', 'fineStructure', 'firstRadiation', - 'fraction', 'Fraction', 'FunctionAssignmentNode', 'FunctionNode', @@ -267,7 +277,6 @@ const knownUndocumented = new Set([ 'Help', 'i', 'ImmutableDenseMatrix', - 'index', 'Index', 'IndexNode', 'Infinity', @@ -280,7 +289,6 @@ const knownUndocumented = new Set([ 'loschmidt', 'magneticConstant', 'magneticFluxQuantum', - 'matrix', 'Matrix', 'molarMass', 'molarMassC12', @@ -291,12 +299,9 @@ const knownUndocumented = new Set([ 'Node', 'nuclearMagneton', 'null', - 'number', - 'bigint', 'ObjectNode', 'OperatorNode', 'ParenthesisNode', - 'parse', 'Parser', 'phi', 'pi', @@ -321,19 +326,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 +372,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" } @@ -382,10 +384,8 @@ describe('Testing examples from (jsdoc) comments', function () { for (const line of lines) { if (line.includes('//')) { let parts = line.split('//') - if (parts[0] && !parts[0].trim()) { - // Indented comment, unusual in examples - // assume this is a comment within some code to evaluate - // i.e., ignore it + if (!parts[0].trim() && !/^\s*(returns|throws)/.test(parts[1])) { + // Only a comment without "returns/throws" so ignore it continue } // Comment specifying a future value or the return of prior code @@ -399,6 +399,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 +407,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 +419,8 @@ describe('Testing examples from (jsdoc) comments', function () { if (line !== '') { if (accumulation) { accumulation += '\n' } accumulation += line + } else { + accumulation = '' } } } diff --git a/test/unit-tests/function/logical/and.test.js b/test/unit-tests/function/logical/and.test.js index 051a838fed..93e427da70 100644 --- a/test/unit-tests/function/logical/and.test.js +++ b/test/unit-tests/function/logical/and.test.js @@ -129,6 +129,7 @@ describe('and', function () { it('should and array - scalar', function () { assert.deepStrictEqual(and(10, [0, 2]), [false, true]) assert.deepStrictEqual(and([0, 2], 10), [false, true]) + assert.deepStrictEqual(and([0, 2], 0), [false, false]) }) it('should and array - array', function () { @@ -154,6 +155,9 @@ describe('and', function () { it('should and dense matrix - scalar', function () { assert.deepStrictEqual(and(10, matrix([0, 2])), matrix([false, true])) assert.deepStrictEqual(and(matrix([0, 2]), 10), matrix([false, true])) + assert.deepStrictEqual( + and(matrix([[9], [0]]), 0), + matrix([[false], [false]])) }) it('should and dense matrix - array', function () { @@ -175,6 +179,9 @@ describe('and', function () { it('should and sparse matrix - scalar', function () { assert.deepStrictEqual(and(10, sparse([[0], [2]])), sparse([[false], [true]])) assert.deepStrictEqual(and(sparse([[0], [2]]), 10), sparse([[false], [true]])) + assert.deepStrictEqual( + and(sparse([0, 0, 3]), 0), + sparse([false, false, false])) }) it('should and sparse matrix - array', function () { diff --git a/test/unit-tests/function/utils/numeric.test.js b/test/unit-tests/function/utils/numeric.test.js new file mode 100644 index 0000000000..9f8cf550ea --- /dev/null +++ b/test/unit-tests/function/utils/numeric.test.js @@ -0,0 +1,38 @@ +import assert from 'assert' +import math from '../../../../src/defaultInstance.js' + +const numeric = math.numeric +const bignumber = math.bignumber +const fraction = math.fraction + +describe('numeric', function () { + it('should convert things to numbers', function () { + assert.strictEqual(numeric(99), 99) + assert(isNaN(numeric(NaN, 'number'))) + assert.strictEqual(numeric('Infinity'), Infinity) + assert.strictEqual(numeric('-2.7', 'number'), -2.7) + assert.strictEqual(numeric(true), 1) + assert.strictEqual(numeric(false, 'number'), 0) + assert.strictEqual(numeric(bignumber('3.7e6')), 3.7e6) + assert.strictEqual(numeric(bignumber(-1.5), 'number'), -1.5) + assert.strictEqual(numeric(fraction(5, 4)), 1.25) + assert.strictEqual(numeric(fraction(-3, 8), 'number'), -0.375) + assert.strictEqual(numeric(0n), 0) + assert.strictEqual(numeric(2n ** 48n, 'number'), 2 ** 48) + }) + + it('should convert things to bignumbers', function () { + assert.deepStrictEqual(numeric(99, 'BigNumber'), bignumber(99)) + assert.deepStrictEqual(numeric(NaN, 'BigNumber'), bignumber(NaN)) + assert.deepStrictEqual(numeric('Infinity', 'BigNumber'), bignumber(Infinity)) + assert.deepStrictEqual(numeric('-2.7', 'BigNumber'), bignumber(-2.7)) + assert.deepStrictEqual(numeric(true, 'BigNumber'), bignumber(1)) + assert.deepStrictEqual( + numeric(bignumber('3.7e6'), 'BigNumber'), bignumber(3.7e6)) + assert.deepStrictEqual(numeric(fraction(5, 4), 'BigNumber'), bignumber(1.25)) + assert.deepStrictEqual(numeric(0n, 'BigNumber'), bignumber(0)) + }) + + // See further tests in the non-standard location + // test/unit-tests/type/numeric.test.js +}) diff --git a/test/unit-tests/type/bigint.test.js b/test/unit-tests/type/bigint.test.js index 3a5d536f77..247a0be473 100644 --- a/test/unit-tests/type/bigint.test.js +++ b/test/unit-tests/type/bigint.test.js @@ -20,20 +20,41 @@ 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 () { assert.strictEqual(bigint(123), 123n) assert.strictEqual(bigint(2.3), 2n) + assert.strictEqual(bigint(3 ** 50), 717897987691852578422784n) }) 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 convert a Complex to a bigint', function () { + assert.strictEqual(bigint(math.complex(0)), 0n) + assert.strictEqual(bigint(math.complex(6543210.9, 0.49)), 6543211n) + assert.throws(() => bigint(math.complex(0, -0.6)), RangeError) }) 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 +62,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,11 +130,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/) - }) - - it('should throw an error if called with a complex number', function () { - assert.throws(function () { bigint(math.complex(2, 3)) }, TypeError) + assert.throws(function () { bigint(1, 2, 3) }, TypeError) + assert.throws(function () { bigint(1, {}, 3) }, /TypeError: Too many arguments/) }) it('should throw an error with wrong type of arguments', function () { diff --git a/test/unit-tests/type/numeric.test.js b/test/unit-tests/type/numeric.test.js index 5b3eb82424..841c470414 100644 --- a/test/unit-tests/type/numeric.test.js +++ b/test/unit-tests/type/numeric.test.js @@ -13,7 +13,6 @@ describe('numeric', function () { }) it('should throw if called with invalid argument', function () { - assert.throws(() => { numeric(true, 'number') }, /Cannot convert/) assert.throws(() => { numeric(null, 'number') }, /Cannot convert/) assert.throws(() => { numeric([], 'number') }, /Cannot convert/) assert.throws(() => { numeric({}, 'number') }, /Cannot convert/) @@ -85,6 +84,31 @@ 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.strictEqual(tobi(math.bignumber(27.4)), 27n) + assert.strictEqual(tobi(math.bignumber(27.8)), 28n) + assert.strictEqual( + tobi(math.fraction('123456789012345678901234567890123456789/2')), + 61728394506172839450617283945061728395n) + }) + 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..21fedf5cb1 100644 --- a/tools/docgenerator.js +++ b/tools/docgenerator.js @@ -567,19 +567,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' }