CARLOS rastrillo docs

🤖 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.