From b9346720d678dad158f9aa02d14858e321d1c7f0 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Fri, 17 Nov 2023 10:15:17 +0100 Subject: [PATCH] feat: extend function `round` with support for units (#3095) * fix #2761: implement support for units in function `round` (WIP) * fix #2761: extend function `round` with support for units * docs: describe all signatures in the docs of function round * chore: fix linting issue * chore: remove less-useful signatures for round with units and matrices --- .../embeddedDocs/function/arithmetic/round.js | 8 ++- src/function/arithmetic/round.js | 63 ++++++++++++++----- test/node-tests/doc.test.js | 2 +- test/typescript-tests/testTypes.ts | 13 ++++ .../function/arithmetic/round.test.js | 30 +++++++-- types/index.d.ts | 23 +++++++ 6 files changed, 115 insertions(+), 24 deletions(-) diff --git a/src/expression/embeddedDocs/function/arithmetic/round.js b/src/expression/embeddedDocs/function/arithmetic/round.js index e30cb82e08..f0f42ff507 100644 --- a/src/expression/embeddedDocs/function/arithmetic/round.js +++ b/src/expression/embeddedDocs/function/arithmetic/round.js @@ -3,7 +3,9 @@ export const roundDocs = { category: 'Arithmetic', syntax: [ 'round(x)', - 'round(x, n)' + 'round(x, n)', + 'round(unit, valuelessUnit)', + 'round(unit, n, valuelessUnit)' ], description: 'round a value towards the nearest integer.If x is complex, both real and imaginary part are rounded towards the nearest integer. When n is specified, the value is rounded to n decimals.', @@ -13,7 +15,9 @@ export const roundDocs = { 'round(-4.2)', 'round(-4.8)', 'round(pi, 3)', - 'round(123.45678, 2)' + 'round(123.45678, 2)', + 'round(3.241cm, 2, cm)', + 'round([3.2, 3.8, -4.7])' ], seealso: ['ceil', 'floor', 'fix'] } diff --git a/src/function/arithmetic/round.js b/src/function/arithmetic/round.js index f532d1564d..5065dafcc9 100644 --- a/src/function/arithmetic/round.js +++ b/src/function/arithmetic/round.js @@ -30,6 +30,8 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed, * * math.round(x) * math.round(x, n) + * math.round(unit, valuelessUnit) + * math.round(unit, n, valuelessUnit) * * Examples: * @@ -47,14 +49,21 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed, * const c = math.complex(3.2, -2.7) * math.round(c) // returns Complex 3 - 3i * + * const unit = math.unit('3.241 cm') + * const cm = math.unit('cm') + * const mm = math.unit('mm') + * math.round(unit, 1, cm) // returns Unit 3.2 cm + * math.round(unit, 1, mm) // returns Unit 32.4 mm + * * math.round([3.2, 3.8, -4.7]) // returns Array [3, 4, -5] * * See also: * * ceil, fix, floor * - * @param {number | BigNumber | Fraction | Complex | Array | Matrix} x Number to be rounded + * @param {number | BigNumber | Fraction | Complex | Unit | Array | Matrix} x Value to be rounded * @param {number | BigNumber | Array} [n=0] Number of decimals + * @param {Unit} [valuelessUnit] A valueless unit * @return {number | BigNumber | Fraction | Complex | Array | Matrix} Rounded value */ return typed(name, { @@ -109,43 +118,63 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed, return x.round(n.toNumber()) }, - // deep map collection, skip zeros since round(0) = 0 - 'Array | Matrix': typed.referToSelf(self => x => deepMap(x, self, true)), + 'Unit, number, Unit': typed.referToSelf(self => function (x, n, unit) { + const valueless = x.toNumeric(unit) + return unit.multiply(self(valueless, n)) + }), + + 'Unit, BigNumber, Unit': typed.referToSelf(self => (x, n, unit) => self(x, n.toNumber(), unit)), + + 'Unit, Unit': typed.referToSelf(self => (x, unit) => self(x, 0, unit)), + + 'Array | Matrix, number, Unit': typed.referToSelf(self => (x, n, unit) => { + // deep map collection, skip zeros since round(0) = 0 + return deepMap(x, (value) => self(value, n, unit), true) + }), + + 'Array | Matrix, BigNumber, Unit': typed.referToSelf(self => (x, n, unit) => self(x, n.toNumber(), unit)), + + 'Array | Matrix, Unit': typed.referToSelf(self => (x, unit) => self(x, 0, unit)), + + 'Array | Matrix': typed.referToSelf(self => x => { + // deep map collection, skip zeros since round(0) = 0 + return deepMap(x, self, true) + }), - 'SparseMatrix, number | BigNumber': typed.referToSelf(self => (x, y) => { - return matAlgo11xS0s(x, y, self, false) + 'SparseMatrix, number | BigNumber': typed.referToSelf(self => (x, n) => { + return matAlgo11xS0s(x, n, self, false) }), - 'DenseMatrix, number | BigNumber': typed.referToSelf(self => (x, y) => { - return matAlgo14xDs(x, y, self, false) + 'DenseMatrix, number | BigNumber': typed.referToSelf(self => (x, n) => { + return matAlgo14xDs(x, n, self, false) }), - 'Array, number | BigNumber': typed.referToSelf(self => (x, y) => { + 'Array, number | BigNumber': typed.referToSelf(self => (x, n) => { // use matrix implementation - return matAlgo14xDs(matrix(x), y, self, false).valueOf() + return matAlgo14xDs(matrix(x), n, self, false).valueOf() }), - 'number | Complex | BigNumber | Fraction, SparseMatrix': typed.referToSelf(self => (x, y) => { + 'number | Complex | BigNumber | Fraction, SparseMatrix': typed.referToSelf(self => (x, n) => { // check scalar is zero if (equalScalar(x, 0)) { // do not execute algorithm, result will be a zero matrix - return zeros(y.size(), y.storage()) + return zeros(n.size(), n.storage()) } - return matAlgo12xSfs(y, x, self, true) + return matAlgo12xSfs(n, x, self, true) }), - 'number | Complex | BigNumber | Fraction, DenseMatrix': typed.referToSelf(self => (x, y) => { + 'number | Complex | BigNumber | Fraction, DenseMatrix': typed.referToSelf(self => (x, n) => { // check scalar is zero if (equalScalar(x, 0)) { // do not execute algorithm, result will be a zero matrix - return zeros(y.size(), y.storage()) + return zeros(n.size(), n.storage()) } - return matAlgo14xDs(y, x, self, true) + return matAlgo14xDs(n, x, self, true) }), - 'number | Complex | BigNumber | Fraction, Array': typed.referToSelf(self => (x, y) => { + 'number | Complex | BigNumber | Fraction, Array': typed.referToSelf(self => (x, n) => { // use matrix implementation - return matAlgo14xDs(matrix(y), x, self, true).valueOf() + return matAlgo14xDs(matrix(n), x, self, true).valueOf() }) }) }) diff --git a/test/node-tests/doc.test.js b/test/node-tests/doc.test.js index a33b2f7c5e..e01b18a3ef 100644 --- a/test/node-tests/doc.test.js +++ b/test/node-tests/doc.test.js @@ -99,7 +99,7 @@ const knownProblems = new Set([ 'mod', 'invmod', 'floor', 'fix', 'expm1', 'exp', 'dotPow', 'dotMultiply', 'dotDivide', 'divide', 'ceil', 'cbrt', 'add', 'usolveAll', 'usolve', 'slu', 'rationalize', 'qr', 'lusolve', 'lup', 'lsolveAll', 'lsolve', 'derivative', - 'symbolicEqual', 'map', 'schur', 'sylvester', 'freqz' + 'symbolicEqual', 'map', 'schur', 'sylvester', 'freqz', 'round' ]) function maybeCheckExpectation (name, expected, expectedFrom, got, gotFrom) { diff --git a/test/typescript-tests/testTypes.ts b/test/typescript-tests/testTypes.ts index 8ce11c3f87..730fb665f7 100644 --- a/test/typescript-tests/testTypes.ts +++ b/test/typescript-tests/testTypes.ts @@ -633,6 +633,9 @@ Chaining examples expectTypeOf(math.chain([1]).round()).toMatchTypeOf< MathJsChain >() + expectTypeOf( + math.chain(math.unit('5.2cm')).round(math.unit('cm')) + ).toMatchTypeOf>() // cube expectTypeOf(math.chain(1).cube()).toMatchTypeOf>() @@ -1904,6 +1907,16 @@ Function round examples math.complex(3.2, -2.7) ) + // unit input + assert.deepStrictEqual( + math.round(math.unit('5.21 cm'), math.unit('cm')), + math.unit('5 cm') + ) + assert.deepStrictEqual( + math.round(math.unit('5.21 cm'), 1, math.unit('cm')), + math.unit('5.2 cm') + ) + // array input assert.deepStrictEqual(math.round([3.2, 3.8, -4.7]), [3, 4, -5]) assert.deepStrictEqual(math.round([3.21, 3.82, -4.71], 1), [3.2, 3.8, -4.7]) diff --git a/test/unit-tests/function/arithmetic/round.test.js b/test/unit-tests/function/arithmetic/round.test.js index 11895ad2cf..624110cb80 100644 --- a/test/unit-tests/function/arithmetic/round.test.js +++ b/test/unit-tests/function/arithmetic/round.test.js @@ -9,6 +9,7 @@ const fraction = math.fraction const matrix = math.matrix const sparse = math.sparse const round = math.round +const unit = math.unit describe('round', function () { it('should round a number to te given number of decimals', function () { @@ -118,10 +119,31 @@ describe('round', function () { assert.deepStrictEqual(round(complex(2.157, math.pi), bignumber(2)), complex(2.16, 3.14)) }) - it('should throw an error if used with a unit', function () { - assert.throws(function () { round(math.unit('5cm')) }, TypeError, 'Function round(unit) not supported') - assert.throws(function () { round(math.unit('5cm'), 2) }, TypeError, 'Function round(unit) not supported') - assert.throws(function () { round(math.unit('5cm'), bignumber(2)) }, TypeError, 'Function round(unit) not supported') + it('should round units', function () { + assert.deepStrictEqual(round(unit('3.12345 cm'), 3, unit('cm')), unit('3.123 cm')) + assert.deepStrictEqual(round(unit('3.12345 cm'), unit('cm')), unit('3 cm')) + assert.deepStrictEqual(round(unit('2 inch'), unit('cm')), unit('5 cm')) + assert.deepStrictEqual(round(unit('2 inch'), 1, unit('cm')), unit('5.1 cm')) + + // bignumber values + assert.deepStrictEqual(round(unit('3.12345 cm'), bignumber(2), unit('cm')), unit('3.12 cm')) + assert.deepStrictEqual(round(unit(bignumber('2'), 'inch'), unit('cm')), unit(bignumber('5'), 'cm')) + assert.deepStrictEqual(round(unit(bignumber('2'), 'inch'), bignumber(1), unit('cm')), unit(bignumber('5.1'), 'cm')) + + // first argument is a collection + assert.deepStrictEqual(round([unit('2 inch'), unit('3 inch')], unit('cm')), [unit('5 cm'), unit('8 cm')]) + assert.deepStrictEqual(round(matrix([unit('2 inch'), unit('3 inch')]), unit('cm')), matrix([unit('5 cm'), unit('8 cm')])) + }) + + it('should throw an error if used with a unit without valueless unit', function () { + assert.throws(function () { round(unit('5cm')) }, TypeError, 'Function round(unit) not supported') + assert.throws(function () { round(unit('5cm'), 2) }, TypeError, 'Function round(unit) not supported') + assert.throws(function () { round(unit('5cm'), bignumber(2)) }, TypeError, 'Function round(unit) not supported') + }) + + it('should throw an error if used with a unit with a second unit that is not valueless', function () { + assert.throws(function () { round(unit('2 inch'), 1, unit('10 cm')) }, Error) + assert.throws(function () { round(unit('2 inch'), unit('10 cm')) }, Error) }) it('should convert to a number when used with a string', function () { diff --git a/types/index.d.ts b/types/index.d.ts index aa99ae8fad..9678b6f6d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1104,6 +1104,10 @@ export interface MathJsInstance extends MathJsFactory { n?: number | BigNumber ): NoLiteralType round(x: MathNumericType, n: U): U + round(x: U, unit: Unit): U + round(x: Unit, unit: Unit): Unit + round(x: Unit, n: number | BigNumber, unit: Unit): Unit + round(x: U, n: number | BigNumber, unit: Unit): U // End of group of rounding functions @@ -4740,6 +4744,25 @@ export interface MathJsChain { this: MathJsChain, n?: number | BigNumber | MathCollection ): MathJsChain + round( + this: MathJsChain, + n: U + ): MathJsChain + round(this: MathJsChain, unit: Unit): MathJsChain + round( + this: MathJsChain, + unit: Unit + ): MathJsChain + round( + this: MathJsChain, + n: number | BigNumber, + unit: Unit + ): MathJsChain + round( + this: MathJsChain, + n: number | BigNumber, + unit: Unit + ): MathJsChain // End of rounding group