Skip to main content
Estimate rounding (calculators 2.1.0) lets an estimator round a linked quantity, and the sell rates of a tender estimate, up to a chosen step. The contract lives in packages/calculators/src/roundUp.ts in stralos-domain. For the user guide, see Rounding and Sell rate rounding.
This is not rounding.ts, the half-up 2 dp snap every calculator quantity passes through. Estimate rounding is an explicit choice to lift a figure up to a step.

Options

Each list is closed and in dropdown order. none and the plain steps are shared spellings.

Linked quantity (QUANTITY_ROUNDING_OPTIONS)

Sell rate (SELL_RATE_ROUNDING_OPTIONS)

There is no ends:9999 option; 10000 is the largest sell-rate step. A stored token outside the lists is treated as unknown and left unrounded. Use quantityRoundingLabel(option) and sellRateRoundingLabel(option) for dropdown text rather than formatting a token yourself. isQuantityRoundingOption and isSellRateRoundingOption are the type guards.

Rules

roundUp(value, option) is pure and never throws. It keeps four rules, each pinned by a test:
  1. Always up (ceiling), never nearest. A value already on a valid point stays where it is.
  2. Two kinds of step. A plain step S is a multiple: ⌈x ÷ S⌉ × S (123.56 with 10 gives 130). An ends:E step is the next number ending in E: with M the smallest power of ten above E, ⌈(x − E) ÷ M⌉ × M + E (123.56 with ends:0.9 gives 123.9; with ends:9, 129).
  3. Exact decimals. Tokens are decimal strings and figures go through scaled BigInts. A number is read as the decimal it prints as at 15 significant figures, so 0.1 + 0.2 rounds as 0.3.
  4. Zero and negatives are left alone. Zero stays zero under every option, so an ends: step never lifts a zero rate to 0.09. A negative figure has no agreed “up” and is returned as is.
none, an unknown token, NaN, and ±Infinity also come back unchanged. roundUpDecimal(value, option) applies the same rounding to a ScaledDecimal ({ units: bigint; scale: number }) for a caller that already holds one. The answer’s scale is the finer of the value’s and the step’s. suggestQuantityRounding(value) returns the step that rounds to three significant figures: 10^(⌊log10 v⌋ − 2), clamped to [0.001, 10000]. The exponent comes from the decimal’s digits, not Math.log10.

Linked quantities

QuantityLinkV1.rounding is applied after Σ × multiplier + offset, always up. The resolver reports the figure before rounding as unrounded, and quantityLinkRuns adds the uplift to the last run so the runs still sum to the rounded quantity. See Linked work-item quantity. api-estimate refuses a rounding on a build-up line priced in whole stock (422 component_link_rounding_whole_stock), because whole-stock ordering already rounds each run up. An unknown token on a work-item link is 422 quantity_link_invalid.

Sell rates in api-estimate

The estimate’s sellRateRounding (default none) applies to every item without its own. A work item’s sellRateRounding overrides it: null inherits the estimate and none keeps the item unrounded. An unknown token is 422 sell_rate_rounding_invalid, and a frozen estimate refuses the change with 409 estimate_frozen. The rounding sits inside the item-rate snap (src/pricing/itemRate.ts): rate0 = half-up 2 dp (price ÷ quantity), rate = roundUp(rate0, option), price = round2(quantity × rate). The stored unitRate is the rounded rate. unitRateUnrounded holds rate0 only when rounding moved it. The uplift is extra sell (margin); the build-up underneath never moves. Rows that carry no rate (provisional sums, quantity zero or less) are never rounded, and neither is any row in a Shared Costs section (effectiveSellRateRounding returns none): the pool is spread into the schedule items, which round themselves. With a rounding the snap is not idempotent below a quantity of 1, so every writer snaps a row once per pass from its rolled-up price and keeps the result. The estimate-rounding PostHog flag hides both controls in the frontend only. A stored rounding keeps applying when the flag is off. See Estimate rounding in the estimate frontend notes.