Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions lib/json/canonicalization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,29 @@ def to_json_c14n
if num < 0
num, sign = -num, '-'
end
native_rep = "%.15E" % num
decimal, exponential = native_rep.split('E')
exp_val = exponential.to_i
# Decompose into the shortest decimal digit sequence that round-trips,
# per RFC 8785 §3.2.2.3 (ECMAScript Number-to-String). Float#to_s already
# yields that shortest sequence; the previous "%.15E" gave only 16
# significant digits and could not round-trip doubles that need 17
# (e.g. 0.1 + 0.2). `decimal` is normalised to a single leading digit and
# `exp_val` is the power of ten of that digit, matching the shape the
# formatting below expects.
rep = num.to_s
if rep =~ /e/i
mantissa, power = rep.split(/e/i)
int_part, frac_part = mantissa.split('.')
digits = int_part + frac_part.to_s
exp_val = power.to_i
else
int_part, frac_part = rep.split('.')
all_digits = int_part + frac_part.to_s
first = (all_digits =~ /[1-9]/) || all_digits.length - 1
exp_val = int_part.length - 1 - first
digits = all_digits[first..]
end
digits = digits.sub(/0+\z/, '')
digits = '0' if digits.empty?
decimal = digits[0] + '.' + (digits.length > 1 ? digits[1..] : '0')
exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s

integral, fractional = decimal.split('.')
Expand Down
21 changes: 14 additions & 7 deletions spec/number_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,20 @@
999999999999999700000 => '999999999999999700000',
999999999999999900000 => '999999999999999900000',
333333333.33333329 => '333333333.3333333',
# -5e-324 => '-5e-324', # Outside Ruby Range
# 1.0000000000000001e+23 => '1.0000000000000001e+23', # Outside Ruby Range
# 295147905179352830000 => '295147905179352830000', # Outside Ruby Range
#-1.7976931348623157e+308 => '-1.7976931348623157e+308', # Outside Ruby Range
#1.7976931348623157e+308 => '1.7976931348623157e+308', # Outside Ruby Range
#1e+23 => '1e+23', # Outside Ruby
#5e-324 => '5e-324', # Outside Ruby Range
# These are representable doubles that need 17 significant digits; they
# were previously mis-serialised (only 16 digits were emitted) and so had
# been left commented out. They now round-trip correctly.
-5e-324 => '-5e-324',
1.0000000000000001e+23 => '1.0000000000000001e+23',
295147905179352830000 => '295147905179352830000',
-1.7976931348623157e+308 => '-1.7976931348623157e+308',
1.7976931348623157e+308 => '1.7976931348623157e+308',
1e+23 => '1e+23',
5e-324 => '5e-324',
# Round-trip regressions: the canonical form must parse back to the same
# double. "%.15E" produced '0.3' for 0.1 + 0.2, a different value.
0.1 + 0.2 => '0.30000000000000004',
123456789012345680.0 => '123456789012345680',
}.each do |data, expected|
if expected.is_a?(String)
it "converts #{data} to #{expected}" do
Expand Down