From b38647f574f82c0141a07233a7fbec74f2f3e8d4 Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Wed, 15 Nov 2023 11:58:06 +0100 Subject: [PATCH 1/5] fix #2761: implement support for units in function `round` (WIP) --- .../embeddedDocs/function/arithmetic/round.js | 4 +- src/function/arithmetic/round.js | 74 ++++++++++++++----- .../function/arithmetic/round.test.js | 37 +++++++++- types/index.d.ts | 32 +++++++- 4 files changed, 122 insertions(+), 25 deletions(-) diff --git a/src/expression/embeddedDocs/function/arithmetic/round.js b/src/expression/embeddedDocs/function/arithmetic/round.js index e30cb82e08..f8de494258 100644 --- a/src/expression/embeddedDocs/function/arithmetic/round.js +++ b/src/expression/embeddedDocs/function/arithmetic/round.js @@ -13,7 +13,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..0938261205 100644 --- a/src/function/arithmetic/round.js +++ b/src/function/arithmetic/round.js @@ -30,6 +30,7 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed, * * math.round(x) * math.round(x, n) + * math.round(unit, n, valuelessUnit) * * Examples: * @@ -47,14 +48,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 +117,75 @@ 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)), + + 'Unit, Array | Matrix, Unit': typed.referToSelf(self => (x, nCollection, unit) => { + // deep map collection, skip zeros since round(0) = 0 + return deepMap(nCollection, (n) => self(x, n, unit), true) + }), + + 'Unit, number | BigNumber, Array | Matrix': typed.referToSelf(self => (x, n, unitCollection) => { + // deep map collection, skip zeros since round(0) = 0 + return deepMap(unitCollection, (unit) => self(x, n, unit), true) + }), + + 'Unit, Array | Matrix': typed.referToSelf(self => (x, unitCollection) => self(x, 0, unitCollection)), + + '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/unit-tests/function/arithmetic/round.test.js b/test/unit-tests/function/arithmetic/round.test.js index 11895ad2cf..f4520668be 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,38 @@ 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')])) + + // decimals is a collection + assert.deepStrictEqual(round(unit('3.12345 cm'), [0, 1, 2], unit('cm')), [unit('3 cm'), unit('3.1 cm'), unit('3.12 cm')]) + + // unit is a collection + assert.deepStrictEqual(round(unit('3.12345 cm'), [unit('cm'), unit('mm')]), [unit('3 cm'), unit('31 mm')]) + assert.deepStrictEqual(round(unit('3.12345 cm'), 1, [unit('cm'), unit('mm')]), [unit('3.1 cm'), unit('31.2 mm')]) + }) + + 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..22e467b0ff 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1103,7 +1103,14 @@ export interface MathJsInstance extends MathJsFactory { x: T, n?: number | BigNumber ): NoLiteralType - round(x: MathNumericType, n: U): U + round(x: MathNumericType | U, n: U): U + round(x: Unit, n: number | BigNumber, unit: Unit): Unit + round(x: Unit, unit: Unit): Unit + round(x: U, unit: Unit): U + round(x: U, n: number | BigNumber, unit: Unit): U + round(x: Unit, n: U, unit: Unit): U + round(x: Unit, n: number | BigNumber, unit: U): U + round(x: Unit, unit: U): U // End of group of rounding functions @@ -4738,8 +4745,27 @@ export interface MathJsChain { */ round( this: MathJsChain, - n?: number | BigNumber | MathCollection - ): MathJsChain + n?: number | BigNumber + ): NoLiteralType + round( + this: MathJsChain, + n: U + ): U + round(this: MathJsChain, n: number | BigNumber, unit: Unit): Unit + round(this: MathJsChain, unit: Unit): Unit + round(this: MathJsChain, unit: Unit): U + round( + this: MathJsChain, + n: number | BigNumber, + unit: Unit + ): U + round(this: MathJsChain, n: U, unit: Unit): U + round( + this: MathJsChain, + n: number | BigNumber, + unit: U + ): U + round(this: MathJsChain, unit: U): U // End of rounding group From 23fcb9862aeb866d6c13b46bcf7155bec97b5c0d Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Wed, 15 Nov 2023 13:51:12 +0100 Subject: [PATCH 2/5] fix #2761: extend function `round` with support for units --- test/node-tests/doc.test.js | 2 +- test/typescript-tests/testTypes.ts | 13 +++++++++++ types/index.d.ts | 36 +++++++++++++++++++++--------- 3 files changed, 39 insertions(+), 12 deletions(-) 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/types/index.d.ts b/types/index.d.ts index 22e467b0ff..7ebc8e0f79 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1103,7 +1103,7 @@ export interface MathJsInstance extends MathJsFactory { x: T, n?: number | BigNumber ): NoLiteralType - round(x: MathNumericType | U, n: U): U + round(x: MathNumericType, n: U): U round(x: Unit, n: number | BigNumber, unit: Unit): Unit round(x: Unit, unit: Unit): Unit round(x: U, unit: Unit): U @@ -4745,27 +4745,41 @@ export interface MathJsChain { */ round( this: MathJsChain, - n?: number | BigNumber - ): NoLiteralType + n?: number | BigNumber | MathCollection + ): MathJsChain round( this: MathJsChain, n: U - ): U - round(this: MathJsChain, n: number | BigNumber, unit: Unit): Unit - round(this: MathJsChain, unit: Unit): Unit - round(this: MathJsChain, unit: Unit): U + ): MathJsChain + round( + this: MathJsChain, + n: number | BigNumber, + unit: Unit + ): MathJsChain + round(this: MathJsChain, unit: Unit): MathJsChain + round( + this: MathJsChain, + unit: Unit + ): MathJsChain round( this: MathJsChain, n: number | BigNumber, unit: Unit - ): U - round(this: MathJsChain, n: U, unit: Unit): U + ): MathJsChain + round( + this: MathJsChain, + n: U, + unit: Unit + ): MathJsChain round( this: MathJsChain, n: number | BigNumber, unit: U - ): U - round(this: MathJsChain, unit: U): U + ): MathJsChain + round( + this: MathJsChain, + unit: U + ): MathJsChain // End of rounding group From 71cadf18e9ae85c133e99b070182e33ad4fadf4e Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Wed, 15 Nov 2023 13:56:36 +0100 Subject: [PATCH 3/5] docs: describe all signatures in the docs of function round --- src/expression/embeddedDocs/function/arithmetic/round.js | 4 +++- src/function/arithmetic/round.js | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/expression/embeddedDocs/function/arithmetic/round.js b/src/expression/embeddedDocs/function/arithmetic/round.js index f8de494258..04ac47dcdd 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.', diff --git a/src/function/arithmetic/round.js b/src/function/arithmetic/round.js index 0938261205..49f7c18183 100644 --- a/src/function/arithmetic/round.js +++ b/src/function/arithmetic/round.js @@ -30,6 +30,7 @@ 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: From cdc45de51ffe5a502f98eb1e73cc74935713802d Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Wed, 15 Nov 2023 13:59:46 +0100 Subject: [PATCH 4/5] chore: fix linting issue --- src/expression/embeddedDocs/function/arithmetic/round.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/expression/embeddedDocs/function/arithmetic/round.js b/src/expression/embeddedDocs/function/arithmetic/round.js index 04ac47dcdd..f0f42ff507 100644 --- a/src/expression/embeddedDocs/function/arithmetic/round.js +++ b/src/expression/embeddedDocs/function/arithmetic/round.js @@ -5,7 +5,7 @@ export const roundDocs = { 'round(x)', 'round(x, n)', 'round(unit, valuelessUnit)', - 'round(unit, n, 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.', From 5dc5524c4eae58579993005d7063f21878d4d8ae Mon Sep 17 00:00:00 2001 From: Jos de Jong Date: Thu, 16 Nov 2023 15:05:50 +0100 Subject: [PATCH 5/5] chore: remove less-useful signatures for round with units and matrices --- src/function/arithmetic/round.js | 12 ------- .../function/arithmetic/round.test.js | 7 ----- types/index.d.ts | 31 +++++-------------- 3 files changed, 7 insertions(+), 43 deletions(-) diff --git a/src/function/arithmetic/round.js b/src/function/arithmetic/round.js index 49f7c18183..5065dafcc9 100644 --- a/src/function/arithmetic/round.js +++ b/src/function/arithmetic/round.js @@ -136,18 +136,6 @@ export const createRound = /* #__PURE__ */ factory(name, dependencies, ({ typed, 'Array | Matrix, Unit': typed.referToSelf(self => (x, unit) => self(x, 0, unit)), - 'Unit, Array | Matrix, Unit': typed.referToSelf(self => (x, nCollection, unit) => { - // deep map collection, skip zeros since round(0) = 0 - return deepMap(nCollection, (n) => self(x, n, unit), true) - }), - - 'Unit, number | BigNumber, Array | Matrix': typed.referToSelf(self => (x, n, unitCollection) => { - // deep map collection, skip zeros since round(0) = 0 - return deepMap(unitCollection, (unit) => self(x, n, unit), true) - }), - - 'Unit, Array | Matrix': typed.referToSelf(self => (x, unitCollection) => self(x, 0, unitCollection)), - 'Array | Matrix': typed.referToSelf(self => x => { // deep map collection, skip zeros since round(0) = 0 return deepMap(x, self, true) diff --git a/test/unit-tests/function/arithmetic/round.test.js b/test/unit-tests/function/arithmetic/round.test.js index f4520668be..624110cb80 100644 --- a/test/unit-tests/function/arithmetic/round.test.js +++ b/test/unit-tests/function/arithmetic/round.test.js @@ -133,13 +133,6 @@ describe('round', function () { // 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')])) - - // decimals is a collection - assert.deepStrictEqual(round(unit('3.12345 cm'), [0, 1, 2], unit('cm')), [unit('3 cm'), unit('3.1 cm'), unit('3.12 cm')]) - - // unit is a collection - assert.deepStrictEqual(round(unit('3.12345 cm'), [unit('cm'), unit('mm')]), [unit('3 cm'), unit('31 mm')]) - assert.deepStrictEqual(round(unit('3.12345 cm'), 1, [unit('cm'), unit('mm')]), [unit('3.1 cm'), unit('31.2 mm')]) }) it('should throw an error if used with a unit without valueless unit', function () { diff --git a/types/index.d.ts b/types/index.d.ts index 7ebc8e0f79..9678b6f6d6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -1104,13 +1104,10 @@ export interface MathJsInstance extends MathJsFactory { n?: number | BigNumber ): NoLiteralType round(x: MathNumericType, n: U): U - round(x: Unit, n: number | BigNumber, unit: Unit): Unit - round(x: Unit, unit: Unit): Unit 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 - round(x: Unit, n: U, unit: Unit): U - round(x: Unit, n: number | BigNumber, unit: U): U - round(x: Unit, unit: U): U // End of group of rounding functions @@ -4751,34 +4748,20 @@ export interface MathJsChain { this: MathJsChain, n: U ): MathJsChain - round( - this: MathJsChain, - n: number | BigNumber, - unit: Unit - ): MathJsChain round(this: MathJsChain, unit: Unit): MathJsChain round( this: MathJsChain, unit: Unit ): MathJsChain - round( - this: MathJsChain, - n: number | BigNumber, - unit: Unit - ): MathJsChain - round( + round( this: MathJsChain, - n: U, + n: number | BigNumber, unit: Unit - ): MathJsChain + ): MathJsChain round( - this: MathJsChain, + this: MathJsChain, n: number | BigNumber, - unit: U - ): MathJsChain - round( - this: MathJsChain, - unit: U + unit: Unit ): MathJsChain // End of rounding group