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.](../../images/3793488963.webp)
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.](../../images/3840478385.webp)
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.](../../images/3804367964.webp)
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.](../../images/3805547622.webp)
...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.](../../images/3811642468.webp)
Now we can take the floor.
return Math.floor(num * -20 - 0.2);

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.](../../images/3821603967.webp)
Shift the returned value over to the dot by adding 4:
return Math.floor(num * -20 - 0.2) * 5 + 4;

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.](../../images/3823111302.webp)
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.](../../images/3785886764.webp)
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.](../../images/3821931644.webp)
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)}`);
}