Skip to content

← Pattern Matching, f-Strings and Newer Syntax step 4 of 23

Easy Primitives

Format Table Rows and Money Amounts

This exercise puts the last two together. You write a row renderer that takes its alignment and width from data, as in "Line Up Receipt Columns with f-Strings". You also write a Money class with its own format codes, as in "Give a Class Its Own Format Codes".

render_row([["Widget", "<"], ["12", ">"]], [10, 5])  # 'Widget    |   12'

m = Money(Decimal("-1234.5"), "USD")
f"{m}"             # 'USD -1,234.50'
f"{m:plain}"       # '-1234.50'
f"{m:accounting}"  # '(USD 1,234.50)'
f"{m:.3f}"         # '-1234.500'

Render a row

Fill in render_row(cells, widths):

  • Each cell is a pair [text, align], where align is <, > or ^. Each width is a whole number, zero or more.
  • Format each cell with one f-string that takes its alignment and width from nested braces, as in f"{text:{align}{width}}". Don't pad by hand or call ljust or rjust.
  • Join the formatted cells with "|".
  • A width is a minimum. Text longer than its width stays whole.
  • Pair cells with widths using zip(cells, widths, strict=True), so lists of different lengths raise ValueError.
  • Empty cells and widths give "".

Give Money its format codes

Money holds a Decimal amount and a currency label, which is always "USD" in the tests. Fill in __format__ so it handles each format spec like this:

  • "" is what f"{m}" passes. It gives the currency, a space, and the amount with comma grouping and two decimal places.
  • "plain" gives the amount with two decimal places, without the currency or grouping.
  • "accounting" gives the currency and the amount without its sign, grouped, with two decimal places. If the amount is below zero, wrap the whole result in parentheses.
  • A spec that starts with . and ends with f, such as .3f, goes to the Decimal with format(self.amount, format_spec). A malformed one, such as .xf, raises ValueError there.
  • Any other spec raises ValueError.

The / in __format__(self, format_spec: str, /) means callers must pass the spec by position, as Python does when it calls __format__. A later exercise in this track, on positional-only and keyword-only parameters, covers / fully.

Decimal edge cases

  • Get the amount without its sign from self.amount.copy_abs(). abs() can round the value to the decimal context's precision, 28 digits by default, and copy_abs() never rounds.
  • Decide on the parentheses from the stored amount, not from the rounded text. -0.001 rounds to 0.00 but is below zero, so it gives "(USD 0.00)".
  • Decimal("-0") is negative zero. It is not below zero, so "accounting" gives "USD 0.00". The other specs keep its minus sign.

Write solve

solve(cells, widths, amounts, spec) returns a dictionary with the keys "row", "money" and "error":

  • Render the row first, outside any try, and let a row error propagate. Only money formatting errors go in "error".
  • Turn each amount into Money(Decimal(amount), "USD"), and format each one with format(money, spec).
  • On success, return {"row": row, "money": [...], "error": ""}, with the formatted amounts in order.
  • If formatting raises ValueError, return {"row": row, "money": [], "error": "ValueError"}. Drop any amounts you already formatted.
  • An empty amounts list formats nothing, so even an unsupported spec gives "money": [] and "error": "".