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.

Where Next?

Popular in Questions Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> somethi...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
tduccuong
Hi, is there any work on GUI with Elixir, that is similar to Electron/Javascript? My idea is to bundle Phoenix and BEAM into a single se...
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
Lily
In templates/appointment/index.html.eex: <%= for appointment <- @appointments do %> <tr> <td><%= appoi...
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
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
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 Postgres...
New
AngeloChecked
What learn first? Rust or Elixir Hi Elixir community! I’m here because i want learn a new language. I’m a junior developer and mainly i ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
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
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement