DEV Community

Noel Worden
Noel Worden

Posted on • Edited on

1

A Rabbit Hole of Decimal Formatting

This week I learned a little something about cleaning up decimals for html. The TL;DR of this post is that the Decimal.to_string/2 function has an option :xsd that converts the decimal to canonical representation, which in effect cleans the number of trailing zeros, without rounding.

The situation was that for a new index page we were showing a record's decimal converted to a percentage. Off the cuff, it was simple logic: example_decimal * 100. But, we are pulling in data from excel sheets and at the moment all data is being cast as strings, so we have to use Decimal to convert and perform the multiplication:

Decimal.mult(Decimal.cast(example_decimal), 100)

Still, relatively simple logic, except for the way Decimal displays the result, based on the amount of non-zeros to the left of the decimal point of the input:

iex> Decimal.mult(Decimal.cast("0.8"), 100)
#Decimal<80.0>
iex> Decimal.mult(Decimal.cast("0.87"), 100)
#Decimal<87.00>

For me that inconsistency was the deal breaker, and the slide down the slippery slope of how to show the user a consistent percentage on the index page. Maybe it's just me, but it seems lazy to show one result with a single zero after the decimal, and then, for no apparent reason to the user, show another result with double zeros after the decimal. Initially, the solution seemed obvious, just slap a round on it so that everything would have double zeros after the decimal.

iex> Decimal.round(Decimal.mult(Decimal.cast("0.8"), 100), 2)
#Decimal<80.00>
iex> Decimal.round(Decimal.mult(Decimal.cast("0.87"), 100), 2)
#Decimal<87.00>

Done, mic drop, I'm off to get some coffee. I was confident in this solution because I had seen a lot of this data and hadn't ever seen it not be a whole number. This solution also allowed accuracy out to the hundredth position, which I felt would cover our bases. But, before I could even leave my desk to get my celebratory coffee, a colleague raised the now obvious question "what about a number with a long decimal, do we really want to round that?". Although I had seen the data and was confident about the assumption of whole numbers, she was 100% right to bring up the edge case, and if we wanted to handle this decimal right we had to account for a long decimal.

I'll skip passed the long discussion and get right to the good stuff. Someone suggested Decimal.to_string/2 with :xsd passed as an option, and it's a great solution. In the docs it states that passing :xsd will convert the number to the canonical XSD representation. Welp, I had no idea what that meant, so I dug into it further. The docs link to W3 xml decimal documentation and the takeaway from that is "... Leading and trailing zeros are prohibited...". Basically, a canonical representation will remove any unnecessary leading and trailing zeros, effectively cleaning up a decimal.

iex> Decimal.to_string(Decimal.mult(Decimal.cast("0.8"), 100), :xsd)
"80.0"
iex> Decimal.to_string(Decimal.mult(Decimal.cast("0.87"), 100), :xsd)
"87.0"

And to see some more extreme examples, here's how Decimal.mult/2 handles multiple zeros after the decimal:

iex> Decimal.mult(Decimal.cast("0.8700"), 100)
#Decimal<87.0000>
iex> Decimal.mult(Decimal.cast("0.8700000"), 100)
#Decimal<87.0000000>

And the cleanup with :xsd:

iex> Decimal.to_string(Decimal.mult(Decimal.cast("0.8700000"), 100), :xsd)
"87.0"

And, if the digits to the right aren't zeros:

iex> Decimal.mult(Decimal.cast("0.8765309"), 100)
#Decimal<87.6530900>

Note the two unnecessary zeros at the end. Now that same decimal as a canonical representation:

iex> Decimal.to_string(Decimal.mult(Decimal.cast("0.8765309"), 100), :xsd)
"87.65309"

For the purpose of a consistent decimal conversion, canonical representation is the best of both worlds. The decimal has unnecessary zeros dropped, but also displays the decimal as accurately as possible. It is definitely a bit odd to have to convert a string to a decimal, perform the math, and ultimately change it back into a string, but life isn't perfect and this is the data we are working with.


This post is part of an ongoing This Week I Learned series. I welcome any critique, feedback, or suggestions in the comments.

AWS GenAI LIVE image

How is generative AI increasing efficiency?

Join AWS GenAI LIVE! to find out how gen AI is reshaping productivity, streamlining processes, and driving innovation.

Learn more

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay