diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index 46a2f56e..a2111466 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -236,13 +236,20 @@ object Format { output.toString() } - private def formatInteger(formatted: FormatSpec, s: Double): String = { + // Truncate a double toward zero, returning the integer part as a BigInt. + // Uses Long as a fast path (mirrors RenderUtils.renderDouble); falls back to + // BigDecimal for values that exceed Long range (~9.2e18). + private def truncateToInteger(s: Double): BigInt = { val sl = s.toLong - val (lhs, rhs) = if (sl < 0) { - ("-", sl.toString.substring(1)) - } else { - ("", sl.toString) - } + if (sl.toDouble == s) BigInt(sl) + else BigDecimal(s).toBigInt + } + + private def formatInteger(formatted: FormatSpec, s: Double): String = { + val i = truncateToInteger(s) + val negative = i.signum < 0 + val lhs = if (negative) "-" else "" + val rhs = i.abs.toString(10) val rhs2 = precisionPad(lhs, rhs, formatted.precision) widen( formatted, @@ -250,7 +257,7 @@ object Format { "", rhs2, numeric = true, - signedConversion = sl >= 0 + signedConversion = !negative ) } @@ -275,11 +282,10 @@ object Format { } private def formatOctal(formatted: FormatSpec, s: Double): String = { - val (lhs, rhs) = if (s < 0) { - ("-", s.toLong.abs.toOctalString) - } else { - ("", s.toLong.toOctalString) - } + val i = truncateToInteger(s) + val negative = i.signum < 0 + val lhs = if (negative) "-" else "" + val rhs = i.abs.toString(8) val rhs2 = precisionPad(lhs, rhs, formatted.precision) widen( formatted, @@ -287,16 +293,15 @@ object Format { if (!formatted.alternate || rhs2(0) == '0') "" else "0", rhs2, numeric = true, - signedConversion = s > 0 + signedConversion = !negative ) } private def formatHexadecimal(formatted: FormatSpec, s: Double): String = { - val (lhs, rhs) = if (s < 0) { - ("-", s.toLong.abs.toHexString) - } else { - ("", s.toLong.toHexString) - } + val i = truncateToInteger(s) + val negative = i.signum < 0 + val lhs = if (negative) "-" else "" + val rhs = i.abs.toString(16) val rhs2 = precisionPad(lhs, rhs, formatted.precision) widen( formatted, @@ -304,7 +309,7 @@ object Format { if (!formatted.alternate) "" else "0x", rhs2, numeric = true, - signedConversion = s > 0 + signedConversion = !negative ) } diff --git a/sjsonnet/test/src/sjsonnet/EvaluatorTests.scala b/sjsonnet/test/src/sjsonnet/EvaluatorTests.scala index 4b7f75c6..76c02da1 100644 --- a/sjsonnet/test/src/sjsonnet/EvaluatorTests.scala +++ b/sjsonnet/test/src/sjsonnet/EvaluatorTests.scala @@ -572,6 +572,24 @@ object EvaluatorTests extends TestSuite { "-123.45600" ) } + test("formatIntegerOverflow") { + // %d, %o, %x should not overflow to Long.MAX_VALUE for large doubles + eval("'%d' % 1e19", useNewEvaluator = useNewEvaluator) ==> ujson.Str("10000000000000000000") + eval("'%d' % 1e20", useNewEvaluator = useNewEvaluator) ==> ujson.Str( + "100000000000000000000" + ) + eval("'%d' % -1e19", useNewEvaluator = useNewEvaluator) ==> ujson.Str( + "-10000000000000000000" + ) + eval("'%o' % 1e19", useNewEvaluator = useNewEvaluator) ==> ujson.Str( + "1053071060221172000000" + ) + eval("'%x' % 1e19", useNewEvaluator = useNewEvaluator) ==> ujson.Str("8ac7230489e80000") + // Sign should be determined from the truncated integer, not the original double: + // -0.3 truncates to 0, so no minus sign should appear. + eval("'%5.3d' % -0.3", useNewEvaluator = useNewEvaluator) ==> ujson.Str(" 000") + eval("'%d' % -0.9", useNewEvaluator = useNewEvaluator) ==> ujson.Str("0") + } test("strict") { eval("({ a: 1 } { b: 2 }).a", strict = false, useNewEvaluator = useNewEvaluator) ==> ujson .Num(1)