From bc2e2e040430410e7d7fb78d5b6316de2c2f25f2 Mon Sep 17 00:00:00 2001 From: He-Pin Date: Tue, 28 Jul 2026 02:24:13 +0800 Subject: [PATCH] fix: use codepoint count for std.format width padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Motivation: `std.format("%5s", "😀")` produced 3 spaces instead of 4 because width was computed using String.length (UTF-16 code units), counting the surrogate pair as 2. Python's % formatting defines width as codepoint count; go-jsonnet uses `len([]rune(str))`. Both yield 4 spaces. Modification: In `widen`, use `codePointCount` for non-numeric (string) paths where supplementary characters can appear. Numeric paths (%d/%f/%g/%x/%o) retain O(1) `.length` since their output is provably ASCII. This avoids O(n) scans on every numeric format call while fixing the string case. Result: Width padding counts supplementary characters as 1 codepoint, matching go-jsonnet and Python. Numeric formatting performance is unchanged. References: - Python docs: width is "minimum field width" in characters (codepoints) - go-jsonnet: uses len([]rune(str)) for width calculation --- sjsonnet/src/sjsonnet/Format.scala | 8 +++++++- .../new_test_suite/format_width_codepoint.jsonnet | 11 +++++++++++ .../format_width_codepoint.jsonnet.golden | 9 +++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet create mode 100644 sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet.golden diff --git a/sjsonnet/src/sjsonnet/Format.scala b/sjsonnet/src/sjsonnet/Format.scala index 77377dac..c8a30990 100644 --- a/sjsonnet/src/sjsonnet/Format.scala +++ b/sjsonnet/src/sjsonnet/Format.scala @@ -286,7 +286,13 @@ object Format { else if (signedConversion && formatted.signCharacter) "+" + lhs else lhs - val missingWidth = formatted.widthOr(-1) - lhs2.length - mhs.length - rhs.length + val missingWidth = + if (numeric) formatted.widthOr(-1) - lhs2.length - mhs.length - rhs.length + else + formatted.widthOr(-1) - + lhs2.codePointCount(0, lhs2.length) - + mhs.codePointCount(0, mhs.length) - + rhs.codePointCount(0, rhs.length) if (missingWidth <= 0) { // Avoid unnecessary string concatenation when parts are empty diff --git a/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet b/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet new file mode 100644 index 00000000..bb469103 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet @@ -0,0 +1,11 @@ +// Width padding must count codepoints, not UTF-16 code units. +// Supplementary characters (surrogate pairs) count as 1 codepoint. +[ + std.assertEqual("%5s" % "😀", " 😀"), + std.assertEqual("%-5s" % "😀", "😀 "), + std.assertEqual("%5s" % "a😀b", " a😀b"), + std.assertEqual("%3s" % "😀😀", " 😀😀"), + std.assertEqual("%5.1s" % "😀x", " 😀"), + std.assertEqual("%5s" % "hello", "hello"), + std.assertEqual("%10s" % "hello", " hello"), +] diff --git a/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet.golden b/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet.golden new file mode 100644 index 00000000..15f719b5 --- /dev/null +++ b/sjsonnet/test/resources/new_test_suite/format_width_codepoint.jsonnet.golden @@ -0,0 +1,9 @@ +[ + true, + true, + true, + true, + true, + true, + true +]