I'm working on a quoting tool for a client. We started with integer pence. Reassuringly sensible. We have since dropped that representation because some of the prices don't fit.

A paper price of £4 per thousand sheets leaves me with 0.4 pence for each sheet. Multiply that by 25,000 sheets and I have 10,000 pence. That's £100 before any other charges. Round the unit price to zero whole pence first and the same multiplication gives away the paper.

I'm keen for the quoting tool to keep charging for things.

Paper sounds like such a harmless thing to sell, a nice stack of sheets. Then you ask how much one sheet costs and suddenly fractions of a penny need a great deal of your attention.

Oh, the currency too. It's easy to picture an amount in whichever currency I'm used to, then build as though every transaction will use it. The money type needs the actual currency alongside all the precision the amount requires, and when I save a quote both need to survive the database trip too.

An integer stores a count. Of what? “Store money as integers” leaves that decision to me. Whole pence represent £1.23 neatly. They look more settled until a unit price needs a fraction of one.

If the unit is one penny, 0.4 pence is unrepresentable. If the unit is a tenth of a penny, the price becomes four units, and the multiplication is exact at that scale:

4 tenths of a penny × 25,000 = 100,000 tenths of a penny
                            = 10,000 pence
                            = £100

Finer fixed-point integers are a valid option. Tenths of a penny would do for this price. Another fixed scale could work too, provided I check how small the prices can get and whether the totals fit. Division needs a rule as well. What happens when its result falls between two of my units?

I still like the simplicity of a count, but treating that as the end of the decision bothers me because the integer will preserve exactly what I give it.

If I give it zero because I've already rounded away the price, it will keep that zero beautifully.

Carry the amount and currency together

Now take £10 and US$10. Both have ten in them. Add the bare numbers and I get twenty. Twenty what?

The API I want refuses that addition. Ordering the amounts? That needs a deliberate policy too. A caller can't tell which amount is worth more from the two bare numbers.

Converting everything into my local currency would give me a common unit, but it creates another job. Which exchange rate? From when? And where have I kept the original amount? I only wanted to record the transaction. Now I'm being asked to make a conversion decision as well.

I'd rather keep its amount and currency together. When the application needs a conversion, it can supply a rate and ask for one explicitly. A different currency being inconvenient for my money type isn't a reason to convert a transaction.

This is one of the reasons I want a money type rather than amounts passed around on their own, so I can read an addition and know the currency check belongs there. Relying on every caller to remember what each number means is a lot of optimism to put into an arithmetic API.

For the amount itself, I'm using apd. Its context controls precision and rounding. A decimal combines a whole number with a power of ten. That whole number is its coefficient. For example, £0.004 is four times ten to the minus three pounds.

That lets me keep the 0.4 pence without first approximating it as a binary fraction. I can carry more precision through the calculation than the invoice displays, because the price has work left to do before it becomes a total.

Formatting it nicely shouldn't get to discard the digits that work still needs.

I haven't bought my way out of arithmetic decisions by choosing a decimal library. One divided by three has no finite decimal expansion. A percentage calculation can create more digits than the context keeps. How many should I retain? I still choose the precision and rounding point. Nor does the library know that one coefficient means pounds and another means dollars, so the money API around it has to check the currencies.

Keeping these decisions together would let me read the arithmetic policy in one place, rather than rediscover it wherever the quoting tool happens to multiply a price.

If I unwrap that money type when saving a quote and store only its amount, I've thrown away part of the value. What does 10 count when I read it back? Guessing from the customer's address or whichever currency the application defaults to puts the ambiguity straight back in.

A custom PostgreSQL composite type lets me give that pair one name in the database too. Here's the paper price as a currency_amount:

CREATE TYPE currency_amount AS (
    currency text,
    amount numeric
);

CREATE TABLE quote_line (
    unit_price currency_amount NOT NULL,
    CHECK (
        (unit_price).currency IS NOT NULL
        AND (unit_price).amount IS NOT NULL
    )
);

INSERT INTO quote_line (unit_price)
VALUES (ROW('GBP', 0.004)::currency_amount);

SELECT unit_price FROM quote_line;

Which unit?

The amount is in the currency's major unit. GBP with 0.004 means £0.004, or 0.4 pence. Pounds, not pence. That convention belongs to the type too because attaching GBP would do little good if half the callers meant pounds and the other half meant pence.

The numeric field has no fixed scale here. Declaring it as numeric(12,2) would round 0.004 to 0.00 on the way in. Wait, that's the unit price gone before I get to multiply it.

The type itself accepts null fields, so the table's CHECK requires both parts. NOT NULL rejects a missing unit_price altogether.

Both parts, please.

Reading unit_price gives back (GBP,0.004). The Go code has to decode both fields into the money type, so the next calculation gets the same currency checks it had before saving the price.

A trip through the database shouldn't turn my price into a bare number.

Take two charges of 0.5 pence each. Rounding each charge to a whole penny using half-up gives two pence, while adding the unrounded charges first and rounding the total gives one penny. Same charges, different answer.

This is the part I can't choose by picking my favourite number type. Does the application round each charge, or the total? Its pricing and accounting rules decide that. The calculation needs an explicit rounding point, rather than leaving that decision to whichever formatting call runs first.

Two decimal places aren't universal

Two decimal places are a habit from pounds and pence. Japanese yen normally display no fractional digits, while Kuwaiti dinars use three. Then there is the locale, which changes the decimal separator, the grouping and where the currency symbol goes, even when I'm displaying exactly the same amount.

bojanz/currency is a good library for this sort of work. Its Amount keeps an apd decimal and a currency code together, and refuses to add pounds to dollars. Its formatter uses Unicode's CLDR data to handle those currency and locale conventions.

Those display conventions belong to the invoice. The calculation keeps its own precision and chooses its own point to round a unit price.

Choose the tie-breaking rule too

Here's what half-up and half-even do with the same four amounts:

Value in penceHalf-up to whole penceHalf-even to whole pence
0.510
1.522
2.532
3.544

Half-even chooses the even result at a tie. That avoids always choosing the upper neighbour for positive midpoint values, though it can't promise an unbiased result for every distribution of inputs.

A unit-price conversion can round, a markup can round, formatting can round too, and spreading those calls around would give me several small decisions to remember every time I change a quote. I could keep looking for another conversion, another formatter, another place to discard a fraction. I want those decisions in the arithmetic API, where I can see which fractions are being discarded and why. The total can't restore those digits.

For the paper order, that means keeping the 0.4 pence until the multiplication has used it, with pounds still attached to the amount. Whole pence can describe the final £100 perfectly well. Asking them to describe the unit price first is where the paper becomes free.

Same concern about fractions of a penny. Slightly different plans for where they go. Watch on YouTube.

This entry came over from blog.haddon.me. I’ve tidied up the grammar and flow, bringing it closer to how I write now. I used AI to help with the grammar, structure and flow. I have dyslexia, and my writing often comes out as a rambling scrawl, so it can be hard to see what I was actually working on at the time. I think the tidied versions are easier to digest and more interesting to read. The artefacts got the same tidy-up while I was at it.