mcbain83 mcbain83 - 1 month ago 42
Ruby Question

Ruby output numbers to 2 decimal places

I'm having trouble serializing my ruby object to json, more specifically the format of the numbers.
I have written an rspec test to illustrate my issue more precisely.

expected = '{ "foo": 1.00, "bar": 4.50, "abc": 0.00, "xyz": 1.23 }'

it 'serializes as expected' do
my_hash = { "foo": 1, "bar": 4.5, "abc": 0, "xyz": 1.23}
expect(my_to_json_method(my_hash)).to eq expected
end


This is the case that I am having trouble with. I can use the sprintf but how do I get the string output as shown in the above example?

Answer

First of all, you should not use floats to represent monetary values. So instead, let's use a more appropriate type: (there's also the Ruby Money gem)

require 'bigdecimal'

my_hash = {
  foo: BigDecimal.new('1.00'),
  bar: BigDecimal.new('4.50'),
  abc: BigDecimal.new('0.00'),
  xyz: BigDecimal.new('1.23')
}

There are several options to represent monetary values. All of the following JSON strings are valid according to the JSON specification and all require special treatment upon parsing. It's up to you to choose the most appropriate.

Note: I'm implementing a custom to_json method to convert the BigDecimal instances to JSON using Ruby's default JSON library. This is just for demonstration purposes, you should generally not patch core (or stdlib) classes.


1. Numbers with fixed precision

This is what you asked for. Note that many JSON libraries will parse these numbers as floating point values by default.

class BigDecimal
  def to_json(*)
    '%.2f' % self
  end
end
puts my_hash.to_json

Output:

{"foo":1.00,"bar":4.50,"abc":0.00,"xyz":1.23}

2. Numbers as strings

This will work across all JSON libraries, but storing numbers as strings doesn't look quite right to me.

class BigDecimal
  def to_json(*)
    '"%.2f"' % self
  end
end
puts my_hash.to_json

Output:

{"foo":"1.00","bar":"4.50","abc":"0.00","xyz":"1.23"}

3. Numbers as integers

Instead of representing monetary values as fractional numbers, you simply output the cents as whole numbers. This is what I usually do.

class BigDecimal
  def to_json(*)
    (self * 100).to_i.to_s
  end
end
puts my_hash.to_json

Output:

{"foo":100,"bar":450,"abc":0,"xyz":123}