Skip to content

This site describes solve-engine 2.44.0, the version npm installs.

Currency

Package: CURRENCY_PACKAGE. Registered by createEngine(); for a slimmer engine, register it explicitly (see choosing packages).

Money is written the way you would say it: a symbol like $, or the currency’s name in words. Amounts in the same currency add and subtract like ordinary numbers, and converting from one currency to another looks up a live exchange rate over the network.

$100 + $50 // $150.00
10 dollars // $10.00

An amount below zero is written with its sign first, the way a statement shows a debit: the minus comes before the symbol, not between the symbol and the digits.

$50 - $80 // -$30.00
-£3.50 // -£3.50

pounds and lb are the unit of mass, not sterling, since a line like 10 pounds in kg is far more often weight than money. Write British pounds with their symbol or their code, £10 or 10 GBP. A mass asked for in money is refused by name, rather than converted at some rate.

10 pounds in kg // 4.54 kg
10 GBP // £10.00

Symbols and words are both recognised. One symbol can stand for several currencies: $ is the US dollar by default, and also the Canadian, Australian and other dollars. Write the currency’s three-letter code after the amount to say which one is meant. A code for a currency with a different symbol, or any other unit after the amount, is refused rather than dropped.

$5 CAD in CAD // $5.00
¥500 CNY in CNY // ¥500.00

Much of the world writes the symbol after the amount, 100 € or 12 kr, and a dollar is often named by its country, A$ for the Australian dollar. Both are read, and so is every amount Solve writes, so an answer copied from one line can be typed into another and mean the same money.

A symbol after the amount is the same money as the symbol before it, with or without a space:

100 € // €100.00
1,000 ₹ // ₹1,000.00
12 ₽ // 12.00 ₽
-100 € // -€100.00

Some currencies are written with letters after the amount, and those letters read back the same way. kr is written for the Swedish, Norwegian and Danish crowns and reads as the Swedish krona, the way $ reads as the US dollar; write NOK or DKK for the others. The letters are matched exactly as written, so Ft is the forint and ft stays the foot.

12.00 kr + 1 SEK // 13.00 kr
12 zł // 12.00 zł
12 Ft // 12.00 Ft
12 Kč // 12.00 Kč
12 Fr // 12.00 Fr

A dollar named by its country has the letters touching the $: A$ (Australian), C$ (Canadian), US$, HK$ (Hong Kong), NZ$ (New Zealand), S$ (Singapore), MX$ (Mexican) and R$ (the Brazilian real). The answer is written with a plain $, as every dollar is, and stays in the currency named:

A$100 + $5 AUD // $105.00
R$12.00 // R$12.00

The rand is written R12.00, and read in that form: the sign touching an amount with its cents. A bare R stays a name, since people use it for a resistance, a radius or the gas constant, so 12 R multiplies by whatever R is, and R12 without cents can be the name of a resistor.

R12.00 + R1,234.56 // R1,246.56

The common codes can be typed in lower case, 100 usd or 50 eur. Several codes are words or units in lower case (cup is the cooking unit, try and mad are words), so only a chosen list is read that way: usd, eur, gbp, jpy, cny, chf, cad, aud, nzd, hkd, sgd, sek, nok, dkk, pln, czk, huf, inr, krw, brl, mxn, zar, ils, thb, aed, sar, myr, idr, vnd, ngn, uah and twd. Any other code is written in capitals.

100 usd // $100.00
50 eur + 50 EUR // €100.00

The boundary: a symbol several currencies share reads as its default, so an amount the engine writes for one of the others reads back as the default. 12 AUD is written $12.00, which reads as US dollars; 12 NOK is written 12.00 kr, which reads as Swedish kronor; 12 CNY is written ¥12.00, which reads as yen, and is then shown as yen are, in whole yen (¥12, see money precision). Keep the code (12 AUD) where the currency matters.

Where the symbol goes in an answer follows the reader’s language when the app showing the notes is set to one. The examples here are written the English way, €1,234.50; a reader whose app is set to German sees 1.234,50 €, the symbol after the amount as German writes it, and a German note reads that form back in as the same money. Only the place and the separators change: the symbol, the cents and the rounding are the same in every language. See formatting results.

Inside the brackets of a function call, or of a list, a comma separates one value from the next: rgb(255,255,255) is three numbers, and max(1,000, 2) is the largest of 1, 0 and 2. An amount of money is the exception. A comma after an amount with its currency sign in front, followed by exactly three digits and then the end of the amount (a comma, a decimal point, a closing bracket or a space), groups the thousands as it does anywhere else, since nobody writes a list as $1 then 000:

compoundInterest($1,000, 5%, 3) // $1,157.63
max($1,000, $2) // $1,000.00
max($1,000.50, $2) // $1,000.50
max(1,000, 2) // 2

The boundary is the ambiguity the rule cannot resolve. max($1,234) is read as the one amount $1,234; to mean $1 and 234, put a space after the comma, as max($1, 234). A plain number keeps the separator reading, so write max(1000, 2) for a thousand. The one other exception is a range’s bound, whose comma straight against the range’s colon groups the thousands, so sum(1,000:2,000) is the range from one thousand to two thousand (see large bounds). An amount with its currency after it (1,000 USD) inside a call is not read as grouped either: write the sign in front, or no comma.

In India a hundred thousand is one lakh, written 1,00,000, and ten million is one crore, 1,00,00,000: the last three digits form a group, and every group before them is two digits. An amount written this way is read beside the rupee sign or after it the code INR, and is the same amount the ordinary grouping gives:

₹1,00,000 // ₹100,000.00
₹1,00,00,000 // ₹10,000,000.00
12,34,567 INR // ₹1,234,567.00
₹12,34,567.89 // ₹1,234,567.89

Without a rupee marker, 12,34,567 is refused in an English engine, because outside the Indian convention a group of two digits is not a group and a refusal is safer than a guess. An engine created with an Indian tag such as en-IN reads the grouping everywhere, and a host that formats results with en-IN gets them back the same way (₹12,34,567.89); see locales.

Conversion between currencies reaches the network and resolves asynchronously. See async and live data.

ExpressionResult
10 USD in GBPconverted at the current rate
100 euros to dollarsthe same, in words
100 USD in GBP on 2024-01-15converted at that day’s rate
100 USD in GBP on 15 Jan 2024the same day, written differently
10 USD in GBP frozenconverted once, then kept at that amount

An on <date> suffix converts at the rate for the day it names rather than today’s, which is what an expense or an invoice reconciled after the fact needs: a note that was right when written should not drift as the market moves. Both date spellings above are read by the same parser used everywhere else. Like the live conversion, the first result is a pending value and the real answer arrives later.

An exchange rate is how much of one currency buys one unit of another, and it moves through the day. So that everyone can agree on one figure for a given day, the European Central Bank publishes a set of reference rates each working day, for about thirty currencies against the euro. The engine asks Frankfurter, a free service that serves those reference rates with no key, for the rate on the day you name. It is the same service the live conversion uses, so a live rate and a past one come from one source. With the rate fetched for 15 January 2024 (0.78441 pounds to the dollar), 100 USD in GBP on 2024-01-15 is £78.44, and the record of where the rate came from names Frankfurter and that day.

A weekend or a bank holiday has no rate of its own: the one in force is the last rate published before it, so on 2024-01-13 (a Saturday) converts at Friday’s rate and the record says Friday. A resolved historical rate never goes stale, since the rate on a fixed past day does not change.

Some dated conversions are refused, each with a sentence saying why, rather than answered with a figure from somewhere else:

ExpressionResult
100 USD in GBP on 1998-06-01refused: the reference rates begin on 4 January 1999
100 USD in GBP on 2090-01-01refused: no rate has been published for a future day
1 BTC in USD on 2024-01-15refused: the reference rates cover currencies, not cryptocurrencies

The first boundary is a choice about where figures come from, not a limit of the service: Frankfurter does answer some earlier dates, but from another source, and a rate from before the euro is not a European Central Bank reference rate. A currency the bank does not quote is refused the same way. The live conversion routes cryptocurrencies to CoinGecko, which has no dated equivalent here.

With live data switched off (network.enabled: false, see switching live data off) a dated conversion makes no request and says the setting is why.

A host with its own source of rates (a bank feed, an accounting system, a paid service that reaches further back) supplies it as historicalRateProvider, and it takes the place of Frankfurter for every dated conversion. It receives the two currency codes, the day as YYYY-MM-DD and an abort signal to pass to its fetch, and returns the rate as a number, or as { rate, asOf } when the rate it found was published for an earlier day than the one asked about:

import { createCurrencyPackage } from "solve-engine/packages";
const currency = createCurrencyPackage({
historicalRateProvider: async (from, to, isoDate, signal) => {
const res = await fetch(`https://example.com/fx/${isoDate}?from=${from}&to=${to}`, { signal });
return (await res.json()).rate;
},
historicalProviderName: "Example FX",
});

createCurrencyPackage() with no argument is the default already in BUILTIN_PACKAGES. Build your own and substitute it into the engine’s packages array. historicalProviderName says whose rates they are in the record (host when left out). A provider that answers with something that is not a positive number is refused rather than converted through. Passing historicalRateProvider: null keeps the on <date> form but declines every source, and a dated conversion then reports that historical rates are not configured rather than falling back to today’s rate.

Every converted amount records where its rate came from: the provider (Frankfurter, the European Central Bank’s reference rates, for the built-in live and historical rates; the name a host gives for its own), whether the rate was fetched live, supplied by the host, or looked up for a past day, and when. The record travels with every line computed from the amount, so an app can show “reference rate, 23 Sep 16:02” beside a total built from converted lines and mark those lines as depending on a rate. Amounts in one currency involve no rate and record nothing. See async and live data for how a host reads it.

Rates from different sources are kept side by side. A note that converts $100 in EUR on one line and $100 in BTC on the next fetches the euro rate from Frankfurter and the bitcoin price from CoinGecko, and both lines convert; each records its own provider. A rate a host supplied is kept beside the engine’s own in the same way.

To stop a converted amount moving with the market, end the line with frozen: it keeps the first answer, with the date it was fixed. See frozen answers.