11package sjsonnet
22
33/**
4- * Minimal re-implementation of Python's `%` formatting logic, since Jsonnet's `%` formatter is
5- * basically "do whatever python does", with a link to :
4+ * Minimal re-implementation of Python's `%` formatting logic. Jsonnet documents `std.format` and
5+ * the `%` operator as following Python formatting rules :
66 *
7- * - https://docs.python.org/2/library/stdtypes.html#string-formatting
7+ * - https://jsonnet.org/ref/stdlib.html#std-format
8+ * - https://docs.python.org/3/library/stdtypes.html#printf-style-string-formatting
89 *
910 * Parses the formatted strings into a sequence of literal strings separated by `%` interpolations
1011 * modelled as structured [[Format.FormatSpec ]]s, and use those to decide how to inteprolate the
@@ -93,12 +94,28 @@ object Format {
9394 conversion == 's' && ! hasWidth && ! hasPrecision &&
9495 (flags & SimpleStringDisqualifierFlags ) == 0
9596
96- def withStarWidth (newWidth : Int ): FormatSpec =
97- new FormatSpec ((bits & ~ (NumberMask << WidthShift )) | (encodeNumber(newWidth) << WidthShift ))
97+ // Jsonnet documents std.format as Python-style formatting. Python treats a negative
98+ // dynamic width as left adjustment with the absolute width.
99+ def withStarWidth (newWidth : Int ): FormatSpec = {
100+ val normalizedWidth =
101+ if (newWidth < 0 ) {
102+ if (newWidth == Int .MinValue )
103+ throw new Exception (" Format width/precision is too large: " + newWidth)
104+ - newWidth
105+ } else newWidth
106+ val normalizedFlags = if (newWidth < 0 ) flags | LeftAdjustedFlag else flags
107+ new FormatSpec (
108+ (bits & ~ (NumberMask << WidthShift ) & ~ (FlagsMask << FlagsShift )) |
109+ (normalizedFlags.toLong << FlagsShift ) |
110+ (encodeNumber(normalizedWidth) << WidthShift )
111+ )
112+ }
98113
114+ // Python treats a negative dynamic precision as zero; %g later maps precision 0 to 1.
99115 def withStarPrecision (newPrecision : Int ): FormatSpec =
100116 new FormatSpec (
101- (bits & ~ (NumberMask << PrecisionShift )) | (encodeNumber(newPrecision) << PrecisionShift )
117+ (bits & ~ (NumberMask << PrecisionShift )) |
118+ (encodeNumber(math.max(newPrecision, 0 )) << PrecisionShift )
102119 )
103120
104121 def withStarValues (newWidth : Int , newPrecision : Int ): FormatSpec =
@@ -241,6 +258,19 @@ object Format {
241258 def widenRaw (formatted : FormatSpec , txt : String ): String =
242259 if (! formatted.hasWidth) txt // fast path: no width/padding needed
243260 else widen(formatted, " " , " " , txt, numeric = false , signedConversion = false )
261+
262+ private def formatString (formatted : FormatSpec , txt : String ): String = {
263+ val truncated =
264+ if (! formatted.hasPrecision) txt
265+ else {
266+ val precision = formatted.precisionValue
267+ if (precision == 0 ) " "
268+ else if (txt.codePointCount(0 , txt.length) <= precision) txt
269+ else txt.substring(0 , txt.offsetByCodePoints(0 , precision))
270+ }
271+ widenRaw(formatted, truncated)
272+ }
273+
244274 def widen (
245275 formatted : FormatSpec ,
246276 lhs : String ,
@@ -645,7 +675,7 @@ object Format {
645675 )
646676 }
647677 i += 1
648- formatted = formatted. withStarWidth(width.asInt )
678+ formatted = withStarWidth(formatted, width, idx )
649679 valuesArr.value(i)
650680 case (false , true ) =>
651681 val precision = valuesArr.value(i)
@@ -655,7 +685,7 @@ object Format {
655685 )
656686 }
657687 i += 1
658- formatted = formatted. withStarPrecision(precision.asInt )
688+ formatted = withStarPrecision(formatted, precision, idx )
659689 valuesArr.value(i)
660690 case (true , true ) =>
661691 val width = valuesArr.value(i)
@@ -672,7 +702,8 @@ object Format {
672702 )
673703 }
674704 i += 1
675- formatted = formatted.withStarValues(width.asInt, precision.asInt)
705+ formatted =
706+ withStarPrecision(withStarWidth(formatted, width, idx), precision, idx)
676707 valuesArr.value(i)
677708 }
678709 } else {
@@ -706,7 +737,8 @@ object Format {
706737 pos
707738 )
708739 }
709- widenRaw(formatted, vs.str)
740+ if (formatted.conversion == 's' ) formatString(formatted, vs.str)
741+ else widenRaw(formatted, vs.str)
710742 case vn : Val .Num =>
711743 val s = vn.asDouble
712744 formatted.conversion match {
@@ -728,7 +760,7 @@ object Format {
728760 val c = if (codePoint >= 0xd800 && codePoint <= 0xdfff ) 0xfffd else codePoint
729761 widenRaw(formatted, Character .toString(c))
730762 case 's' =>
731- widenRaw (formatted, RenderUtils .renderDouble(s))
763+ formatString (formatted, RenderUtils .renderDouble(s))
732764 case _ =>
733765 Error .fail(
734766 " unsupported format conversion at position %d, got number" .format(i)
@@ -752,7 +784,7 @@ object Format {
752784 )
753785 case _ : Val .Null =>
754786 formatted.conversion match {
755- case 's' => widenRaw (formatted, " null" )
787+ case 's' => formatString (formatted, " null" )
756788 case 'c' => Error .fail(" %c expected number or string, got null" )
757789 case _ =>
758790 Error .fail(
@@ -766,7 +798,7 @@ object Format {
766798 case r : Val .Obj => Materializer .apply0(r, new Renderer (indent = - 1 ))
767799 case _ => Materializer (rawVal)
768800 }
769- widenRaw (formatted, value.toString)
801+ formatString (formatted, value.toString)
770802 }
771803 i += 1
772804 formattedValue
@@ -807,7 +839,7 @@ object Format {
807839 case 'g' => formatGeneric(formatted, numericValue).toLowerCase
808840 case 'G' => formatGeneric(formatted, numericValue)
809841 case 'c' => Error .fail(" %c expected number or string, got boolean" )
810- case 's' => widenRaw (formatted, textValue)
842+ case 's' => formatString (formatted, textValue)
811843 case _ =>
812844 Error .fail(
813845 " expected number or string at position %d, got boolean" .format(index)
@@ -816,7 +848,7 @@ object Format {
816848
817849 private def formatStrictBoolean (formatted : FormatSpec , index : Int , value : String ): String =
818850 formatted.conversion match {
819- case 's' => widenRaw (formatted, value)
851+ case 's' => formatString (formatted, value)
820852 case 'c' => Error .fail(" %c expected number or string, got boolean" )
821853 case _ =>
822854 Error .fail(
@@ -984,14 +1016,47 @@ object Format {
9841016 case _ => false
9851017 }
9861018
1019+ private def withStarWidth (formatted : FormatSpec , width : Val , idx : Int ): FormatSpec =
1020+ try formatted.withStarWidth(starInteger(width, " width" , idx))
1021+ catch {
1022+ case e : Error => throw e
1023+ case e : Exception =>
1024+ Error .fail(e.getMessage)
1025+ }
1026+
1027+ private def withStarPrecision (formatted : FormatSpec , precision : Val , idx : Int ): FormatSpec =
1028+ try formatted.withStarPrecision(starInteger(precision, " precision" , idx))
1029+ catch {
1030+ case e : Error => throw e
1031+ case e : Exception =>
1032+ Error .fail(e.getMessage)
1033+ }
1034+
1035+ private def starInteger (value : Val , name : String , idx : Int ): Int =
1036+ value match {
1037+ case n : Val .Num =>
1038+ val d = n.asDouble
1039+ // Jsonnet has one number type, so Python's "integer required for *" rule maps to
1040+ // finite whole-number values. Fractional star arguments must not be rounded.
1041+ if (! d.isWhole || ! d.isValidInt)
1042+ Error .fail(" * %s at position %d requires an integer" .format(name, idx))
1043+ d.toInt
1044+ case _ =>
1045+ Error .fail(" * %s at position %d requires an integer" .format(name, idx))
1046+ }
1047+
1048+ // std.format follows Python's % formatting. Jsonnet numbers have IEEE double semantics, so
1049+ // floating-point formats preserve the sign bit of -0.0 even though -0.0 == 0.0.
1050+ private def isNegative (s : Double ): Boolean = s < 0 || (s == 0.0 && 1.0 / s < 0 )
1051+
9871052 private def formatInteger (formatted : FormatSpec , s : Double ): String = {
9881053 formatIntegralRadix(formatted, s, 10 , _ => " " )
9891054 }
9901055
9911056 private def formatFloat (formatted : FormatSpec , s : Double ): String = {
9921057 widen(
9931058 formatted,
994- if (s < 0 ) " -" else " " ,
1059+ if (isNegative(s) ) " -" else " " ,
9951060 " " ,
9961061 sjsonnet.DecimalFormat
9971062 .format(
@@ -1002,7 +1067,7 @@ object Format {
10021067 math.abs(s)
10031068 ),
10041069 numeric = true ,
1005- signedConversion = s >= 0
1070+ signedConversion = ! isNegative(s)
10061071 )
10071072
10081073 }
@@ -1051,14 +1116,31 @@ object Format {
10511116 }
10521117 }
10531118
1119+ private def decimalExponent (value : Double ): Int = {
1120+ if (value == 0 || value.isNaN || value.isInfinity) 0
1121+ else {
1122+ val exact = new java.math.BigDecimal (value)
1123+ var exponent = math.floor(math.log10(value)).toInt
1124+ while (exact.compareTo(java.math.BigDecimal .ONE .scaleByPowerOfTen(exponent + 1 )) >= 0 ) {
1125+ exponent += 1
1126+ }
1127+ while (exact.compareTo(java.math.BigDecimal .ONE .scaleByPowerOfTen(exponent)) < 0 ) {
1128+ exponent -= 1
1129+ }
1130+ exponent
1131+ }
1132+ }
1133+
10541134 private def roundedGenericExponent (s : Double , precision : Int ): Int = {
10551135 if (s == 0 ) 0
10561136 else {
10571137 val abs = math.abs(s)
1058- val rawExponent = math.floor(math.log10( abs)).toInt
1138+ val rawExponent = decimalExponent( abs)
10591139 val scale = math.pow(10 , rawExponent - precision + 1 )
1060- val rounded = Math .round(abs / scale) * scale
1061- if (rounded == 0 ) 0 else math.floor(math.log10(rounded)).toInt
1140+ val roundedDigits = Math .round(abs / scale)
1141+ if (roundedDigits == 0 ) 0
1142+ else if (roundedDigits.toDouble >= math.pow(10 , precision)) rawExponent + 1
1143+ else rawExponent
10621144 }
10631145 }
10641146
@@ -1069,7 +1151,7 @@ object Format {
10691151 if (exponent < - 4 || exponent >= precision) {
10701152 widen(
10711153 formatted,
1072- if (s < 0 ) " -" else " " ,
1154+ if (isNegative(s) ) " -" else " " ,
10731155 " " ,
10741156 sjsonnet.DecimalFormat
10751157 .format(
@@ -1080,13 +1162,13 @@ object Format {
10801162 math.abs(s)
10811163 ),
10821164 numeric = true ,
1083- signedConversion = s >= 0
1165+ signedConversion = ! isNegative(s)
10841166 )
10851167 } else {
1086- val fractionalPrecision = math.max(0 , precision - exponent - 1 )
1168+ val fractionalPrecision = math.max(0 , precision - 1 - exponent )
10871169 widen(
10881170 formatted,
1089- if (s < 0 ) " -" else " " ,
1171+ if (isNegative(s) ) " -" else " " ,
10901172 " " ,
10911173 sjsonnet.DecimalFormat
10921174 .format(
@@ -1097,7 +1179,7 @@ object Format {
10971179 math.abs(s)
10981180 ),
10991181 numeric = true ,
1100- signedConversion = s >= 0
1182+ signedConversion = ! isNegative(s)
11011183 )
11021184 }
11031185
@@ -1106,7 +1188,7 @@ object Format {
11061188 private def formatExponent (formatted : FormatSpec , s : Double ): String = {
11071189 widen(
11081190 formatted,
1109- if (s < 0 ) " -" else " " ,
1191+ if (isNegative(s) ) " -" else " " ,
11101192 " " ,
11111193 sjsonnet.DecimalFormat
11121194 .format(
@@ -1117,7 +1199,7 @@ object Format {
11171199 math.abs(s)
11181200 ),
11191201 numeric = true ,
1120- signedConversion = s >= 0
1202+ signedConversion = ! isNegative(s)
11211203 )
11221204 }
11231205
0 commit comments