Repository: josdejong/mathjs
Pull request #867 (2017-06-05 04:26:32)

Splitunit nearlyequal
Added checks in `splitUnit` to correct for round-off error
Minor edit in docs


Patch:
diff --git a/docs/datatypes/units.md b/docs/datatypes/units.md
index b63470ece..148654456 100644
--- a/docs/datatypes/units.md
+++ b/docs/datatypes/units.md
@@ -187,10 +187,10 @@ length, mass, etc.
 Test whether a unit equals an other unit. Units are equal
 when they have the same base and same value when normalized to SI units.
 
-### unit.format([precision])
+### unit.format([options])
 Get a string representation of the unit. The function
-will determine the best fitting prefix for the unit. If precision is defined,
-the units value will be rounded to the provided number of digits.
+will determine the best fitting prefix for the unit. See the [Format](../functions/format.md)
+page for available options.
 
 ### unit.fromJSON(json)
 Revive a unit from a JSON object. Accepts
diff --git a/lib/type/unit/Unit.js b/lib/type/unit/Unit.js
index 2c88ef200..46eceec4d 100644
--- a/lib/type/unit/Unit.js
+++ b/lib/type/unit/Unit.js
@@ -12,6 +12,7 @@ function factory (type, config, load, typed, math) {
   var pow       = load(require('../../function/arithmetic/pow'));
   var abs       = load(require('../../function/arithmetic/abs'));
   var fix       = load(require('../../function/arithmetic/fix'));
+  var round     = load(require('../../function/arithmetic/round'));
   var equal     = load(require('../../function/relational/equal'));
   var isNumeric = load(require('../../function/utils/isNumeric'));
   var format    = load(require('../../function/string/format'));
@@ -1224,14 +1225,42 @@ function factory (type, config, load, typed, math) {
     var x = this.clone();
     var ret = [];
     for(var i=0; i<parts.length; i++) {
+      // Convert x to the requested unit
       x = x.to(parts[i]);
       if(i==parts.length-1) break;
-      // fix rounds a number towards 0
-      var fixedVal = fix(x.toNumeric());
-      var y = new Unit(fixedVal, parts[i].toString());
+
+      // Get the numeric value of this unit
+      var xNumeric = x.toNumeric();
+
+      // Check to see if xNumeric is nearly equal to an integer,
+      // since fix can incorrectly round down if there is round-off error
+      var xRounded = round(xNumeric);
+      var xFixed;
+      var isNearlyEqual = equal(xRounded, xNumeric);
+      if (isNearlyEqual) {
+        xFixed = xRounded;
+      }
+      else {
+        xFixed = fix(x.toNumeric());
+      }
+
+      var y = new Unit(xFixed, parts[i].toString());
       ret.push(y);
       x = subtract(x, y);
     }
+
+    // This little bit fixes a bug where the remainder should be 0 but is a little bit off.
+    // But instead of comparing x, the remainder, with zero--we will compare the sum of
+    // all the parts so far with the original value. If they are nearly equal,
+    // we set the remainder to 0.
+    var testSum = 0;
+    for(var i=0; i<ret.length; i++) {
+      testSum = add(testSum, ret[i].value);
+    }
+    if(equal(testSum, this.value)) {
+      x.value = 0;
+    }
+
     ret.push(x);
 
     return ret;


Test patch:
diff --git a/test/type/unit/Unit.test.js b/test/type/unit/Unit.test.js
index 78bf71823..8587ad49b 100644
--- a/test/type/unit/Unit.test.js
+++ b/test/type/unit/Unit.test.js
@@ -1171,6 +1171,18 @@ describe('Unit', function() {
       assert.equal((new Unit(1, 'm')).splitUnit(['in', 'ft']).toString(), "39 in,0.030839895013123605 ft");
       assert.equal((new Unit(1, 'm')).splitUnit([ new Unit(null, 'ft'), new Unit(null, 'in') ]).toString(), "3 ft,3.3700787401574765 in");
     });
+
+    it('should be resistant to round-off error', function() {
+
+      assert.equal((new Unit(-12, 'in')).splitUnit(['ft', 'in']).toString(), "-1 ft,0 in");
+      assert.equal((new Unit(12, 'in')).splitUnit(['ft', 'in']).toString(), "1 ft,0 in");
+      assert.equal((new Unit(24, 'in')).splitUnit(['ft', 'in']).toString(), "2 ft,0 in");
+      assert.equal((new Unit(36, 'in')).splitUnit(['ft', 'in']).toString(), "3 ft,0 in");
+      assert.equal((new Unit(48, 'in')).splitUnit(['ft', 'in']).toString(), "4 ft,0 in");
+      assert.equal((new Unit(60, 'in')).splitUnit(['ft', 'in']).toString(), "5 ft,0 in");
+      assert.equal((new Unit(36000, 'in')).splitUnit(['ft', 'in']).toString(), "3000 ft,0 in");
+
+    });
   });
 
   describe('toSI', function() {