Repository: bitcoin/bitcoin
File: src/util/string.h (C/C++)

Code before:
#include <sstream>
#include <string>      // IWYU pragma: export
#include <string_view> // IWYU pragma: export
#include <vector>

namespace util {
void ReplaceAll(std::string& in_out, const std::string& search, const std::string& substitute);

/** Split a string on any char found in separators, returning a vector.
 *
 * If sep does not occur in sp, a singleton with the entirety of sp is returned.
 *
 * Note that this function does not care about braces, so splitting
 * "foo(bar(1),2),3) on ',' will return {"foo(bar(1)", "2)", "3)"}.
 */
template <typename T = Span<const char>>
std::vector<T> Split(const Span<const char>& sp, std::string_view separators)
{
    std::vector<T> ret;
    auto it = sp.begin();
    auto start = it;
    while (it != sp.end()) {
        if (separators.find(*it) != std::string::npos) {
            ret.emplace_back(start, it);
            start = it + 1;
        }
        ++it;
    }
    ret.emplace_back(start, it);
    return ret;
}

/** Split a string on every instance of sep, returning a vector.
 *
 * If sep does not occur in sp, a singleton with the entirety of sp is returned.
 *
 * Note that this function does not care about braces, so splitting
 * "foo(bar(1),2),3) on ',' will return {"foo(bar(1)", "2)", "3)"}.
 */
template <typename T = Span<const char>>
std::vector<T> Split(const Span<const char>& sp, char sep)
{
    return Split<T>(sp, std::string_view{&sep, 1});
}

[[nodiscard]] inline std::vector<std::string> SplitString(std::string_view str, char sep)
{
    return Split<std::string>(str, sep);
}

[[nodiscard]] inline std::vector<std::string> SplitString(std::string_view str, std::string_view separators)

Review comment (bug):
While it gets the job done, I think the parser is unnecessarily complicated for several reasons:
* Redundant Counters: Separate (but mutually exclusive) `count_normal` and `count_pos`.
* Repetition: The check for "Format specifier incorrectly terminated by end of string" is repeated in multiple places.
* Inefficient Character Handling: Multiple character checks for `%` (and manual digit detection).
* Scattered Iterations: Increment operations (++it), continue statements, and lookaheads (str.end()) make the control flow harder to follow.

I have a hard time explaining the behavior of the parser in words, following the logic of the code. Can we use a state-machine based parser instead, without lookaround, iterating char by char without jumping around, changing the state as we go along, e.g. something like this:
```C++
constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
{
    unsigned count{0};
    unsigned maybe_num{0};
    bool inside_format_specifier = false;
    bool has_normal = false, has_positional = false;
    for (auto it = str.begin(); it < str.end(); ++it) {
        if (*it == '%') inside_format_specifier = !inside_format_specifier;
        else if (inside_format_specifier) {
            if (std::isdigit(*it)) maybe_num = maybe_num * 10 + (*it - '0');
            else if (*it == '$') {
                if (maybe_num == 0) throw "Positional format specifier must be at least 1";
                count = std::max(count, maybe_num);
                has_positional = true;
                inside_format_specifier = false;
            } else {
                ++count;
                maybe_num = 0;
                has_normal = true;
                inside_format_specifier = false;
            }
        } else maybe_num = 0;
    }

    if (has_normal && has_positional) throw "Format specifiers must be all positional or all non-positional!";
    if (inside_format_specifier || maybe_num > 0) throw "Format specifier incorrectly terminated by end of string";
    if (num_params != count) throw "Format specifier count must match the argument count!";
}
```

This is the reason I'm not ack-ig yet.

Code after:
#include <locale>
#include <sstream>
#include <string>      // IWYU pragma: export
#include <string_view> // IWYU pragma: export
#include <vector>

namespace util {
/**
 * @brief A wrapper for a compile-time partially validated format string
 *
 * This struct can be used to enforce partial compile-time validation of format
 * strings, to reduce the likelihood of tinyformat throwing exceptions at
 * run-time. Validation is partial to try and prevent the most common errors
 * while avoiding re-implementing the entire parsing logic.
 *
 * @note Counting of `*` dynamic width and precision fields (such as `%*c`,
 * `%2$*3$d`, `%.*f`) is not implemented to minimize code complexity as long as
 * they are not used in the codebase. Usage of these fields is not counted and
 * can lead to run-time exceptions. Code wanting to use the `*` specifier can
 * side-step this struct and call tinyformat directly.
 */
template <unsigned num_params>
struct ConstevalFormatString {
    const char* const fmt;
    consteval ConstevalFormatString(const char* str) : fmt{str} { Detail_CheckNumFormatSpecifiers(fmt); }
    constexpr static void Detail_CheckNumFormatSpecifiers(std::string_view str)
    {
        unsigned count_normal{0}; // Number of "normal" specifiers, like %s
        unsigned count_pos{0};    // Max number in positional specifier, like %8$s
        for (auto it{str.begin()}; it < str.end();) {
            if (*it != '%') {
                ++it;
                continue;
            }

            if (++it >= str.end()) throw "Format specifier incorrectly terminated by end of string";
            if (*it == '%') {
                // Percent escape: %%
                ++it;
                continue;
            }

            unsigned maybe_num{0};
            while ('0' <= *it && *it <= '9') {
                maybe_num *= 10;
                maybe_num += *it - '0';
                ++it;
            };

            if (*it == '$') {
                // Positional specifier, like %8$s