This site describes solve-engine 2.44.0, the version npm installs.
Currency
Package:
CURRENCY_PACKAGE. Registered bycreateEngine(); 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.0010 dollars // $10.00An 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.50pounds 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 kg10 GBP // £10.00Symbols 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.00Writing money as it is written
Section titled “Writing money as it is written”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.001,000 ₹ // ₹1,000.0012 ₽ // 12.00 ₽-100 € // -€100.00Some 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 kr12 zł // 12.00 zł12 Ft // 12.00 Ft12 Kč // 12.00 Kč12 Fr // 12.00 FrA 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.00R$12.00 // R$12.00The 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.56The 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.0050 eur + 50 EUR // €100.00The 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.
Amounts inside a call
Section titled “Amounts inside a call”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.63max($1,000, $2) // $1,000.00max($1,000.50, $2) // $1,000.50max(1,000, 2) // 2The 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.
Indian grouping
Section titled “Indian grouping”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.0012,34,567 INR // ₹1,234,567.00₹12,34,567.89 // ₹1,234,567.89Without 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.
| Expression | Result |
|---|---|
10 USD in GBP | converted at the current rate |
100 euros to dollars | the same, in words |
100 USD in GBP on 2024-01-15 | converted at that day’s rate |
100 USD in GBP on 15 Jan 2024 | the same day, written differently |
10 USD in GBP frozen | converted 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.
Where a past day’s rate comes from
Section titled “Where a past day’s rate comes from”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:
| Expression | Result |
|---|---|
100 USD in GBP on 1998-06-01 | refused: the reference rates begin on 4 January 1999 |
100 USD in GBP on 2090-01-01 | refused: no rate has been published for a future day |
1 BTC in USD on 2024-01-15 | refused: 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.
Using your own historical rates
Section titled “Using your own historical rates”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.
Where a rate came from
Section titled “Where a rate came from”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.