-
-
Notifications
You must be signed in to change notification settings - Fork 310
[Ruby] On using BigDecimal
We’ve recently gone deep down the rabbit hole of “Big Decimal”. Although we accumulated decades of experience in Ruby, this was more challenging than we initially thought, because of the few things we discovered along the way (that led to quite a bit of rework!).
Context: We build Lago : open-source metering and usage-based billing
We handle objects with up to 5 decimals, without transforming them. Why? We need to keep each object as precise as possible, and delay the rounding to 2 decimal places to the last step: when our users choose to display a charge or an invoice in a specific currency, for instance. Why didn’t we just store values in decimal? We need to support multiple decimals for complex pricing : e.g. $0,0015 per hour. We want to give the maximum flexibility to our users to fit their pricing needs.
BigInt: the ‘by default’ option with Ruby doesn’t support decimals, so we ruled it out. Float objects: we hit accuracy limits very quickly as float in Ruby (like in many other languages) can lead to rounding issues, it’s designed for performance and not for precise calculations. BigDecimal: we knew it ranked lower on the performance side, but it seemed to be the only relevant option. Moreover, PostgreSQL provides a Decimal type that is very well integrated with Rails Active Record, which, combined with BigDecimal provides an ‘out-of-the-box’ validation for scale and precision.
Example of a migration:
With BigDecimal, we can specify precision and scale in the decimal column (PostgreSQL).
t.decimal :rate_amount, null: false, default: 0, precision: 5
Precision: total number of digits (including before and after the decimal point). Scale: number of digits after the decimal point.
Rounding Floats can lead to errors.
Execution time for a Float calculation
Execution Time for a BigDecimal calculation
Handling numbers can be tricky on the front-end side. We either have different types (string, float, integers), format (cents, decimal) or purpose (money, percent). For all of them we also need to change the UI display, depending on currency and user’s locale. How we solved them Back-End and Front-End agreed on some rules when dealing with numbers. If a value sent or received contains a decimal (no matter the precision), we’ll manage it as type ‘String’. On the other hand, all other values that won’t contain any decimal will be of type ‘Number’.
The Front-End component libraries can accept both types as inputs, so we don’t have to think about type management when building our interfaces. Internally, for manipulation and formatting, those components will systematically transform values into type ‘String’.
For display purposes, we use Javascript APIs that accept options to define the number of decimals or the display format (currency, percent).
We shared these notes and code snippets in the simplest form possible, because it’s what we were looking for when we researched the issue. From the questions we spotted in the Ruby community, it looks like other people were confused as well, so we hope this post proves useful to you as well.