Typorithm · easy

Decimal quantities and explicit rounding

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

Step 1 of 4 · intro card

Decimal quantities and explicit rounding

0%

Concept and contract

Keep decimal inputs exact and state when and how a report rounds its result.

Binary floating-point is useful, but it is not an exact representation of every decimal quantity supplied by an API. Decimal can represent decimal strings without first introducing a binary float approximation.

This fictional exercise totals nonnegative amounts and rounds once at the output boundary. It deliberately excludes exchange rates, currency conversion and compensation normalization; those are additional domain policies, not properties of the Decimal class. The bounded input uses at most one thousand fixed-point ASCII strings, each at most sixteen characters and four fractional digits; scientific notation is excluded.

Mechanism and worked example

The function receives bounded strings, creates Decimal values inside a local precision context, rejects non-finite and negative quantities, then sums with a Decimal zero.

The function receives bounded strings, creates Decimal values inside a local precision context, rejects non-finite and negative quantities, then sums with a Decimal zero. Quantizing to 0.01 with ROUND_HALF_UP states both the number of decimal places and the tie rule.

Returning a formatted string preserves trailing zeros in JSON output. The local context avoids changing another component’s process-wide decimal precision policy.

Complete worked example

python

import re
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext

def amount(values):
    if not isinstance(values, list) or len(values) > 1000:
        raise ValueError("at most 1000 amounts required")
    with localcontext() as context:
        context.prec = 28
        try:
            numbers = []
            for value in values:
                if not isinstance(value, str) or len(value) > 16 or not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,4})?", value):
                    raise ValueError("bounded decimal strings required")
                number = Decimal(value)
                if not number.is_finite() or number < 0:
                    raise ValueError("finite nonnegative values required")
                numbers.append(number)
            total = sum(numbers, Decimal("0"))
            return format(total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), "f")
        except InvalidOperation as error:
            raise ValueError("invalid decimal amount") from error
Reasoning
Keep decimal inputs exact and state when and how a report rounds its result. Compare the stated values and failure boundary before changing the implementation.

Counterexamples and common mistakes

The counterexample rounds each input before summing.

The counterexample rounds each input before summing. Two values of 0.005 then produce a different total from summing their exact values and rounding once.

Constructing Decimal from a float can also preserve the float approximation rather than the intended decimal text. NaN and Infinity are values the library can represent, so parsing success alone is not a sufficient domain validation. State a finite, bounded input contract explicitly.

Counterexample: a runnable policy defect

python

import re
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext

def amount(values):
    if not isinstance(values, list) or len(values) > 1000:
        raise ValueError("at most 1000 amounts required")
    with localcontext() as context:
        context.prec = 28
        try:
            numbers = []
            for value in values:
                if not isinstance(value, str) or len(value) > 16 or not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,4})?", value):
                    raise ValueError("bounded decimal strings required")
                number = Decimal(value)
                if not number.is_finite() or number < 0:
                    raise ValueError("finite nonnegative values required")
                numbers.append(number.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP))
            total = sum(numbers, Decimal("0"))
            return format(total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), "f")
        except InvalidOperation as error:
            raise ValueError("invalid decimal amount") from error
Reasoning
This runnable counterexample violates the declared contract; the companion lab requires it to fail.

Independent practice: API client task

Change the operation into a mean: require at least one input, divide the exact total by the number of inputs and round once at the end.

Change the operation into a mean: require at least one input, divide the exact total by the number of inputs and round once at the end. Keep all input restrictions.

Begin with two values whose exact average is already two decimals, then try a rounding boundary and an empty list. Do not average values after separately rounding them, and do not switch back to float for the division.

Step 1: Compute a mean of at least one accepted amount. Step 2: Divide before the single final rounding step.

Practice starter: solve before reading the solution

python

import re
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext

def amount(values):
    # Remove this placeholder, then adapt the retained baseline:
    # Compute a mean of at least one accepted amount.
    raise NotImplementedError
    if not isinstance(values, list) or len(values) > 1000:
        raise ValueError("at most 1000 amounts required")
    with localcontext() as context:
        context.prec = 28
        try:
            numbers = []
            for value in values:
                if not isinstance(value, str) or len(value) > 16 or not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,4})?", value):
                    raise ValueError("bounded decimal strings required")
                number = Decimal(value)
                if not number.is_finite() or number < 0:
                    raise ValueError("finite nonnegative values required")
                numbers.append(number)
            total = sum(numbers, Decimal("0"))
            return format(total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), "f")
        except InvalidOperation as error:
            raise ValueError("invalid decimal amount") from error
Reasoning
Compute a mean of at least one accepted amount. Divide before the single final rounding step.
  • Hint 1: Use the number of accepted input values as the denominator.
  • Hint 2: Keep the calculation in the local Decimal context.

Solution, tests and trade-offs

The practice solution performs division inside the same local context and uses the original number of accepted inputs as its denominator.

The practice solution performs division inside the same local context and uses the original number of accepted inputs as its denominator. Empty input is rejected rather than presented as a measured zero average.

Cases include trailing zeros, non-finite strings, a boolean and two small values exposing premature rounding. These checks establish the stated arithmetic policy, not the correctness of a financial report with unspecified units or missing observations.

Practice solution and acceptance checks

python

import re
from decimal import Decimal, InvalidOperation, ROUND_HALF_UP, localcontext

def amount(values):
    if not isinstance(values, list) or len(values) > 1000:
        raise ValueError("at most 1000 amounts required")
    with localcontext() as context:
        context.prec = 28
        try:
            numbers = []
            for value in values:
                if not isinstance(value, str) or len(value) > 16 or not re.fullmatch(r"[0-9]+(?:\.[0-9]{1,4})?", value):
                    raise ValueError("bounded decimal strings required")
                number = Decimal(value)
                if not number.is_finite() or number < 0:
                    raise ValueError("finite nonnegative values required")
                numbers.append(number)
            if not numbers:
                raise ValueError("mean needs observations")
            total = sum(numbers, Decimal("0")) / len(numbers)
            return format(total.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP), "f")
        except InvalidOperation as error:
            raise ValueError("invalid decimal amount") from error
Reasoning
Use the number of accepted input values as the denominator. Keep the calculation in the local Decimal context.

Interview reasoning and next step

An interviewer may ask whether Decimal alone makes money calculations correct.

An interviewer may ask whether Decimal alone makes money calculations correct. Explain that units, inclusion rules, precision, rounding and missing values still need decisions.

The worked contract has one output rounding boundary and a documented finite range of inputs. When connecting the result to an API report, keep an observed zero separate from an absent measurement and publish the denominator used for a calculated mean.

Optional stretch — after the core task works: Add an explicit unit field and reject a mixture of different units.

Computer practice: https://applydjinn.com/typorithm/python-automation-lab-20261105.zip. Run verify.py using your selected Python interpreter, then adapt the retained exercise baseline and check it with --practice followed by the lesson id. The standard-library lab uses owned temporary 127.0.0.1 loopback servers, fictional pages and disposable files; it never scans production feeds or opens the working ApplyDjinn database.

Keep decimal inputs exact and state when and how a report rounds its result.