Phrogz
Phrogz

Reputation: 303371

Force JSON serialization of numbers to specific precision

Consider the following code that is intended to round numbers to the nearest one-hundredth and serialize the result to JSON:

require 'json'
def round_to_nearest( value, precision )
  (value/precision).round * precision
end
a = [1.391332, 0.689993, 4.84678]
puts a.map{ |n| round_to_nearest(n,0.01) }.to_json
#=> [1.3900000000000001,0.6900000000000001,4.8500000000000005]

Is there a way to use JSON to serialize all numbers with a specific level of precision?

a.map{ ... }.to_json( numeric_decimals:2 )
#=> [1.39,0.69,4.85]

This could be either with the Ruby built-in JSON library or another JSON gem.

Edit: As noted in comments to answers below, I'm looking for a general solution to all JSON serialization for arbitrary data that includes numbers, not specifically a flat array of numbers.


Note that the above problem can be fixed in this specific case by rewriting the method:

def round_to_nearest( value, precision )
  factor = 1/precision
  (value*factor).round.to_f / factor
end

...but this does not solve the general desire to force a precision level during serialization.

Upvotes: 5

Views: 4736

Answers (4)

Phrogz
Phrogz

Reputation: 303371

Since the built-in JSON library does not call #as_json or #to_json on Numerics (presumably for speed) we can use the ActiveSupport library from Rails (without needing Rails).

We do our monkey-patch delicately, so that it only takes effect when a user-specified option is passed when calling to_json:

require 'active_support/json' # gem install activesupport

class Float
  def as_json(options={})
    if options[:decimals]
      value = round(options[:decimals])
      (i=value.to_i) == value ? i : value
    else
      super
    end
  end
end

data = { key: [ [ 2.991134, 2.998531 ], ['s', 34.127876] ] }
puts data.to_json             #=> {"key":[[2.991134,2.998531],["s",34.127876]]}
puts data.to_json(decimals:2) #=> {"key":[[2.99,3],["s",34.13]]}

As shown in the last example, there's a little extra code used to convert integer-valued-floats to pure integers, just so that serialization doesn't waste bytes outputting 3.00 and can instead put just 3 (the same value in JSON and JS).

Upvotes: 3

AJcodez
AJcodez

Reputation: 34206

I'd just pre-round it using ruby's built-in round method: http://www.ruby-doc.org/core-1.9.3/Float.html#method-i-round

a.map{ |n| n.round(2) }.to_json

That looks clean to me instead of getting all types of custom libraries and passing in arguments.

Edit for comment:

I know you can do that with activesupport.

# If not using rails
require 'active_support/json'

class Float
  def as_json(options={})
    self.round(2)
  end
end

{ 'key' => [ [ 3.2342 ], ['hello', 34.1232983] ] }.to_json
# => {"key":[[3.23],["hello",34.12]]}

More exact solution: better monkey-patch

Upvotes: 3

the Tin Man
the Tin Man

Reputation: 160571

Because you are dealing with floating-point, I'm inclined to say to convert to strings using format strings, and pass those. They'd allow you to set the precision nicely:

a = [1.391332, 0.689993, 4.84678]
a.map{ |f| '%.2f' % f }
=> ["1.39", "0.69", "4.85"]

If you want true floats, convert them back:

a.map{ |f| ('%.2f' % f).to_f }
=> [1.39, 0.69, 4.85]

From that point you could write something to override JSON's default Array serializer, or the Float serializer, however JSON does it, with one that accepts the precision, or format-string, you want.


Just to clarify, the use of to_f would occur before serializing, which would not bloat the resulting JSON output or force any sort of "magic" on the receiving side. Floats would transfer and be deserialized as floats:

irb(main):001:0> require 'json'
true
irb(main):002:0> a = [1.391332, 0.689993, 4.84678]
[
    [0] 1.391332,
    [1] 0.689993,
    [2] 4.84678
]
irb(main):003:0> a.map{ |f| ('%.2f' % f).to_f }.to_json
"[1.39,0.69,4.85]"
irb(main):004:0> JSON.parse(a.map{ |f| ('%.2f' % f).to_f }.to_json)
[
    [0] 1.39,
    [1] 0.69,
    [2] 4.85
]

Start with something like:

class Float
  def to_json
    ('%.2f'%self).to_f
  end
end
(355.0/113).to_json # => 3.14

and figure out how to apply it to an array of Floats. You'll probably need to use the pure Ruby JSON though.

Upvotes: 0

Jonas Fagundes
Jonas Fagundes

Reputation: 1519

  1. Precision should not be handled during serialization;
  2. Probably you have some semantic on these numbers that you are not making it explicit (maybe is Currency value? and maybe this is a quick and dirty script that doesn't need to make this semantic explicit) and
  3. Using float pointing numbers as your implementation when what your program really needs precision is a bad approach for the problem. Floating point numbers has inherent inaccuracy in their representation (Floating Point Accuracy Problems ). Use BigDecimal instead.

Upvotes: -2

Related Questions