Accounting for floating point error when rounding
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)
    })
})