OpenFeed.Amount (OpenFeed v0.1.0)

Copy Markdown View Source

Converting OpenFeed's monetary values to Decimal.

Why this exists

OpenFeed is not consistent about how it encodes money on the wire, and the inconsistency runs along domain lines:

FieldJSON type
BankingTransaction.amountstring"-52.00" (ISO 20022)
BankingBalance.currentBalancestring
EnergyInvoice.invoiceAmountnumber193.8
EnergyBillingTransaction.amountnumber

So Decimal.new/1 works on a banking amount and raises on an energy one, while Decimal.from_float/1 is the reverse. Getting this wrong produces a crash on whichever domain you tested second.

to_decimal/1 takes either, plus nil for absent optional fields.

Never use floats for money

Energy amounts arrive as JSON numbers, which Jason decodes to floats. Convert once at the boundary with this function and keep Decimal from then on — don't add floats together and convert at the end.

Examples

iex> OpenFeed.Amount.to_decimal("-52.00")
Decimal.new("-52.00")

iex> OpenFeed.Amount.to_decimal(193.8)
Decimal.new("193.8")

iex> OpenFeed.Amount.to_decimal(200)
Decimal.new(200)

iex> OpenFeed.Amount.to_decimal(nil)
nil

Summary

Functions

Convert an OpenFeed monetary value to a Decimal.

Like to_decimal/1 but raises ArgumentError on an unparseable value.

Functions

to_decimal(decimal)

@spec to_decimal(String.t() | number() | Decimal.t() | nil) :: Decimal.t() | nil

Convert an OpenFeed monetary value to a Decimal.

Accepts an ISO 20022 amount string, a JSON number (integer or float), an existing Decimal, or nil. Returns nil for nil and for anything unparseable, so a malformed optional field does not take down a sync.

Examples

iex> OpenFeed.Amount.to_decimal("1234.56")
Decimal.new("1234.56")

iex> OpenFeed.Amount.to_decimal("not money")
nil

Floats convert via their shortest representation, not their binary expansion:

iex> OpenFeed.Amount.to_decimal(0.1)
Decimal.new("0.1")

to_decimal!(value)

@spec to_decimal!(String.t() | number() | Decimal.t()) :: Decimal.t()

Like to_decimal/1 but raises ArgumentError on an unparseable value.

Use this for fields the spec marks required, where silently getting nil would hide a real problem.

Examples

iex> OpenFeed.Amount.to_decimal!("-52.00")
Decimal.new("-52.00")

iex> OpenFeed.Amount.to_decimal!("nope")
** (ArgumentError) could not read "nope" as an OpenFeed monetary amount