🤖 currency
amadan.net/rastrillo/rastrillo/currency
Formats, seeds and parses money: an integer count of a currency's
smallest unit, with its ISO 4217 code, written in a reader's language
from CLDR data. Forms and
Templates are the
guides; form and ui call this package for you.
Format and InputValue
func Format(locale string, minor int64, code string) (string, error)
func InputValue(locale string, minor int64, code string) (string, error)
Format writes an amount the way locale writes money: its digits, its
marks, its grouping, and where the symbol goes.
locale |
minor |
code |
Format |
|---|---|---|---|
en |
128450 | USD |
$1,284.50 |
de |
128450 | EUR |
1.284,50 € |
pt-PT |
128450 | EUR |
1284,50 € |
ja |
1285 | JPY |
¥1,285 |
en |
1500 | KWD |
KWD 1.500 |
en |
-500 | USD |
-$5.00 |
The currency comes from code, never from locale. Every digit of the
currency's smallest unit is shown, so nothing is rounded away. A code
outside the storage set is an error.
InputValue is the text to put in an input: the language's digits and
decimal mark, no group marks, no symbol. 150000 USD is 1500.00 in
en, and 128450 EUR is 1284,50 in de. The parser reads it back as
the same amount.
Codes
func Normalize(code string) (string, error)
func Exponent(code string) (int, error)
func EligibleDefault(code string) bool
Normalize trims the code, requires three ASCII letters, upper-cases
it, and requires it to be in the storage set: the 174 codes rastrillo
will store an amount in. Every entry point that takes a code calls it.
A code outside the set is refused, never given a guessed scale: KDW, a
typo for KWD, would otherwise store fils as cents. See
money.
Exponent is the code's number of decimal places: 0 for JPY, 2 for
USD, 3 for KWD. It never changes for a code.
EligibleDefault reports whether a new record may start in the code.
153 codes can. A code no longer issued, such as HRK, still stores,
formats and edits, but is not a default for new records.
Parsing
func ParseNormalized(locale, s, code string) (int64, error)
func Parse(locale, s, code string) (int64, error)
func Clean(s string) string
ParseNormalized is a money field's default reading. It accepts text
with one plausible reading, refuses text that could be two amounts, and
never rounds. Forms has the
rules and a table of examples.
Parse is the strict reading, for Field.Strict. It takes only
locale's own format: one sign (a leading or trailing -, never +
or parentheses), at most one marker for the currency (its symbol in
locale, or its code), and no more decimals than the currency has.
Both read signs. Whether a negative amount is allowed is the caller's decision. Both return 0 and no error for blank text.
Clean removes direction marks and turns every kind of space into a
plain one, then trims. Both readings start with it, and so does a
required check: text of only spaces or direction marks is blank.
Text with two readings
type AmbiguousError struct {
Readings []Reading
}
type Reading struct {
Decimal string
Storable bool
Minor int64
}
func (e *AmbiguousError) Choices(locale, code string) (a, b string)
func (e *AmbiguousError) Error() string
ParseNormalized returns an *AmbiguousError for text whose readings
disagree, such as 1.500 on an English page. Readings holds each one,
smallest first. Decimal is the exact amount; Storable is false when
it has more decimals than the currency, and then Minor is not set.
Choices writes the smallest and largest readings so that no reader
can take one for the other: the ISO code after the amount, no group
marks, and an extra zero where three decimals would look like a group.
1.500 in a KWD field gives 1.5 KWD and 1500 KWD; 1,234 in a EUR
field on a German page gives 1,2340 EUR and 1234 EUR. form puts
them in the money_ambiguous message.
Errors
var (
ErrMalformedCurrency = errors.New("currency code is not three ASCII letters")
ErrUnsupportedCurrency = errors.New("currency code is not in the storage set")
ErrNotInteger = errors.New("amount is not an integer count of minor units")
ErrOutOfRange = errors.New("amount does not fit in int64")
ErrInvalid = errors.New("money_invalid")
ErrPrecision = errors.New("money_precision")
ErrDecimalComma = errors.New("money_decimal_comma")
ErrDecimalPoint = errors.New("money_decimal_point")
ErrCurrencySign = errors.New("money_currency")
)
var ErrWrongCurrency = errors.New("money_wrong_currency")
The first four are for you: a bad code, or an amount that is not an
integer that fits. The rest are a person's input refused. Their text is
the rastrillo.ui.* catalog key's last part, and form turns each into
the message the person sees.
Rescale and Legacy
func Rescale(v int64, scale int, code string) (int64, error)
type Legacy struct {
Currency string
Scale int
}
Rescale converts an integer with scale decimal places into code's
minor units, exactly or not at all. At scale 2, 1200 is 12 JPY, and 1250
is an error: 1250 at scale 2 is 12.50, which JPY cannot hold. Nothing
is rounded, because a rounded price is a price nobody chose.
Legacy is a manifest's [legacy] table, the currency and scale that
amounts stored before currencies existed were written in. The SQL
upgrade and the event replay both read it.
Resolve and AmountFromAny
func Resolve(tag string) locales.Resolution
func AmountFromAny(v any) (minor int64, present bool, err error)
Resolve is locales.Resolve, here
so a caller handling money needs one import. A money field checks the
language its form sent with Resolve(v).ID == v.
AmountFromAny is the money template function's argument check. It
takes any integer type, or a pointer to one, where a nil pointer is no
amount rather than zero. It refuses floats, strings, and unsigned values
too large for an int64.
A language without its pack
Asked for a language whose pack is not linked, Format writes the
amount in the nearest linked parent's conventions, or the app's default
language's, and logs a warning once per pack:
currency: de-AT needs the de pack, which is not linked; amounts are formatted in en instead. Import _ "amadan.net/rastrillo/rastrillo/locales/de"
The currency never changes; only how it is written. The languages your app offers are checked at boot and always linked, so this happens only for a language your code asks for directly. See locales.
currency/replay
amadan.net/rastrillo/rastrillo/currency/replay holds the money rules
a generated mergeable store replays its events with. You call it only
if you append money events by hand.
Every event a currency-aware store writes carries "V": 2, whatever its
kind. An event with no V is from before the upgrade, version 1. The
version is checked before anything else, so an event from a newer
version is refused, not guessed at:
event 4: version 3 is not one this binary understands.
In a version-2 event, each money field has a currency if and only if it
has an amount. Neither means unchanged. Both set the field. An amount
in a different currency from the one the field already has is refused:
a currency change needs its own event. A version-1 amount is rescaled
from the manifest's [legacy].
Rules.Apply folds one event, Replay a whole stream. A store's reads
and its boot check both go through them, so an event that breaks the
rules is refused on the next read, not only at the next boot.
currency/sqlmoney
amadan.net/rastrillo/rastrillo/currency/sqlmoney upgrades a generated
app's SQL money columns at boot and checks them. gen.Boot calls
Upgrade; you do not.
Upgrade decides what to do from the columns it finds, so a second boot
does nothing. An old column price is replaced by price_minor and
price_currency: the two are added, each amount is copied with
Rescale from [legacy], and price is dropped. Dropping it rewrites
the table, so the first boot after the upgrade takes longer on a big
table. A money field new to an existing table gets its two columns,
empty in every existing row. Then every money field is checked with
CheckPairs: no row may have an amount without a currency, or a
currency without an amount. Any failure rolls everything back, and the
app does not start.
Read this page as markdown — exact, unstyled, and cheap for an agent to fetch.