Accounting for floating point error when rounding I have to implement some business logic that will round a number (typically a double) upwards. Given that this number comes from a third-party and I don't know how they come to calculate it I'm anticipating errors in it. My proposed routine to mitigate rounding errors (like Math.ceil(1.000000000000002) = 2) is as follows:
export const roundupDiscount = (discount: number) =&gt; {
    // We have to give advantage to the customer for the discount value when rounding,
    // that is to say, we will sacrifice the penny when we can't be exact. This means
    // we have to always round the discount upwards (so we can't Math.round because
    // half the time we'll round in the wrong direction.)
    // To account for numbers like, say, 1.000000000000002 (i.e. (1 / 5 / 7) * 5 * 7))
    // then we subtract off a &quot;small&quot; number (scaled with the magnitude of discount)
    // which will nudge below the number we will round up to

    if (!discount) return 0 // discount shouldn't be negative

    const magnitude = Math.floor(Math.log(discount) / Math.log(10))
    const epsilon = Math.pow(10, magnitude - 14)
    return Math.ceil(discount - epsilon)
}

Given that a double has between 15-17 significant digits in it, I'm choosing epsilon, arbitrarily, such that I have a couple of trailing digits to play with.
Can anybody spot any cases where this is going to get things wrong? simplify this thing, or, given this is node.js point me towards an npm module that does this kind of thing for me?
Note: I have to get this past code-review at work so I'd much prefer if there was a good library out there that I can use instead and not even bother submitting this!
Discovered this needs rework
Testing this with small numbers shows that for all practical purposes, this function is broken. I'd like roundupDiscount(Number.EPSILON) to be 0 (which could come about from subtracting two numbers meant to be equal, but differing by an error (similar to the classic (0.1 + 0.2) - 0.3)).
However for a number like Number.EPSILON its value, including significant digits, is 0.000000000000000222044604925031, and so the computed epsilon here being 10e-28 is way too small to have any effect. This function only really works when the integer part &gt; 0
It can be corrected in this respect by clamping the magnitude at 0 but I think I'll have a battle to convince anyone this should go into our codebase ...
Updated implementation:
export const roundupDiscount = (discount: number) =&gt; {
    // Given that discount is &quot;sensible&quot;, if it is equal to some integer x plus a
    // small positive error component, then it must return x. Otherwise it must
    // return Math.ceil(discount)
    
    if (discount &lt; 0 || discount &gt; Number.MAX_SAFE_INTEGER) return discount
    if (discount &lt; Number.EPSILON) return 0
        
    const magnitude = Math.floor(Math.max(0, Math.log(discount) / Math.log(10)))
    const epsilon = Math.pow(10, magnitude - 12) // treat last 3-4 significant digits as error
    return Math.abs(Math.ceil(discount - epsilon)) // Math.abs to prevent -0
} 

Test cases:
describe('roundupDiscount', () =&gt; {
    it('rounds the item discount correctly for numbers of differing magnitude', () =&gt; {
        let epsilon = Number.EPSILON; // machine epsilon - smallest positive number which, added to 1, will produce a value different from 1

        expect(roundupDiscount(1 + epsilon)).toBe(1)
        expect(roundupDiscount(1 + (10 * epsilon))).toBe(1)
        expect(roundupDiscount(1 + (100 * epsilon))).toBe(1)
        expect(roundupDiscount(1 + (1000 * epsilon))).toBe(1)
        expect(roundupDiscount(1 + (10000 * epsilon))).toBe(2) // &quot;error&quot; no longer small

        // test it handles large numbers ...
        const largeNum = 1000000000;
        const largeEpsilon = largeNum * epsilon;
        expect(roundupDiscount(largeNum + largeEpsilon)).toBe(largeNum)
        expect(roundupDiscount(largeNum + (10 * largeEpsilon))).toBe(largeNum)
        expect(roundupDiscount(largeNum + (100 * largeEpsilon))).toBe(largeNum)
        expect(roundupDiscount(largeNum + (1000 * largeEpsilon))).toBe(largeNum)
        expect(roundupDiscount(largeNum + (10000 * largeEpsilon))).toBe(largeNum + 1)

        // test it handles small numbers ...
        expect(roundupDiscount(epsilon)).toBe(0)
        expect(roundupDiscount(10 * epsilon)).toBe(0)
        expect(roundupDiscount(100 * epsilon)).toBe(0)
        expect(roundupDiscount(1000 * epsilon)).toBe(0)
        expect(roundupDiscount(10000 * epsilon)).toBe(1)
    })
})