diff --git a/lib/json/canonicalization.rb b/lib/json/canonicalization.rb index e8ec97f..2e8acc4 100644 --- a/lib/json/canonicalization.rb +++ b/lib/json/canonicalization.rb @@ -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('.') diff --git a/spec/number_spec.rb b/spec/number_spec.rb index 29f3d8e..aba082e 100644 --- a/spec/number_spec.rb +++ b/spec/number_spec.rb @@ -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