From f69edf810e3da858f1fcec8ebe25bf1dcc795a4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pa=CC=84vels=20Nadtoc=CC=8Cajevs?= <7645683+bruvzg@users.noreply.github.com> Date: Fri, 13 Feb 2026 23:43:15 +0200 Subject: [PATCH] [String] Allow using Java style `%[arg$]` prefix in `sprintf`. --- core/string/ustring.cpp | 97 +++++++++++++++++++++---------- tests/core/string/test_string.cpp | 9 +++ 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/core/string/ustring.cpp b/core/string/ustring.cpp index 2772ddf055..c605d8d48f 100644 --- a/core/string/ustring.cpp +++ b/core/string/ustring.cpp @@ -5176,10 +5176,13 @@ String String::sprintf(const Span &values, bool *error) const { static const String MINUS("-"); static const String PLUS("+"); + LocalVector used_args; + used_args.resize_initialized(values.size()); String formatted; char32_t *self = (char32_t *)get_data(); bool in_format = false; uint64_t value_index = 0; + int selected_index = -1; int min_chars = 0; int min_decimals = 0; bool in_decimals = false; @@ -5206,15 +5209,16 @@ String String::sprintf(const Span &values, bool *error) const { case 'o': // Octal case 'x': // Hexadecimal (lowercase) case 'X': { // Hexadecimal (uppercase) - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } - if (!values[value_index].is_num()) { + if (!values[index].is_num()) { return "a number is required"; } - int64_t value = values[value_index]; + int64_t value = values[index]; int base = 16; bool capitalize = false; switch (c) { @@ -5270,21 +5274,25 @@ String String::sprintf(const Span &values, bool *error) const { } formatted += str; - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; in_format = false; break; } case 'f': { // Float - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } - if (!values[value_index].is_num()) { + if (!values[index].is_num()) { return "a number is required"; } - double value = values[value_index]; + double value = values[index]; bool is_negative = std::signbit(value); String str = String::num(Math::abs(value), min_decimals); const bool is_finite = Math::is_finite(value); @@ -5316,17 +5324,21 @@ String String::sprintf(const Span &values, bool *error) const { } formatted += str; - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; in_format = false; break; } case 'v': { // Vector2/3/4/2i/3i/4i - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } int count; - switch (values[value_index].get_type()) { + switch (values[index].get_type()) { case Variant::VECTOR2: case Variant::VECTOR2I: { count = 2; @@ -5344,7 +5356,7 @@ String String::sprintf(const Span &values, bool *error) const { } } - Vector4 vec = values[value_index]; + Vector4 vec = values[index]; String str = "("; for (int i = 0; i < count; i++) { double val = vec[i]; @@ -5386,16 +5398,20 @@ String String::sprintf(const Span &values, bool *error) const { str += ")"; formatted += str; - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; in_format = false; break; } case 's': { // String - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } - String str = values[value_index]; + String str = values[index]; // Padding. if (left_justified) { str = str.rpad(min_chars); @@ -5404,19 +5420,23 @@ String String::sprintf(const Span &values, bool *error) const { } formatted += str; - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; in_format = false; break; } case 'c': { - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } // Convert to character. String str; - if (values[value_index].is_num()) { - int value = values[value_index]; + if (values[index].is_num()) { + int value = values[index]; if (value < 0) { return "unsigned integer is lower than minimum"; } else if (value >= 0xd800 && value <= 0xdfff) { @@ -5424,9 +5444,9 @@ String String::sprintf(const Span &values, bool *error) const { } else if (value > 0x10ffff) { return "unsigned integer is greater than maximum"; } - str = chr(values[value_index]); - } else if (values[value_index].get_type() == Variant::STRING) { - str = values[value_index]; + str = chr(values[index]); + } else if (values[index].get_type() == Variant::STRING) { + str = values[index]; if (str.length() != 1) { return "%c requires number or single-character string"; } @@ -5442,7 +5462,10 @@ String String::sprintf(const Span &values, bool *error) const { } formatted += str; - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; in_format = false; break; } @@ -5486,6 +5509,14 @@ String String::sprintf(const Span &values, bool *error) const { } break; } + case '$': { + if (min_chars > 0) { + selected_index = min_chars - 1; + } + min_chars = 0; + pad_with_zeros = false; + break; + } case '.': { // Float/Vector separator. if (in_decimals) { return "too many decimal points in format"; @@ -5496,27 +5527,30 @@ String String::sprintf(const Span &values, bool *error) const { } case '*': { // Dynamic width, based on value. - if (value_index >= values.size()) { + uint64_t index = (selected_index >= 0 ? selected_index : value_index); + if (index >= values.size()) { return "not enough arguments for format string"; } - Variant::Type value_type = values[value_index].get_type(); - if (!values[value_index].is_num() && + Variant::Type value_type = values[index].get_type(); + if (!values[index].is_num() && value_type != Variant::VECTOR2 && value_type != Variant::VECTOR2I && value_type != Variant::VECTOR3 && value_type != Variant::VECTOR3I && value_type != Variant::VECTOR4 && value_type != Variant::VECTOR4I) { return "* wants number or vector"; } - int size = values[value_index]; + int size = values[index]; if (in_decimals) { min_decimals = size; } else { min_chars = size; } - - ++value_index; + if (selected_index == -1) { + ++value_index; + } + used_args[index] = true; break; } @@ -5535,6 +5569,7 @@ String String::sprintf(const Span &values, bool *error) const { left_justified = false; show_sign = false; in_decimals = false; + selected_index = -1; break; default: formatted += c; @@ -5546,8 +5581,10 @@ String String::sprintf(const Span &values, bool *error) const { return "incomplete format"; } - if (value_index != values.size()) { - return "not all arguments converted during string formatting"; + for (const bool &b : used_args) { + if (!b) { + return "not all arguments converted during string formatting"; + } } if (error) { diff --git a/tests/core/string/test_string.cpp b/tests/core/string/test_string.cpp index ee1effe091..331a066f86 100644 --- a/tests/core/string/test_string.cpp +++ b/tests/core/string/test_string.cpp @@ -1296,6 +1296,15 @@ TEST_CASE("[String] sprintf") { REQUIRE(error == false); CHECK(output == String("fish 99.990 frog")); + ///// Argument indices. + format = "fish %2$d frog %1$s xx"; + args.clear(); + args.push_back("test"); + args.push_back(5); + output = format.sprintf(args, &error); + REQUIRE(error == false); + CHECK(output == String("fish 5 frog test xx")); + ///// Errors // More formats than arguments.