From c0ca6da6a4efaa538f91fc4c78a779108f859dc4 Mon Sep 17 00:00:00 2001 From: Eric Mansfield Date: Sun, 10 Mar 2019 06:36:50 -0600 Subject: [PATCH 1/2] Gracefully handle round-off errors in fix, ceil, and floor (#1432) * Fixed unit base recognition and formatting for user-defined units * Manually copied work from another branch * Removed semicolons * Replaced assert.equal with assert.strictEqual * Added support for bignumber * Replaced var with const --- src/function/arithmetic/ceil.js | 18 ++++++++++++++++-- src/function/arithmetic/fix.js | 7 +++++-- src/function/arithmetic/floor.js | 18 ++++++++++++++++-- test/function/arithmetic/ceil.test.js | 24 +++++++++++++++++++++++- test/function/arithmetic/fix.test.js | 22 ++++++++++++++++++++++ test/function/arithmetic/floor.test.js | 20 ++++++++++++++++++++ 6 files changed, 102 insertions(+), 7 deletions(-) diff --git a/src/function/arithmetic/ceil.js b/src/function/arithmetic/ceil.js index 9d6876eb2..44600ee20 100644 --- a/src/function/arithmetic/ceil.js +++ b/src/function/arithmetic/ceil.js @@ -1,8 +1,12 @@ 'use strict' const deepMap = require('../../utils/collection/deepMap') +const nearlyEqual = require('../../utils/number').nearlyEqual +const bigNearlyEqual = require('../../utils/bignumber/nearlyEqual') function factory (type, config, load, typed) { + const round = load(require('../../function/arithmetic/round')) + /** * Round a value towards plus infinity * If `x` is complex, both real and imaginary part are rounded towards plus infinity. @@ -32,14 +36,24 @@ function factory (type, config, load, typed) { * @return {number | BigNumber | Fraction | Complex | Array | Matrix} Rounded value */ const ceil = typed('ceil', { - 'number': Math.ceil, + 'number': function (x) { + if (nearlyEqual(x, round(x), config.epsilon)) { + return round(x) + } else { + return Math.ceil(x) + } + }, 'Complex': function (x) { return x.ceil() }, 'BigNumber': function (x) { - return x.ceil() + if (bigNearlyEqual(x, round(x), config.epsilon)) { + return round(x) + } else { + return x.ceil() + } }, 'Fraction': function (x) { diff --git a/src/function/arithmetic/fix.js b/src/function/arithmetic/fix.js index f46289749..43ac0d66a 100644 --- a/src/function/arithmetic/fix.js +++ b/src/function/arithmetic/fix.js @@ -3,6 +3,9 @@ const deepMap = require('../../utils/collection/deepMap') function factory (type, config, load, typed) { + const ceil = load(require('../../function/arithmetic/ceil')) + const floor = load(require('../../function/arithmetic/floor')) + /** * Round a value towards zero. * For matrices, the function is evaluated element wise. @@ -32,7 +35,7 @@ function factory (type, config, load, typed) { */ const fix = typed('fix', { 'number': function (x) { - return (x > 0) ? Math.floor(x) : Math.ceil(x) + return (x > 0) ? floor(x) : ceil(x) }, 'Complex': function (x) { @@ -43,7 +46,7 @@ function factory (type, config, load, typed) { }, 'BigNumber': function (x) { - return x.isNegative() ? x.ceil() : x.floor() + return x.isNegative() ? ceil(x) : floor(x) }, 'Fraction': function (x) { diff --git a/src/function/arithmetic/floor.js b/src/function/arithmetic/floor.js index e38ee1bf2..6f9a22867 100644 --- a/src/function/arithmetic/floor.js +++ b/src/function/arithmetic/floor.js @@ -1,8 +1,12 @@ 'use strict' const deepMap = require('../../utils/collection/deepMap') +const nearlyEqual = require('../../utils/number').nearlyEqual +const bigNearlyEqual = require('../../utils/bignumber/nearlyEqual') function factory (type, config, load, typed) { + const round = load(require('../../function/arithmetic/round')) + /** * Round a value towards minus infinity. * For matrices, the function is evaluated element wise. @@ -31,14 +35,24 @@ function factory (type, config, load, typed) { * @return {number | BigNumber | Fraction | Complex | Array | Matrix} Rounded value */ const floor = typed('floor', { - 'number': Math.floor, + 'number': function (x) { + if (nearlyEqual(x, round(x), config.epsilon)) { + return round(x) + } else { + return Math.floor(x) + } + }, 'Complex': function (x) { return x.floor() }, 'BigNumber': function (x) { - return x.floor() + if (bigNearlyEqual(x, round(x), config.epsilon)) { + return round(x) + } else { + return x.floor() + } }, 'Fraction': function (x) { diff --git a/test/function/arithmetic/ceil.test.js b/test/function/arithmetic/ceil.test.js index fa9cd1593..568774a28 100644 --- a/test/function/arithmetic/ceil.test.js +++ b/test/function/arithmetic/ceil.test.js @@ -49,7 +49,7 @@ describe('ceil', function () { approx.deepEqual(ceil(complex(-1.3, -1.8)), complex(-1, -1)) }) - it('should return the ceil of a number', function () { + it('should return the ceil of a fraction', function () { const a = fraction('2/3') assert(ceil(a) instanceof math.type.Fraction) assert.strictEqual(a.toString(), '0.(6)') @@ -66,6 +66,28 @@ describe('ceil', function () { assert.strictEqual(ceil(fraction(-2.1)).toString(), '-2') }) + it('should gracefully handle round-off errors', function () { + assert.strictEqual(ceil(3.0000000000000004), 3) + assert.strictEqual(ceil(7.999999999999999), 8) + assert.strictEqual(ceil(-3.0000000000000004), -3) + assert.strictEqual(ceil(-7.999999999999999), -8) + assert.strictEqual(ceil(30000.000000000004), 30000) + assert.strictEqual(ceil(799999.9999999999), 800000) + assert.strictEqual(ceil(-30000.000000000004), -30000) + assert.strictEqual(ceil(-799999.9999999999), -800000) + }) + + it('should gracefully handle round-off errors with bignumbers', function () { + assert.deepStrictEqual(ceil(bignumber(3.0000000000000004)), bignumber(3)) + assert.deepStrictEqual(ceil(bignumber(7.999999999999999)), bignumber(8)) + assert.deepStrictEqual(ceil(bignumber(-3.0000000000000004)), bignumber(-3)) + assert.deepStrictEqual(ceil(bignumber(-7.999999999999999)), bignumber(-8)) + assert.deepStrictEqual(ceil(bignumber(30000.000000000004)), bignumber(30000)) + assert.deepStrictEqual(ceil(bignumber(799999.9999999999)), bignumber(800000)) + assert.deepStrictEqual(ceil(bignumber(-30000.000000000004)), bignumber(-30000)) + assert.deepStrictEqual(ceil(bignumber(-799999.9999999999)), bignumber(-800000)) + }) + it('should throw an error for units', function () { assert.throws(function () { ceil(unit('5cm')) }, TypeError, 'Function ceil(unit) not supported') }) diff --git a/test/function/arithmetic/fix.test.js b/test/function/arithmetic/fix.test.js index f46fe28fb..5974a8ab7 100644 --- a/test/function/arithmetic/fix.test.js +++ b/test/function/arithmetic/fix.test.js @@ -67,6 +67,28 @@ describe('fix', function () { assert.strictEqual(fix(fraction(-2.1)).toString(), '-2') }) + it('should gracefully handle round-off errors', function () { + assert.strictEqual(fix(3.0000000000000004), 3) + assert.strictEqual(fix(7.999999999999999), 8) + assert.strictEqual(fix(-3.0000000000000004), -3) + assert.strictEqual(fix(-7.999999999999999), -8) + assert.strictEqual(fix(30000.000000000004), 30000) + assert.strictEqual(fix(799999.9999999999), 800000) + assert.strictEqual(fix(-30000.000000000004), -30000) + assert.strictEqual(fix(-799999.9999999999), -800000) + }) + + it('should gracefully handle round-off errors with bignumbers', function () { + assert.deepStrictEqual(fix(bignumber(3.0000000000000004)), bignumber(3)) + assert.deepStrictEqual(fix(bignumber(7.999999999999999)), bignumber(8)) + assert.deepStrictEqual(fix(bignumber(-3.0000000000000004)), bignumber(-3)) + assert.deepStrictEqual(fix(bignumber(-7.999999999999999)), bignumber(-8)) + assert.deepStrictEqual(fix(bignumber(30000.000000000004)), bignumber(30000)) + assert.deepStrictEqual(fix(bignumber(799999.9999999999)), bignumber(800000)) + assert.deepStrictEqual(fix(bignumber(-30000.000000000004)), bignumber(-30000)) + assert.deepStrictEqual(fix(bignumber(-799999.9999999999)), bignumber(-800000)) + }) + it('should throw an error on unit as parameter', function () { // unit assert.throws(function () { fix(unit('5cm')) }, TypeError, 'Function fix(unit) not supported') diff --git a/test/function/arithmetic/floor.test.js b/test/function/arithmetic/floor.test.js index 63530529d..76c5d9787 100644 --- a/test/function/arithmetic/floor.test.js +++ b/test/function/arithmetic/floor.test.js @@ -66,6 +66,26 @@ describe('floor', function () { assert.strictEqual(floor(fraction(-2.1)).toString(), '-3') }) + it('should gracefully handle round-off errors', function () { + assert.strictEqual(floor(3.0000000000000004), 3) + assert.strictEqual(floor(7.999999999999999), 8) + assert.strictEqual(floor(-3.0000000000000004), -3) + assert.strictEqual(floor(-7.999999999999999), -8) + assert.strictEqual(floor(30000.000000000004), 30000) + assert.strictEqual(floor(799999.9999999999), 800000) + assert.strictEqual(floor(-30000.000000000004), -30000) + }) + + it('should gracefully handle round-off errors with bignumbers', function () { + assert.deepStrictEqual(floor(bignumber(3.0000000000000004)), bignumber(3)) + assert.deepStrictEqual(floor(bignumber(7.999999999999999)), bignumber(8)) + assert.deepStrictEqual(floor(bignumber(-3.0000000000000004)), bignumber(-3)) + assert.deepStrictEqual(floor(bignumber(-7.999999999999999)), bignumber(-8)) + assert.deepStrictEqual(floor(bignumber(30000.000000000004)), bignumber(30000)) + assert.deepStrictEqual(floor(bignumber(799999.9999999999)), bignumber(800000)) + assert.deepStrictEqual(floor(bignumber(-30000.000000000004)), bignumber(-30000)) + }) + it('should throw an error with a unit', function () { assert.throws(function () { floor(unit('5cm')) }, TypeError, 'Function floor(unit) not supported') }) From 30b45d86336659e46a0b1eb8cd5d684ef25ec8b3 Mon Sep 17 00:00:00 2001 From: Eric Mansfield Date: Sun, 10 Mar 2019 06:39:13 -0600 Subject: [PATCH 2/2] Fuzzy range endpoint (#1434) * Fixed unit base recognition and formatting for user-defined units * Began writing tests for range * Use fuzzy comparisons for detecting range endpoint --- src/function/matrix/range.js | 20 ++++++++++++-------- test/function/matrix/range.test.js | 12 ++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/function/matrix/range.js b/src/function/matrix/range.js index 4978235fb..b18e845b3 100644 --- a/src/function/matrix/range.js +++ b/src/function/matrix/range.js @@ -2,6 +2,10 @@ function factory (type, config, load, typed) { const matrix = load(require('../../type/matrix/function/matrix')) + const smaller = load(require('../relational/smaller')) + const larger = load(require('../relational/larger')) + const smallerEq = load(require('../relational/smallerEq')) + const largerEq = load(require('../relational/largerEq')) const ZERO = new type.BigNumber(0) const ONE = new type.BigNumber(1) @@ -131,12 +135,12 @@ function factory (type, config, load, typed) { const array = [] let x = start if (step > 0) { - while (x < end) { + while (smaller(x, end)) { array.push(x) x += step } } else if (step < 0) { - while (x > end) { + while (larger(x, end)) { array.push(x) x += step } @@ -157,12 +161,12 @@ function factory (type, config, load, typed) { const array = [] let x = start if (step > 0) { - while (x <= end) { + while (smallerEq(x, end)) { array.push(x) x += step } } else if (step < 0) { - while (x >= end) { + while (largerEq(x, end)) { array.push(x) x += step } @@ -183,12 +187,12 @@ function factory (type, config, load, typed) { const array = [] let x = start if (step.gt(ZERO)) { - while (x.lt(end)) { + while (smaller(x, end)) { array.push(x) x = x.plus(step) } } else if (step.lt(ZERO)) { - while (x.gt(end)) { + while (larger(x, end)) { array.push(x) x = x.plus(step) } @@ -209,12 +213,12 @@ function factory (type, config, load, typed) { const array = [] let x = start if (step.gt(ZERO)) { - while (x.lte(end)) { + while (smallerEq(x, end)) { array.push(x) x = x.plus(step) } } else if (step.lt(ZERO)) { - while (x.gte(end)) { + while (largerEq(x, end)) { array.push(x) x = x.plus(step) } diff --git a/test/function/matrix/range.test.js b/test/function/matrix/range.test.js index c76e973dc..301c82165 100644 --- a/test/function/matrix/range.test.js +++ b/test/function/matrix/range.test.js @@ -84,6 +84,18 @@ describe('range', function () { assert.throws(function () { bigmath.range('1:a') }, /is no valid range/) }) + it('should gracefully handle round-off errors', function () { + assert.deepStrictEqual(range(1, 2, 0.1, true)._size, [11]) + assert.deepStrictEqual(range(0.1, 0.2, 0.01, true)._size, [11]) + assert.deepStrictEqual(range(1, 5, 0.1)._size, [40]) + assert.deepStrictEqual(range(2, 1, -0.1, true)._size, [11]) + assert.deepStrictEqual(range(5, 1, -0.1)._size, [40]) + assert.deepStrictEqual(range(-3.2909135802469143, 3.2909135802469143, (3.2909135802469143 + 3.2909135802469143) / 10, true)._size, [11]) + assert.deepStrictEqual(range(-3.2909135802469143, 3.2909135802469143, (3.2909135802469143 + 3.2909135802469143) / 9, true)._size, [10]) + assert.deepStrictEqual(range(-3.2909135802469143, 3.2909135802469143, (3.2909135802469143 + 3.2909135802469143) / 10)._size, [10]) + assert.deepStrictEqual(range(-3.2909135802469143, 3.2909135802469143, (3.2909135802469143 + 3.2909135802469143) / 9)._size, [9]) + }) + describe('option includeEnd', function () { it('should parse a string and include end', function () { assert.deepStrictEqual(range('1:6', false), matrix([1, 2, 3, 4, 5]))