Lodash _.ceil Negative Precision Logic Explained
Lodash’s _.ceil method handles rounding to specified
decimal places or tens places by manipulating numbers through
exponential string notation rather than arithmetic multiplication and
division. When configured with a heavily negative precision—rounding to
large powers of ten such as hundreds, thousands, or far beyond—it
bypasses standard binary floating-point representation drift before
delegating the final integer rounding to JavaScript’s native
Math.ceil. This article breaks down the exact algorithmic
steps, IEEE 754 precision dynamics, and edge cases that occur when
rounding with large negative precisions in Lodash.
The Underlying Mechanism: Exponential String Shifting
In standard JavaScript, rounding to multiples of ten using
multiplication or division often introduces floating-point errors due to
IEEE 754 binary fractions (for example, 6040 / 100 might
result in tiny inaccuracies in certain intermediate steps). To prevent
this, Lodash generates _.ceil via an internal
createRound factory function that uses exponential
notation:
function createRound(methodName) {
const func = Math[methodName];
return (number, precision) => {
precision = precision == null ? 0 : Math.min(toInteger(precision), 292);
if (precision) {
let pair = `${number}e`.split('e');
const value = func(`${pair[0]}e${+pair[1] + precision}`);
pair = `${value}e`.split('e');
return +`${pair[0]}e${+pair[1] - precision}`;
}
return func(number);
};
}Instead of performing mathematical shifts like
number * Math.pow(10, precision), Lodash converts the
number into a string and splits it by the exponent marker
'e'.
Execution Flow for Negative Precision
When a negative precision is provided (e.g.,
_.ceil(6040, -2)):
- String Conversion and Splitting: Lodash appends
'e'to the input number and splits it. For6040,${6040}e.split('e') yields['6040', '']. - Forward Decimal Shift: The precision is added to
the existing exponent. Here,
+pair[1] + precisioncomputes0 + (-2) = -2. The string passed toMath.ceilis'6040e-2', which JavaScript converts directly into60.4. - Integer Rounding:
Math.ceil(60.4)executes natively, returning61. - Reverse Shift: The integer result is split again
(
['61', '']) and the exponent is reversed:+pair[1] - precisionevaluates to0 - (-2) = 2. - Final Type Coercion: The unary plus coerces the
string
'61e2'back into a numeric value:6100.
Behavior Under Heavily Negative Precision
Lodash includes an upper clamp for positive precision
(Math.min(toInteger(precision), 292)) to avoid exceeding
JavaScript's maximum safe double-precision exponent of 308.
However, Lodash does not apply a corresponding lower clamp (such as
Math.max(precision, -292)). Consequently, supplying heavily
negative precisions triggers the lower-end boundary limits of IEEE 754
floating-point arithmetic.
1. Underflow to Zero
In 64-bit floating-point numbers, subnormal numbers terminate near
5e-324. If the negative precision forces the shifted
exponential string below this threshold (for instance,
precision = -350 on a standard integer):
- The shifted string
'100e-350'evaluates to0during numeric parsing. Math.ceil(0)yields0.- The reverse shift calculates
0 - (-350) = 350, producing'0e350'. - Parsing
+'0e350'results in0.
2. Upward Rounding and Exponent Overflow
If the number is small but positive, and shifted such that it does
not completely underflow to 0 (evaluating instead as a
subnormal float like 1e-320):
Math.ceil('1e-320')rounds any positive value strictly greater than0up to1.- The reverse shift calculates
1e - (-precision), such as+'1e350'. - Because double-precision floats overflow to infinity beyond
approximately
1.7976931348623157e+308, the expression evaluates toInfinity.
Summary
Lodash's _.ceil avoids floating-point calculation error
by transforming numbers into exponential strings, applying native
Math.ceil to the shifted value, and reversing the exponent.
When subjected to heavily negative precisions, the absence of a minimum
boundary clamp means the output is dictated entirely by IEEE 754
underflow (collapsing to 0) or post-rounding exponent
overflow (escalating to Infinity).