Keep Units and Rounding Separate in a Coding Brief

Drafted and reviewed using AI assistance.

On this page

For a length field, specify the input unit, canonical stored value, calculation precision, and display rounding separately. Showing three decimal places should not silently replace a more precise stored quantity.

“Support metres and millimetres, rounded nicely” leaves several decisions open. The builder must know whether 1 m equals 1000 mm, whether negative values are valid, and what happens when a rounded display is reopened for editing.

This guide defines those decisions for a small fictional length-entry application. Its data and rules are teaching examples, not measurement specifications or requirements for financial or medical software.

Start with a unit and input dictionary

The example accepts a decimal string and a separate, mandatory unit selector:

Unit value Meaning Convert to canonical millimetres Maximum fractional digits in input
m metres Multiply by exactly 1000 6
mm millimetres Keep the value 3

NIST's SI prefix table defines milli as one-thousandth, supporting the relation 1000 mm = 1 m. The input limits and rounding choices below are our application policy; they do not come from the prefix definition.

Choose this bounded input grammar:

  • Accept non-negative ASCII decimal strings with . as the decimal point.
  • Require digits before the decimal point. Allow 0, but no extra leading zeroes such as 01.
  • Accept at most 11 characters and the unit-specific fractional limits above.
  • Reject signs, exponents, commas, whitespace, empty strings, and special values such as NaN.
  • Accept canonical lengths from 0 through 1000000 mm, inclusive. Zero is valid; negative values are not.
  • Require m or mm; never guess a unit from the number's size.

The two fractional limits are deliberate. Every accepted quantity can be expressed exactly using at most three fractional millimetre digits or six fractional metre digits. Switching an edit field between the two units therefore does not require inventing more input precision or discarding part of the value.

These are narrow rules for an exercise. An application that accepts localized number formats should define that parsing policy separately rather than silently interpreting a comma in multiple ways.

Separate storage, calculation, and presentation

Concern Chosen rule Why it is separate
Original input Retain the source string and its unit in this example They explain what was entered
Canonical quantity Store exact decimal millimetres; serialize that quantity as a decimal string A transport number must not quietly introduce a binary-float conversion
Calculation Use decimal values with a 28-significant-digit context for this bounded conversion/addition model Precision is an arithmetic setting, not the number of visible decimal places
Metre display Three places, ROUND_HALF_UP Presentation may discard detail without changing the quantity
Millimetre display One place, ROUND_HALF_UP The two displays use different scales
Edit buffer Show an unrounded decimal value in the selected unit Editing should begin from the quantity, not from its rounded label

For example, an authored stored record could be:

{
  "sourceText": "0.0004",
  "sourceUnit": "m",
  "millimetres": "0.4"
}

The original string is not the same thing as a binary floating-point value later converted to a string. Python's 3.12 decimal documentation explains constructors, arithmetic contexts, and fixed-exponent quantize operations. Converting a float captures that float's value; it does not recover an earlier decimal text input. Decimal arithmetic also remains subject to its chosen precision and rounding settings.

The teaching calculation here uses Python 3.12.14, verified separately from the documentation page. Its arithmetic choices are scoped to this small conversion/addition model.

Work through exact strings before designing the field

These manually specified expectations distinguish the quantity from each display:

Input string Unit Canonical mm Metre display, 3 places Millimetre display, 1 place
1 m 1000 1.000 1000.0
1000 mm 1000 1.000 1000.0
0.0005 m 0.5 0.001 0.5
0.0004 m 0.4 0.000 0.4
12.345 mm 12.345 0.012 12.3
12.35 mm 12.35 0.012 12.4
1.234567 m 1234.567 1.235 1234.6
0.001 mm 0.001 0.000 0.0
999.999999 m 999999.999 1000.000 1000000.0

The first two rows are the same quantity expressed in different units. The third row fixes a halfway case: the chosen half-up rule shows 0.001 m. A different rounding mode could produce a different display, so “round normally” is not a sufficient requirement.

The fourth and eighth rows show that a positive quantity may display as zero at the selected precision. The last row shows a display rounding up to the maximum while the stored quantity remains slightly below it. None of those displays should overwrite the canonical value.

For the table's half-up boundary, the relevant Python operation is:

from decimal import Decimal, ROUND_HALF_UP

shown = Decimal("0.0005").quantize(
    Decimal("0.001"), rounding=ROUND_HALF_UP
)

The example's unrounded input, not a previously formatted display, is what enters that operation.

Calculate first and round the final display

Consider adding two lengths of 0.0004 m. Their canonical values are 0.4 mm each, so the sum is 0.8 mm, or 0.0008 m. The chosen metre display of the sum is 0.001 m.

If the application instead adds the two rounded displays, it adds 0.000 + 0.000 and gets 0.000. The display has become an unintended calculation input.

For this bounded model, convert accepted input strings to canonical values, perform the defined calculation, and quantize only the output being displayed. This rule does not promise exact results for every possible future operation: division, much larger values, or a different precision context need their own specified treatment.

Reopen a rounded value without changing it

Use the fourth table row as an editing history:

Step Screen or action Canonical value afterward
Enter 0.0004 m Save accepted exact input 0.4 mm
View the metre label Display 0.000 m Still 0.4 mm
Open the editor in metres Edit buffer is 0.0004, not 0.000 Still 0.4 mm
Save without editing Preserve the quantity Still 0.4 mm
Deliberately replace the edit buffer with 0 and save Commit a new quantity 0 mm

Changing a display-unit preference should also leave the quantity unchanged. A saved value and a preference for how to view it are different pieces of state.

Make invalid input outcomes explicit

Input and unit Chosen outcome
1, no unit Reject: choose a unit
1,2 m Reject: comma is outside this grammar
-1 mm Reject: negative lengths are not accepted
NaN m or 1e3 mm Reject: special values and exponent syntax are not accepted
0.0001 mm Reject: more than three fractional millimetre digits
1000.000001 m Reject: greater than the canonical maximum
Empty input, 1 m, or 01 mm Reject the empty, whitespace, or leading-zero form

Rejection should leave the last saved canonical quantity unchanged. The builder still needs to implement and check the actual field's error and editing behavior; an arithmetic helper alone does not provide it.

Copy this length-field brief

Plan a length field for the fictional metre/millimetre rules in this guide.

Input:
Separate exact decimal string and required unit m or mm. No float-first
parsing. Use non-negative ASCII digits, an optional dot/fraction, a leading
zero below one, no extra leading zeroes, no sign/exponent/comma/whitespace.
Maximum 11 characters; at most 6 fractional places in m or 3 in mm.
Canonical range: 0 through 1000000 mm inclusive. Reject invalid input
without changing the last saved quantity.

Store/calculate/display:
Convert m to mm by exactly 1000. Retain source string/unit separately
from canonical decimal mm, serialized as a decimal string.
Use a 28-significant-digit decimal context for this bounded model.
Display m at 3 places and mm at 1 place, ROUND_HALF_UP.
Do not use rounded display text as storage or calculation input.

Expected canonical mm for the nine examples:
1 m -> 1000; 1000 mm -> 1000; 0.0005 m -> 0.5;
0.0004 m -> 0.4; 12.345 mm -> 12.345; 12.35 mm -> 12.35;
1.234567 m -> 1234.567; 0.001 mm -> 0.001;
999.999999 m -> 999999.999.
Use the guide's complete table for both display results.

Editing:
0.4 mm displays as 0.000 m, but opens as exact 0.0004 in the metre editor.
Saving unchanged or switching display units preserves 0.4 mm.
Only an explicit edit to zero commits zero.
Adding 0.0004 m twice gives 0.8 mm, displayed as 0.001 m.

Inspect the current parse/storage/calculation/edit paths. Return a bounded
plan and local-fixture checks covering the table and invalid forms.
Identify every existing float or rounded-string conversion on that path.
Do not extend this example into measurement tolerances or other domains.
No database, live API, package installation, production edits or deployment
in this investigation step.

The authored table, invalid-input forms, exact edit buffers, and addition example were checked with a small offline Python 3.12.14 model. That result covers these calculations and chosen parsing rules; it does not establish how an application's UI, transport, or persistent storage behaves.