Gracie williams
Gracie williams

Reputation: 1145

Custom Rounding up to nearest 0.05

I want to do custom round up to near 0.05 , based on following condition.It is hard to explain , but following example will be easy to understand .

12.910 - 12.90
12.920 - 12.90
12.930 - 12.90
12.940 - 12.90
12.941 - 12.95
12.950 - 12.95

12.960 - 12.95
12.970 - 12.95
12.980 - 12.95
12.990 - 12.95
12.991 - 13.00 
13.000 - 13.00 

I tried several function , but it is rounding up 12.98 to 13.00.

function   customRound( num) {
    return Math.round(num * 20) / 20;
}

Upvotes: 1

Views: 343

Answers (3)

AuxTaco
AuxTaco

Reputation: 5171

Coming at this visually, your rounding algorithm seems to look like this:
A number line, extending from 12.94 to 13.04, with the interval (12.94, 12.99] marked in red and the interval (12.99, 13.04] marked in blue. A red dot is at 12.95, and a blue dot is at 13.00.
The dot is where you want to round to for that interval. ( marks the open end of an interval, ] the closed end. (12.99 belongs to the red interval.) We'll implement this algorithm by manipulating the line to match Math.floor's.

First, let's work with integers.

num * 100

The same number line, but the scale has changed. Line: [1294, 1304], red: (1294, 1299] with dot at 1295, blue: (1299, 1304] with dot at 1300.

Your rounding interval is left-open and right-closed, but Math.floor is left-closed and right-open. We can flip the line to match by multiplying by −1:

  num *  100 * -1
⇒ num * -100

The number line has been flipped. Line: [-1304, -1294], blue: [-1304, -1299) with dot at -1300, red: [-1299, -1294) with dot at -1295.

Your rounding intervals' lengths are 5, so we need to put the ends of the intervals on multiples of 5...

num * -100 - 1

The intervals have shifted 1 towards negative infinity. Line: [-1305, -1295], blue: [-1305, -1300) with dot at -1301, red: [-1299, -1294) with dot at -1296.

...before dividing by 5 to match Math.floor.

 (num * -100 - 1  ) / 5
⇒ num *  -20 - 0.2

The scale has changed again. Line: [-261, -259], blue: [-261, -260) with dot at -260.2, red: [-260, -259) with dot at -259.2.

Now we can take the floor.

return Math.floor(num * -20 - 0.2);

The same number line, now with Math.floor arrows! Blue arrow pointing to -261, red arrow pointing to -260.

Scale back up to the original by multiplying by 5:

return Math.floor(num * -20 - 0.2) * 5;

Another scale change. Line: [-1305, -1295], blue: [-1305, -1300) with dot at -1301 and arrow at -1305, red: [-1299, -1294) with dot at -1296 and arrow at -1300.

Shift the returned value over to the dot by adding 4:

return Math.floor(num * -20 - 0.2) * 5 + 4;

The arrows have moved. Blue arrow pointing to -1301, red arrow pointing to -1296.

Undo the alignment we did earlier:

  return Math.floor(num * -20 - 0.2) * 5 + 4 + 1;
⇒ return Math.floor(num * -20 - 0.2) * 5 + 5;

The intervals have shifted 1 towards positive infinity. Line: [-1304, -1294], blue: [-1304, -1299) with dot and arrow at -1300, red: [-1299, -1294) with dot and arrow at -1295.

Undo the flip:

  return (Math.floor(num * -20 - 0.2) *  5 + 5) * -1;
⇒ return  Math.floor(num * -20 - 0.2) * -5 - 5;

The number line has been flipped again. Line: [1294, 1304], red: (1294, 1299] with dot and arrow at 1295, blue: (1299, 1304] with dot and arrow at 1300.

And divide the whole thing by 100 to get your original scale back:

  return (Math.floor(num * -20 - 0.2) * -5    - 5) / 100;
⇒ return  Math.floor(num * -20 - 0.2) * -0.05 - 0.05;

The original number line is back, now with arrows pointing at the dots. Line: [12.94, 13.04], red: (12.94, 12.99] with arrow and dot at 12.95, blue: (12.99, 13.04] with arrow and dot at 13.00.

Using Robin Zigmond's testing framework,

function customRound(num) {
    return Math.floor(num * -20 - 0.2) * -0.05 - 0.05;
}

// test desired results
var tests = [12.91, 12.92, 12.93, 12.94, 12.941, 12.95, 12.96, 12.97, 12.98, 12.99, 12.991, 13];

for (var i=0; i<tests.length; i++) {
  console.log(`${tests[i].toFixed(3)} - ${customRound(tests[i]).toFixed(2)}`);
}

Upvotes: 2

Robin Zigmond
Robin Zigmond

Reputation: 18249

As far as I can tell from your example, the desired behaviour appears to be "round up to the nearest 0.01, then round that result down to the nearest 0.05".

This can be implemented as follows. As you can see, it agrees exactly with your examples (I even took care to format it the same way) - but please let me know if I've got the wrong end of the stick.

function customRound(num) {
    var intermediateResult = Math.ceil(num*100)/100;
    return Math.floor(intermediateResult*20)/20;
}

// test desired results
var tests = [12.91, 12.92, 12.93, 12.94, 12.941, 12.95, 12.96, 12.97, 12.98, 12.99, 12.991, 13];

for (var i=0; i<tests.length; i++) {
  console.log(`${tests[i].toFixed(3)} - ${customRound(tests[i]).toFixed(2)}`);
}

Upvotes: 1

Brian Hadley
Brian Hadley

Reputation: 135

If you really intended to round everything within < .01 of the nearest .05 then try the below, to get the precision of the number it uses answer from Is there a reliable way in JavaScript to obtain the number of decimal places of an arbitrary number?

function decimalPlaces(n) {
  var s = "" + (+n);
  var match = /(?:\.(\d+))?(?:[eE]([+\-]?\d+))?$/.exec(s);
  if (!match) { return 0; }  
  return Math.max(
      0,  // lower limit.
      (match[1] == '0' ? 0 : (match[1] || '').length)  
      - (match[2] || 0));  
}

var test = 12.941;
var factor = Math.pow(10,decimalPlaces(test));
var remainder = ((test * factor) % (.05 * factor))/factor;

var result;

if (remainder>.04) {
    result = Math.round(test*20)/20;
} else {
    result = (test*factor - remainder*factor)/factor;
}

console.log('result is:',result);

Upvotes: 1

Related Questions