# Formatting Values

tinytab has three functions for turning a single number into a display string. They share the same rules: the value is the first argument, and every option is a keyword argument.


# Numbers

[`fmt_number()`](../reference/fmt_number.md) rounds to a fixed number of decimal places (2 by default) and groups thousands with a separator:


``` python
from tinytab import fmt_number

fmt_number(1234567.891)
```


    '1,234,567.89'


Use `decimals=` to change the precision:


``` python
fmt_number(1234567.891, decimals=0)
```


    '1,234,568'


The thousands separator is a comma by default. Set another with `sep_mark=`:


``` python
fmt_number(1234567.891, sep_mark=" ")
```


    '1 234 567.89'


# Percentages

[`fmt_percent()`](../reference/fmt_percent.md) takes a proportion, where `1` means 100%, and shows one decimal place by default:


``` python
from tinytab import fmt_percent

fmt_percent(0.256)
```


    '25.6%'


``` python
fmt_percent(0.256, decimals=0)
```


    '26%'


# Currencies

[`fmt_currency()`](../reference/fmt_currency.md) adds a currency symbol and uses the right number of decimal places for the currency. US dollars are the default:


``` python
from tinytab import fmt_currency

fmt_currency(1234.5)
```


    '$1,234.50'


Choose another currency with a three-letter code (in any case). Yen are shown without decimal places, and negative amounts put the sign before the symbol:


``` python
[
    fmt_currency(1234.5, currency="eur"),
    fmt_currency(1234.5, currency="JPY"),
    fmt_currency(-5, currency="GBP"),
]
```


    ['€1,234.50', '¥1,234', '-£5.00']


The supported codes are USD, EUR, GBP, and JPY. Any other code is an error, and the message lists the valid ones:


``` python
fmt_currency(10, currency="XYZ")
```


    ---------------------------------------------------------------------------
    ValueError                                Traceback (most recent call last)
    Cell In[8], line 1
    ----> 1 fmt_currency(10, currency="XYZ")

    File ~/py_projects/example-python-package/tinytab/_formats.py:136, in fmt_currency(x, currency)
        132 if code not in CURRENCY_SYMBOLS:
        133     # List the valid choices in the message, so the user can fix the call without having to
        134     # look anything up. (Joining a dict iterates over its keys.)
        135     valid = ", ".join(CURRENCY_SYMBOLS)
    --> 136     raise ValueError(f"Unknown currency {currency!r}. Valid currencies: {valid}.")
        138 decimals = 0 if code == "JPY" else 2
        139 # Format the absolute value and put the sign in front of the symbol ("-$5.00"). Formatting
        140 # the negative number directly would put the sign after it ("$-5.00").

    ValueError: Unknown currency 'XYZ'. Valid currencies: USD, EUR, GBP, JPY.


# Only numbers

All three functions accept only numbers (integers and floats). Anything else, including a number written as a string, raises a `TypeError`:


``` python
fmt_number("10")
```


    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    Cell In[9], line 1
    ----> 1 fmt_number("10")

    File ~/py_projects/example-python-package/tinytab/_formats.py:68, in fmt_number(x, decimals, sep_mark)
         44 def fmt_number(x: float, *, decimals: int = 2, sep_mark: str = ",") -> str:
         45     """Format a number with a fixed number of decimal places and a thousands separator.
         46 
         47     Parameters
       (...)     66     '1,235'
         67     """
    ---> 68     _check_number(x)
         69     if decimals < 0:
         70         raise ValueError(f"`decimals` must be zero or greater, got {decimals!r}.")

    File ~/py_projects/example-python-package/tinytab/_formats.py:34, in _check_number(x, arg)
         28 def _check_number(x: object, arg: str = "x") -> None:
         29     # `bool` is a subclass of `int`, but formatting `True` as "1.00" is almost always a mistake
         30     if isinstance(x, bool) or not isinstance(x, (int, float)):
         31         # The error message names the argument, shows the bad value, and says what was expected
         32         # (practices doc, item 21). `!r` formats the value with `repr()`, so a string shows up
         33         # with quotes ('10') and can't be mistaken for a number.
    ---> 34         raise TypeError(f"Expected a number for `{arg}`, got {type(x).__name__}: {x!r}")

    TypeError: Expected a number for `x`, got str: '10'


`True` and `False` are rejected too, even though Python treats them as the integers 1 and 0. Formatting a boolean as "1.00" is almost always a mistake.
