sergio

sergio

How to get accounts where balance > 0 using ex_money?

Trying to find all accounts with a non-zero balance using ex_money in Postgres.

zero_money = Money.new(:USD, 0)

# My schema field: field :current_balance, Money.Ecto.Composite.Type
Repo.all(
  from(b in BankAccount,
    where: b.user_id == ^user.id,
    where: b.current_balance > ^zero_money
  )
)

Error I’m getting:

** (DBConnection.EncodeError) cannot encode anonymous tuple {“USD”, Decimal.new(“0”)}. Please define a custom Postgrex extension that matches on its underlying type:
use Postgrex.BinaryExtension, type: “typeinthedb”
(postgrex 0.17.5) lib/postgrex/type_module.ex:150: Postgrex.DefaultTypes.encode_tuple/3
(postgrex 0.17.5) lib/postgrex/type_module.ex:947: Postgrex.DefaultTypes.encode_params/3
(postgrex 0.17.5) lib/postgrex/query.ex:75: DBConnection.Query.Postgrex.Query.encode/3

I’ve reviewed the docs for both ex_money and ex_money_sql but can’t find any mentioned of where queries.

Marked As Solved

sergio

sergio

Yes I did all that, confirmed.

I found a random Github issue with the solution:

zero_money = Money.new(:USD, 0)
Repo.all(
  from(b in BankAccount,
    where: b.user_id == ^user.id,
    where: b.current_balance > type(^zero_money, b.current_balance)
  )
)

SELECT b0.“id”, b0.“name”, b0.“current_apr”, b0.“promotional_apr_expiration_date”, b0.“real_apr”, b0.“payment_due_date”, b0.“statement_closing_date”, b0.“current_balance”, b0.“minimum_payment”, b0.“user_id”, b0.“inserted_at”, b0.“updated_at” FROM “bank_accounts” AS b0 WHERE (b0.“user_id” = $1) AND (b0.“current_balance” > $2::money_with_currency) [“d37defd7-1576-4198-b2ac-815b67b2fee2”, Money.new(:USD, “3000”)]

So the type keyword is from Ecto, and it’s casting the zero_money variable to the same type as the b.current_balance column?

Is that how this is working?

Also Liked

kip

kip

ex_cldr Core Team

@thiagomajesk, its a good conversation and always appropriate to talk about storage of and calculations on money. Here are some of my thoughts on microdollars, floating point and serialising money in general.

Floating point is a bad idea for representing money

As you noted, there is a general recommendation not to use floats/doubles. Floating point is, by definition, making tradeoffs between size, performance and precision and accuracy that are different to the requirements for money. At the simplest level, floating point representations have issues with precision and associativity. For example:

# Accuracy cannot be guaranteed
iex> 0.1 + 0.2
0.30000000000000004

# Not associative
iex> a = 1.0000001
iex> b = 2.0000002
iex> c = 3.0000003
iex> (a + b) + c
6.0000006
iex> a + (b + c)
6.000000600000001

Integers aren’t perfect either

Integers are a better fit, they can at least represent a set of real numbers up to a certain precision (limited by the machine word size typically). It works appropriately (for money) for addition, subtraction and multiplication - but we still have issues with division. A good example is what happens when we divide 10 by 3:

# The infinite series of 3.3333...... is rounded
iex> 10 / 3
3.3333333333333335
# Integer division won't round trip
iex> div(10, 3) * 3
9

The issue of integer division isn’t solved by microdollars. This is one of the reasons why the Money.split/2 exits. It does division but also returns any remainder so that (money / number) * number + remainder == money is a guarantee.

Decimals

Decimal implementations, in computers, make different tradeoffs to precision, scale and speed. Whereas floating point emphasises speed, decimal tends to prioritise precision and scale. In many implementations, including the Decimal, the precision can be arbitrarily large but defaults to 28 digits.

iex> Decimal.Context.get()
%Decimal.Context{
  precision: 28,
  rounding: :half_up,
  flags: [],
  traps: [:invalid_operation, :division_by_zero]
}

Note too the rounding: :half_up. There are several rounding strategies in Decimal and its worth being familiar with them if you’re working with money. ex_money uses :half_even as its default rounding since that is most common in financial applications and is even known as banker’s rounding.

So why can Decimal preserve prevision better than floating point? In part because many decimal libraries, including Decimal represent the decimal number as an integer with an associated scale factor (to indicate where the decimal place is located). Decimal also stores an exponent which can optimise the size of the integer part without losing precision. Postgres which has a very similar type called numeric stores only the integer and scale.

Serialisation

Not all interchange formats or storage formats support decimal data. Thankfully Postgres and its many descendant databases do with the numeric data type. In addition ex_money_sql provides Ecto and Postgres data types that combines the currency code with the money amount into a single data type to maximise data integrity - the design goal is to never be in a position where the currency type is unknown or ambiguous.

For interchange formats that have no decimal data type, like JSON, the amount is cast to a string since thats the only JSON data type that can preserve precision and scale. In fact money is cast to a JSON object of the form {\"currency\":\"USD\",\"amount\":\"100\"}.

On Precision and Scale

The microdollar format has the advantage of performance while maintaining a fixed scale or 6 digits. However financial calculations are expected to retain 7 or 8 digits of scale during calculations - think stock market, home loan interest. And as @LostKobrakai points out, Digital Tokens (crypto) may require more than 6 digits of scale. Bitcoin calculations may need up to 10 digits of scale.

Finally

Money and Float make different tradeoffs amongst performance, precision, scale and accuracy. Float emphasises performance. ex_money prioritises precision, scale and accuracy. microdollar aims for a middle ground. Each approach has its benefits and compromises.

When it comes to money, I believe precision, scale and accuracy are paramount and thats why ex_money uses Decimal for Elixir representation, a composite data type called money_with_currency for Postgres and a string-based object format for JSON (including embedded Ecto schemas.

kip

kip

ex_cldr Core Team

ex_money is built upon ex_cldr so localisation is an intrinsic part of the library:

iex> m = Money.new(:USD, 100)
Money.new(:USD, "100")

# Locale-specific formatting
iex> Money.to_string(m, locale: :de)
{:ok, "100,00 $"}

iex> Money.to_string(m, locale: :de, format: :long)
{:ok, "100 US-Dollar"}

iex> Money.to_string(m, locale: :es, format: :long)
{:ok, "100 dólares estadounidenses"}

# Locale specific parsing (which take care to understand
# the locales decimal and grouping separators
iex> Money.parse("100,00 $", locale: :de)
Money.new(:USD, "100.00")

# Return display names that can be used in UI for
# example, as column headings
iex> Cldr.Currency.display_name :USD, locale: :de
{:ok, "US-Dollar"}

iex> Cldr.Currency.display_name :USD, locale: :ar
{:ok, "دولار أمريكي"}

And although not strictly localisation, ex_money knows all the ISO4217 currency formats so rounding is done to the correct number of digits when required and formatting displays the correct number of decimals. For example:

# JPY has no decimal places (in common use at least)
iex> Money.to_string Money.new(:JPY, 54321)
{:ok, "¥54,321"}

# Tunisian Dinar has three decimal places
iex> Money.to_string Money.new(:TND, "1000.12345")
{:ok, "TND 1,000.123"}

# Swiss Franc cash smallest unit is 5 centimes.
# Rounding is `:half_even`
iex> Money.round Money.new(:CHF, "123.73"), currency_digits: :cash
Money.new(:CHF, "123.75")
kip

kip

ex_cldr Core Team

I’ve sent a proposal to the Ecto mailing list about deriving the database type from an Ecto.ParameterizedType (like Money.Ecto.Composite.Type) and casting to it since Ecto already knows the type involved.

Unless that proposal is accepted (and I consider it a low change of success) it will be necessary to either use:

# as you are already doing
where: b.current_balance > type(^Money.zero(:USD), b.current_balance)

# or
where: b.current_balance > fragment("?::money_with_currency", ^Money.zero(:USD))

Note that Money.zero/1 is an explicit function for returning a 0 amount money in some currency.

I will go ahead and write some custom operators for :money_with_currency since there is still the risk of comparing money of different currencies which is not semantically correct.

kip

kip

ex_cldr Core Team

I’m very late to this thread, apologies. I’m glad you got a resolution - and I agree that having to use the type/2 macro isn’t very ergonomic. I’ll see if following the error messages advice and building a binary extension can make this better.

kip

kip

ex_cldr Core Team

And I should also define custom comparison operators because otherwise Postgres will happily compare two money_with_currency types, even if they are of a different currency. This because Postgres, like the BEAM, has built-in term ordering.

Where Next?

Popular in Questions Top

senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
logicmason
Hi there, I'm working through my first release with elixir/phoenix. I've built a release with distillery and found that it crashes when I...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
wernerlaude
In AR this is so simple @articles = current_user.articles How to do in Ecto? def index(conn, _params) do current_user = conn.assig...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
johnnyicon
Hi all, I've just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I'm trying to use Postg...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New

We're in Beta

About us Mission Statement