mastodon/lib/json-canonicalization/floats_fix.rb

97 lines
5.6 KiB
Ruby
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# frozen_string_literal: true
# Originally written by ViaCelestia
# https://github.com/dryruby/json-canonicalization/pull/6
class Numeric
# This is intended to be compliant with ECMA-262, version 6.0 (ES6)
#
# See https://262.ecma-international.org/6.0/#sec-tostring-applied-to-the-number-type
#
# JSON does not permit NaN or infinite values, so those raise an error
def to_json_c14n
raise RangeError if self.is_a?(Float) && !self.finite?
return '0' if self.zero?
# We may or may not be using scientific notation (see https://en.wikipedia.org/wiki/Scientific_notation)
# at this point, but the terminology is the same. Numbers are represented as a significand
# (also known as a mantissa) multiplied by 10 raised to an exponent. A number like 1701 may be represented
# as 1701 * 10^0, 170.1 * 10^1, 17.01 * 10^2, or (in scientific notation) 1.701 * 10^3. ES6 and Ruby don't
# always agree on when to use scientific notation, but if Ruby has done the conversion, we can use the
# exponent below when reproducing the behavior in the ES6 spec.
significand_digits, exponent_digits = self.abs.to_s.split('e', 2)
integer_digits, fraction_digits = significand_digits.split('.', 2)
# From the ES6 spec:
#
# "The abstract operation ToString converts a Number m to String format as follows ...
# let n, k, and s be integers such that k ≥ 1, 10k1 ≤ s < 10k, the Number value for s × 10nk is m,
# and k is as small as possible. Note that k is the number of digits in the decimal representation of s,
# that s is not divisible by 10, and that the least significant digit of s is not necessarily uniquely
# determined by these criteria."
#
# This is just a different sort of exponential notation, but instead of preferring 0 < s < 10 as in
# scientific notation, here we want s to be an integer, and not divisible by 10. Since we're relying
# on ruby's existing #to_s, s is just the significand without the decimal or any leading or trailing
# zeroes
s = significand_digits.sub('.', '').sub(/^-?0*/, '').sub(/0*$/, '')
# Once we know s, k is easy
k = s.length
# If n is positive, it represents the number of digits (including trailing zeroes) to the left of the
# decimal. If n is negative or zero, it represents the number of zeroes to the right of the decimal.
# n-1 is also equal to the exponent used in scientific notation represenations of m, so if we already
# have that representation, we can use that rather than try to recalculate where the decimal would be
# If we don't already have an exponent, we just do digit counting rather than using Math.log10(self) or
# the slightly more precise Math.log2(self)/Math.log2(10) since that can lose precision for values very
# close to n = 22, like the Integer value 999999999999999700000
n = if exponent_digits
exponent_digits.to_i + 1
elsif integer_digits.to_i > 0
integer_digits.length
else
-fraction_digits.index(/[1-9]/)
end
exponent = n - 1
# Per the spec, positive numbers do not include a sign, but exponents always do
sign = self.negative? ? '-' : ''
exponent_sign = exponent.negative? ? '-' : '+'
if k <= n && n <= 21 # Whole numbers, possibly with trailing zeroes, and < 10^21
# return the String consisting of the code units of the k digits of the decimal representation of s
# (in order, with no leading zeroes), followed by nk occurrences of the code unit 0x0030 (DIGIT ZERO).
[sign, s, '0' * (n - k)].join
elsif 0 < n && n <= 21 # Numbers with an integer component < 10^21
# return the String consisting of the code units of the most significant n digits of the decimal
# representation of s, followed by the code unit 0x002E (FULL STOP), followed by the code units of the
# remaining kn digits of the decimal representation of s.
[sign, s[0..(n-1)], '.', s[n..-1]].join
elsif -6 < n && n <= 0 # Fractional numbers to no more than 6 decimal places
# return the String consisting of the code unit 0x0030 (DIGIT ZERO), followed by the code unit 0x002E
# (FULL STOP), followed by n occurrences of the code unit 0x0030 (DIGIT ZERO), followed by the code
# units of the k digits of the decimal representation of s
[sign, '0.', '0' * (-n), s].join
elsif k == 1 # single significant digit outside of -6 < n <= 21
# return the String consisting of the code unit of the single digit of s, followed by code unit 0x0065
# (LATIN SMALL LETTER E), followed by the code unit 0x002B (PLUS SIGN) or the code unit 0x002D
# (HYPHEN-MINUS) according to whether n1 is positive or negative, followed by the code units of the decimal
# representation of the integer abs(n1) (with no leading zeroes).
#
# This produces "1e-18", rather than Ruby's default "1.0e-18"
[sign, s, 'e', exponent_sign, exponent.abs].join
else # multiple significant digits outside of -6 < n <= 21
# Return the String consisting of the code units of the most significant digit of the decimal representation
# of s, followed by code unit 0x002E (FULL STOP), followed by the code units of the remaining k1 digits of
# the decimal representation of s, followed by code unit 0x0065 (LATIN SMALL LETTER E), followed by code unit
# 0x002B (PLUS SIGN) or the code unit 0x002D (HYPHEN-MINUS) according to whether n1 is positive or negative,
# followed by the code units of the decimal representation of the integer abs(n1) (with no leading zeroes).
[sign, s[0], '.', s[1..-1], 'e', exponent_sign, exponent.abs].join
end
end
end