Rounding with Decimal library

I need a function that takes a Decimal (it’s money, so two decimal places) and rounds up to the nearest quarter. I’m trying to avoid converting to floats and staying within the Decimal world.

This function works, but it is hideous. The decimal library only natively supports rounding to the whole or half, and I can’t slip values like :lt and :gt into Enum.member? because they are not enumerable, so I used separate conditions for :lt and :gt.

  def round_up_to_nearest_quarter(preroundedamount) do
    alias Decimal, as: D

    base = D.round(preroundedamount, 0, :floor)
    frac = D.sub(preroundedamount, base)

    newfrac =
      cond do
        D.cmp(frac, 0) == :eq ->
          D.new("0.0")

        D.cmp(frac, D.new("0.25")) == :lt ->
          D.new("0.25")

        D.cmp(frac, D.new("0.25")) == :eq ->
          D.new("0.25")

        D.cmp(frac, D.new("0.5")) == :lt ->
          D.new("0.5")

        D.cmp(frac, D.new("0.5")) == :eq ->
          D.new("0.5")

        D.cmp(frac, D.new("0.75")) == :lt ->
          D.new("0.75")

        D.cmp(frac, D.new("0.75")) == :eq ->
          D.new("0.75")

        D.cmp(frac, D.new("1.0")) == :lt ->
          D.new("1.0")

        D.cmp(frac, D.new("1.0")) == :eq ->
          D.new("1.0")

        true ->
          D.new("0.0")
      end

    D.add(base, newfrac)
  end

What is the most Elixir-ian** way to do this?

**Also, what is the adjective form of “Elixir”? Looking for something like “Pythonic”.

1 Like

You may consider using money then.

2 Likes

I still don’t see a configurable rounding function in Money

Hello. It was a nice brain teaser. It took me a while to get it right but I think it works.

defmodule Test do
  @roundings Enum.map(~w(00.0 0.25 0.50 0.75 1.00), &Decimal.new/1)

  def round_up_to_nearest_quarter(preroundedamount) do
    base = Decimal.round(preroundedamount, 0, :floor)
    frac = Decimal.sub(preroundedamount, base)

    newfrac =
      Enum.reduce_while(@roundings, frac, fn rounding, frac ->
        if Decimal.cmp(frac, rounding) in [:lt, :eq] do
          {:halt, rounding}
        else
          {:cont, frac}
        end
      end)

    Decimal.add(base, newfrac)
  end
end

Here is a offhand test

iex(45)> money = Enum.map(~w[1.25 2.58 3.82 7.86 56.54], &Decimal.new/1)
[#Decimal<1.25>, #Decimal<2.58>, #Decimal<3.82>, #Decimal<7.86>,
 #Decimal<56.54>]
iex(46)> Enum.map(money, &Test.round_up_to_nearest_quarter/1)
[#Decimal<1.25>, #Decimal<2.75>, #Decimal<4.00>, #Decimal<8.00>,
 #Decimal<56.75>]

Note the @roundings module attribute in the top. This way it calculates the roundings once at compile time, not every time you need to compare.

Offcourse the variable names could be improved but I am not very familiar with the financial terminology.

Please test it and tell me what you think.

1 Like

I discovered an even easier way to do it: multiply by 4, round up, and div back by 4.

rounded = n |> Decimal.mult(4) |> Decimal.round(0, :up) |> Decimal.div(4)
2 Likes