Typorithm · medium

Money, rounding and UTC normalization

pythonetldata-engineeringindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Money, rounding and UTC normalization

0%

Concept and contract

Use decimal source values and an explicit rounding policy; reject timestamps without a timezone.

Normalization should remove accidental representation differences without hiding business decisions. Monetary strings become integer cents using Decimal and a named rounding mode. Timestamps become UTC only after the input supplies an offset. A naive wall-clock time has no unambiguous UTC equivalent under this source contract.

The baseline selects ROUND_HALF_EVEN when quantizing to two decimal places. That policy is explicit and testable, not a universal accounting rule. The example accepts a bounded teaching input; a production contract would also define finite values, currency, precision limits and how invalid or overflowing amounts are quarantined.

Mechanism and worked example

Decimal receives the original price string, then quantize applies the chosen cent precision before conversion to an integer.

Decimal receives the original price string, then quantize applies the chosen cent precision before conversion to an integer. The timestamp parser returns an aware datetime, and astimezone converts its represented instant to UTC. The report contains both the integer amount and normalized ISO timestamp.

For 2.675 the string-based half-even calculation yields 268 cents. A different case using 1.005 distinguishes tie-breaking policies. Follow the offset across conversion rather than merely replacing a timezone label: attaching UTC without shifting the clock would describe a different instant. The naive timestamp case must raise ValueError.

Complete worked example

python

from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN

def inspect_example(price, timestamp):
    amount = Decimal(price).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
    instant = datetime.fromisoformat(timestamp)
    if instant.utcoffset() is None:
        raise ValueError('explicit timezone required')
    return {'amount_cents': int(amount * 100),
            'utc': instant.astimezone(timezone.utc).isoformat()}
Reasoning
Decimal receives the original price string, then quantize applies the chosen cent precision before conversion to an integer. The timestamp parser returns an aware datetime, and astimezone converts its represented instant to UTC. The report contains both the integer amount and normalized ISO timestamp.
Download the ETL and Data Engineering computer lab ↗

Focused decision: use the complete example for context

python

amount = Decimal(price).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
Decision
Use the declared half-up rounding policy in the practice task without passing through a binary float.
Download the ETL and Data Engineering computer lab ↗

Counterexamples and common mistakes

The mutation constructs Decimal from a float conversion of the price string.

The mutation constructs Decimal from a float conversion of the price string. The intermediate binary approximation can fall on a different side of a rounding boundary, so the first monetary case no longer produces the expected cents. Decimal cannot reconstruct the original decimal intent after that information has been lost.

Timezone mistakes can be equally subtle. Calling replace(tzinfo=UTC) labels the existing clock reading and does not perform offset conversion. Neither successful parsing nor a familiar printed date establishes the correct instant. Tests should use nonzero offsets and include an input with no offset to expose an unjustified assumption.

Controlled bug: observe the rejected data contract

python

from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN

def inspect_example(price, timestamp):
    amount = Decimal(float(price)).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
    instant = datetime.fromisoformat(timestamp)
    if instant.utcoffset() is None:
        raise ValueError('explicit timezone required')
    return {'amount_cents': int(amount * 100),
            'utc': instant.astimezone(timezone.utc).isoformat()}
Reasoning
The mutation constructs Decimal from a float conversion of the price string. The intermediate binary approximation can fall on a different side of a rounding boundary, so the first monetary case no longer produces the expected cents. Decimal cannot reconstruct the original decimal intent after that information has been lost.
Download the ETL and Data Engineering computer lab ↗

Independent practice

Change only the monetary rounding policy to ROUND_HALF_UP.

Change only the monetary rounding policy to ROUND_HALF_UP. Preserve string-based Decimal construction, integer cents, UTC conversion and rejection of naive timestamps. The half-cent tie case should change while the timestamp observations remain identical. A new policy should not accidentally relax a separate data contract.

Import the new rounding constant explicitly and pass it into quantize. Use the declared cases to compare baseline and solution, especially 1.005. Avoid replacing the result with a hand-written adjustment to the last digit; the decimal operation already defines how ties are treated across the supported numeric inputs.

Step 1: Use the declared half-up rounding policy in the practice task without passing through a binary float. Step 2: Edit the owned exercise file and run verify.py --practice etl-normalize. Preserve caller inputs and the independently declared failure cases.

Practice starter: solve before reading the solution

python

from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN

def inspect_example(price, timestamp):
    amount = Decimal(price).quantize(Decimal('0.01'), rounding=ROUND_HALF_EVEN)
    instant = datetime.fromisoformat(timestamp)
    if instant.utcoffset() is None:
        raise ValueError('explicit timezone required')
    return {'amount_cents': int(amount * 100),
            'utc': instant.astimezone(timezone.utc).isoformat()}

# Independent task: Use the declared half-up rounding policy in the practice task without passing through a binary float.
Reasoning
Use the declared half-up rounding policy in the practice task without passing through a binary float.
Download the ETL and Data Engineering computer lab ↗
  • Hint 1: Change only the monetary rounding policy to ROUND_HALF_UP. Preserve string-based Decimal construction, integer cents, UTC conversion and rejection of naive timestamps. The half-cent tie case should change while the timestamp observations remain identical. A new policy should not accidentally relax a separate data contract.
  • Hint 2: Import the new rounding constant explicitly and pass it into quantize. Use the declared cases to compare baseline and solution, especially 1.005. Avoid replacing the result with a hand-written adjustment to the last digit; the decimal operation already defines how ties are treated across the supported numeric inputs.

Solution, tests and trade-offs

The accepted solution names half-up rounding at the calculation site.

The accepted solution names half-up rounding at the calculation site. Independent cases show that 1.005 becomes 101 cents while the other observations still satisfy their contracts. Separating amount and instant assertions helps identify which normalization decision changed when a later review updates a policy.

Document the policy together with source precision and currency. For real systems, version the transformation so a replay can explain why historical totals differ after a rule change. Do not silently recompute published financial reports under a new rounding mode without a reconciliation process, even when the new calculation is locally consistent.

Practice solution and acceptance checks

python

from datetime import datetime, timezone
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP

def inspect_example(price, timestamp):
    amount = Decimal(price).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
    instant = datetime.fromisoformat(timestamp)
    if instant.utcoffset() is None:
        raise ValueError('explicit timezone required')
    return {'amount_cents': int(amount * 100),
            'utc': instant.astimezone(timezone.utc).isoformat()}
Reasoning
The accepted solution names half-up rounding at the calculation site. Independent cases show that 1.005 becomes 101 cents while the other observations still satisfy their contracts. Separating amount and instant assertions helps identify which normalization decision changed when a later review updates a policy.
Download the ETL and Data Engineering computer lab ↗

Interview reasoning and next step

Explain the difference between parsing, quantization and unit conversion, and why their order matters.

Explain the difference between parsing, quantization and unit conversion, and why their order matters. Then explain the difference between changing a timezone label and converting an instant. These are two examples of the same engineering habit: preserve meaning while making representation deliberate and reviewable.

For a stretch, add accepted negative and offset-boundary examples, then explicitly reject nonfinite amounts under a strengthened source contract. Write separate expected outputs for each rounding mode rather than inferring one from the other. Keep these experiments in the owned lab instead of introducing live exchange rates or external timezone assumptions.

Optional stretch — after the core task works: Add finite-value validation and negative rounding ties as separate source-contract changes.

Computer practice: https://applydjinn.com/typorithm/etl-data-engineering-lab-20261107.zip. Use an isolated Python 3.13 environment, install requirements.txt and run verify.py. Edit exercises/etl-normalize.py and run --practice etl-normalize. Each case owns its files and database; Parquet uses the pinned DuckDB engine. The recovery probe terminates a separate interpreter and reopens the SQLite file. The app retains static examples, prepared results and questions without an editor or code execution. Use the course Download button once to save every lesson and illustration; opening an unsaved course does not automatically download it.

Use decimal source values and an explicit rounding policy; reject timestamps without a timezone.