Rounding Rules Need One Honest Home
A safe refactor can turn scattered rounding arithmetic into explicit domain policies without changing behavior, as long as each meaning keeps its own name and boundary tests.
Flow
A safe extraction workflow
1Inventory
List each expression and the decision its result supports.
2Classify
Give each distinct meaning a separate policy name.
3Extract
Move pure arithmetic while preserving operands and order.
4Verify
Test boundary examples and protect caller behavior.
5Document
Record callers and related rules that remain intentionally separate.
When a codebase contains repeated case arithmetic, the first temptation is to create one helper named roundCaseQuantity and replace every expression. That is fast, but it hides the very differences that make quantity logic risky. A safer implementation path starts with semantics, not deduplication.
First, build a small inventory. For every expression involving a case size, record the input quantity, the divisor, the operation, and the decision that consumes the result. Ask a plain-language question: is this count paying for enough cases, counting only complete cases, checking whether a total is whole, or splitting one case between recipients? If the question changes, the function name should change.
Second, define the narrow primitives. A generic policy surface might include a coverage count that rounds up, a complete-case count that rounds down, a remainder, and lower and upper halves for a balanced split. Keep them pure and keep their contracts short. Do not add database reads, logging, or business branching to a calculation module. Those responsibilities make it harder to compare the extracted function with the expression it replaced.
Third, migrate one caller at a time. Preserve the exact operands and operation order during the first pass. If the original code normalizes the quantity before taking a remainder, leave that normalization in the caller unless it is part of the shared contract. If a caller uses a capacity guard around a half-case calculation, keep the guard in the caller. The extraction should clarify the arithmetic, not silently redesign the surrounding policy.
Use worked examples before running the whole suite. With twenty-five units and twelve units per case, coverage returns three, complete cases returns two, and the remainder returns one. With twelve units, coverage and complete cases both return one and the remainder is zero. With a five-unit case, the lower and upper halves are two and three. With a one-unit case, the halves are zero and one. These examples expose accidental use of the wrong primitive immediately.
Fourth, write tests at two levels. Unit tests should state the contract of every primitive, including exact multiples, partial quantities, odd packs, and the smallest pack. Caller tests should prove that the planner or validator still produces the same meaningful outcome. If the system has a golden or characterization suite, run it as a behavior lock: a clean extraction should not change its established outputs.
Fifth, document the map. A short README should say which primitive answers which question and list the active callers. It should also call out similar-looking operations that are intentionally not included, such as nearest-case rounding with a tolerance or a fractional-cart rule. This is not documentation for documentation’s sake. It prevents the next contributor from treating the new module as a universal mathematical toolbox.
There are useful review checks. Confirm that each name describes a business meaning rather than a language operator. Confirm that no caller got a ceiling where it needs a floor. Confirm that an odd split still sums to the original case size. Confirm that invalid divisors are handled by the surrounding contract. Finally, check the diff for unrelated cleanup; small, focused migrations are easier to prove correct.
The tradeoff is a few more names and files. That cost is real, but it buys local reasoning. A reviewer can inspect caseCountCeil and ask whether the caller needs coverage. A test can state that halfCaseLower plus halfCaseUpper equals the pack size. A future policy change can be made deliberately instead of emerging from a copied expression. In systems where quantities cross planning, allocation, and purchase boundaries, that is a good trade.
The practical recipe is therefore: classify the meanings, extract honest primitives, preserve the old arithmetic, test distinguishing boundaries, document intentional non-unifications, and then consider any broader cleanup as a separate change.