From dbcdb036d61d2d2bf2d3ea43edae23336780040b Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Mon, 24 Aug 2026 22:49:55 +0800 Subject: [PATCH 1/3] Replace boost/algorithm with standalone string utils --- src/stan/io/json/json_data_handler.hpp | 17 ++- src/stan/io/stan_csv_reader.hpp | 27 +++-- src/stan/io/string_utils.hpp | 108 ++++++++++++++++++ src/test/unit/io/stan_csv_reader_test.cpp | 1 - src/test/unit/services/check_adaptation.hpp | 22 ++-- .../optimize/laplace_jacobian_test.cpp | 1 - .../services/optimize/laplace_sample_test.cpp | 1 - .../sample/hmc_nuts_diag_e_adapt_test.cpp | 14 +-- .../sample/standalone_gqs_parallel_test.cpp | 1 - .../services/sample/standalone_gqs_test.cpp | 1 - src/test/unit/util.hpp | 3 +- 11 files changed, 144 insertions(+), 52 deletions(-) create mode 100644 src/stan/io/string_utils.hpp diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 3e0adeb279d..764f1187fad 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -15,7 +16,6 @@ #include #include #include -#include #include namespace stan { @@ -147,7 +147,7 @@ class json_data_handler : public stan::json::json_handler { } inline std::string key_str() { - return boost::algorithm::join(key_stack, "."); + return stan::io::join(key_stack, "."); } std::string outer_key_str() { @@ -181,7 +181,7 @@ class json_data_handler : public stan::json::json_handler { std::string key; stack.pop_back(); while (!stack.empty()) { - key = boost::algorithm::join(stack, "."); + key = stan::io::join(stack, "."); if (slot_types_map[key] == meta_type::ARRAY_OF_TUPLES) return true; stack.pop_back(); @@ -194,12 +194,12 @@ class json_data_handler : public stan::json::json_handler { std::string key; stack.pop_back(); while (!stack.empty()) { - key = boost::algorithm::join(stack, "."); + key = stan::io::join(stack, "."); if (slot_dims_map.count(key) == 1) return slot_dims_map[key]; stack.pop_back(); } - key = boost::algorithm::join(keys, "."); + key = stan::io::join(keys, "."); if (slot_dims_map.count(key) != 1) unexpected_error(key, "not an array"); return slot_dims_map[key]; @@ -210,13 +210,13 @@ class json_data_handler : public stan::json::json_handler { std::string key; stack.pop_back(); while (!stack.empty()) { - key = boost::algorithm::join(stack, "."); + key = stan::io::join(stack, "."); if (slot_dims_map.count(key) == 1) break; stack.pop_back(); } if (stack.empty()) { - key = boost::algorithm::join(key_stack, "."); + key = stan::io::join(key_stack, "."); unexpected_error(key, "ill-formed array"); } slot_dims_map[key] = update; @@ -354,8 +354,7 @@ class json_data_handler : public stan::json::json_handler { continue; } std::vector all_dims; - std::vector slots; - split(slots, var.first, boost::is_any_of("."), boost::token_compress_on); + std::vector slots = stan::io::split(var.first, ".", true); std::string slot; for (size_t i = 0; i < slots.size(); ++i) { slot.append(slots[i]); diff --git a/src/stan/io/stan_csv_reader.hpp b/src/stan/io/stan_csv_reader.hpp index b4e14bcc702..4b5b217c793 100644 --- a/src/stan/io/stan_csv_reader.hpp +++ b/src/stan/io/stan_csv_reader.hpp @@ -1,7 +1,7 @@ #ifndef STAN_IO_STAN_CSV_READER_HPP #define STAN_IO_STAN_CSV_READER_HPP -#include +#include #include #include #include @@ -15,8 +15,7 @@ namespace io { inline void prettify_stan_csv_name(std::string& variable) { if (variable.find_first_of(":.") != std::string::npos) { - std::vector parts; - boost::split(parts, variable, boost::is_any_of(":")); + std::vector parts = stan::io::split(variable, ":"); for (auto& part : parts) { int pos = part.find('.'); if (pos > 0) { @@ -25,7 +24,7 @@ inline void prettify_stan_csv_name(std::string& variable) { part += "]"; } } - variable = boost::algorithm::join(parts, "."); + variable = stan::io::join(parts, "."); } } @@ -126,10 +125,10 @@ class stan_csv_reader { size_t equal = lhs.find("="); if (equal != std::string::npos) { name = lhs.substr(0, equal); - boost::trim(name); + stan::io::trim(name); value = lhs.substr(equal + 1, lhs.size()); - boost::trim(value); - boost::replace_first(value, " (Default)", ""); + stan::io::trim(value); + stan::io::remove_first(value, " (Default)"); } else { if (lhs.compare(" data") == 0) { ss >> comment; @@ -138,9 +137,9 @@ class stan_csv_reader { size_t equal = lhs.find("="); if (equal != std::string::npos) { name = lhs.substr(0, equal); - boost::trim(name); + stan::io::trim(name); value = lhs.substr(equal + 2, lhs.size()); - boost::replace_first(value, " (Default)", ""); + stan::io::remove_first(value, " (Default)"); } if (name.compare("file") == 0) @@ -176,7 +175,7 @@ class stan_csv_reader { std::stringstream(value) >> metadata.chain_id; } else if (name.compare("init") == 0) { metadata.init = value; - boost::trim(metadata.init); + stan::io::trim(metadata.init); } else if (name.compare("seed") == 0) { std::stringstream(value) >> metadata.seed; metadata.random_seed = false; @@ -209,7 +208,7 @@ class stan_csv_reader { while (ss.good()) { std::string token; std::getline(ss, token, ','); - boost::trim(token); + stan::io::trim(token); if (prettify_name) { prettify_stan_csv_name(token); @@ -239,7 +238,7 @@ class stan_csv_reader { // parse stepsize std::getline(ss, line, '='); // stepsize - boost::trim(line); + stan::io::trim(line); ss >> adaptation.step_size; if (lines == 2) // ADVI reports stepsize, no metric return; @@ -265,7 +264,7 @@ class stan_csv_reader { for (int col = 0; col < cols; col++) { std::string token; std::getline(line_ss, token, ','); - boost::trim(token); + stan::io::trim(token); std::stringstream(token) >> adaptation.metric(row, col); } std::getline(ss, line); @@ -335,7 +334,7 @@ class stan_csv_reader { std::stringstream ls(line); for (int col = 0; col < cols; col++) { std::getline(ls, line, ','); - boost::trim(line); + stan::io::trim(line); try { samples(row, col) = static_cast(std::stold(line)); // If the value read is out of the range of representable values by diff --git a/src/stan/io/string_utils.hpp b/src/stan/io/string_utils.hpp new file mode 100644 index 00000000000..8d5d31a94fe --- /dev/null +++ b/src/stan/io/string_utils.hpp @@ -0,0 +1,108 @@ +#ifndef STAN_IO_STRING_UTILS_HPP +#define STAN_IO_STRING_UTILS_HPP + +#include +#include +#include + +namespace stan { +namespace io { + +/** + * Joins a vector of strings/string_views into a single string, + * separated by `delimiter`. + * + * @param container strings to join + * @param delimiter separator inserted between parts + * @return the joined string + */ +template +inline std::string join(T&& container, const std::string_view& delimiter) { + if (container.empty()) { + return ""; + } + std::size_t total = (container.size() - 1) * delimiter.size(); + for (const auto& element : container) { + total += std::string_view(element).size(); + } + std::string result; + result.reserve(total); + bool first = true; + for (const auto& element : container) { + if (!first) { + result.append(delimiter); + } + result.append(std::string_view(element)); + first = false; + } + return result; +} + +/** + * Splits a string on any character found in `delimiters`. + * + * Empty substrings are preserved. Empty delimiters return the input as a + * single element. When `compress_delims` is true, consecutive delimiters are + * treated as a single delimiter. + * + * @param input string to split + * @param delimiters separators between parts + * @param compress_tokens whether to combine consecutive delimiters + * @return the split strings + */ +inline std::vector split(const std::string_view& input, + const std::string_view& delimiters, + bool compress_delims = false) { + std::vector result; + std::size_t start = 0; + while (start <= input.size()) { + const std::size_t end = input.find_first_of(delimiters, start); + if (end == std::string_view::npos) { + result.emplace_back(input.substr(start)); + break; + } + result.emplace_back(input.substr(start, end - start)); + start = end + 1; + if (compress_delims) { + const std::size_t next = input.find_first_not_of(delimiters, start); + start = (next == std::string_view::npos) ? input.size() : next; + } + } + return result; +} + +/** + * Remove leading and trailing whitespace from a string in place. + * + * @param input string to trim + */ +inline void trim(std::string& input) { + constexpr std::string_view whitespace = " \t\n\r\f\v"; + const std::size_t start = input.find_first_not_of(whitespace); + if (start == std::string::npos) { + input.clear(); + return; + } + const std::size_t end = input.find_last_not_of(whitespace); + input.erase(end + 1); + input.erase(0, start); +} + +/** + * Removes the first occurrence of a substring from a string in place. + * + * @param input string to modify + * @param substring substring to remove + */ +inline void remove_first(std::string& input, + const std::string_view& substring) { + const std::size_t position = input.find(substring); + if (position != std::string::npos) { + input.erase(position, substring.size()); + } +} + +} +} + +#endif diff --git a/src/test/unit/io/stan_csv_reader_test.cpp b/src/test/unit/io/stan_csv_reader_test.cpp index f913615732d..4f77e3a03e1 100644 --- a/src/test/unit/io/stan_csv_reader_test.cpp +++ b/src/test/unit/io/stan_csv_reader_test.cpp @@ -1,6 +1,5 @@ #include #include -#include #include #include #include diff --git a/src/test/unit/services/check_adaptation.hpp b/src/test/unit/services/check_adaptation.hpp index af863ea8139..e5a70acb46b 100644 --- a/src/test/unit/services/check_adaptation.hpp +++ b/src/test/unit/services/check_adaptation.hpp @@ -7,7 +7,7 @@ #include #include #include -#include +#include namespace stan { namespace test { @@ -28,9 +28,8 @@ void check_adaptation(const size_t& num_params, break; } } - std::vector strs; - boost::split(strs, param_strings[offset], boost::is_any_of(", "), - boost::token_compress_on); + std::vector strs = + stan::io::split(param_strings[offset], ", ", true); EXPECT_EQ(num_params, strs.size()); for (size_t i = 0; i < num_params; i++) { ASSERT_NEAR(param_vals[i], test::unit::stod(strs[i]), err_margin); @@ -51,9 +50,8 @@ void check_adaptation(const size_t& num_rows, const size_t& num_cols, } } for (size_t i = 0, ij = 0; i < num_rows; i++) { - std::vector strs; - boost::split(strs, param_strings[offset + i], boost::is_any_of(", "), - boost::token_compress_on); + std::vector strs = + stan::io::split(param_strings[offset + i], ", ", true); EXPECT_EQ(num_cols, strs.size()); for (size_t j = 0; j < num_cols; j++, ij++) { ASSERT_NEAR(param_vals[ij], test::unit::stod(strs[j]), err_margin); @@ -74,9 +72,8 @@ void check_different(const size_t& num_params, break; } } - std::vector strs; - boost::split(strs, param_strings[offset], boost::is_any_of(", "), - boost::token_compress_on); + std::vector strs = + stan::io::split(param_strings[offset], ", ", true); EXPECT_EQ(num_params, strs.size()); for (size_t i = 0; i < num_params; i++) { ASSERT_GT(fabs(param_vals[i] - test::unit::stod(strs[i])), margin); @@ -97,9 +94,8 @@ void check_different(const size_t& num_rows, const size_t& num_cols, } } for (size_t i = 0, ij = 0; i < num_rows; i++) { - std::vector strs; - boost::split(strs, param_strings[offset + i], boost::is_any_of(", "), - boost::token_compress_on); + std::vector strs = + stan::io::split(param_strings[offset + i], ", ", true); EXPECT_EQ(num_cols, strs.size()); for (size_t j = 0; j < num_cols; j++, ij++) { ASSERT_GT(fabs(param_vals[ij] - test::unit::stod(strs[j])), margin); diff --git a/src/test/unit/services/optimize/laplace_jacobian_test.cpp b/src/test/unit/services/optimize/laplace_jacobian_test.cpp index 7abeadc9455..874bef71b21 100644 --- a/src/test/unit/services/optimize/laplace_jacobian_test.cpp +++ b/src/test/unit/services/optimize/laplace_jacobian_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/unit/services/optimize/laplace_sample_test.cpp b/src/test/unit/services/optimize/laplace_sample_test.cpp index e292305cb70..a90c133f99f 100644 --- a/src/test/unit/services/optimize/laplace_sample_test.cpp +++ b/src/test/unit/services/optimize/laplace_sample_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/unit/services/sample/hmc_nuts_diag_e_adapt_test.cpp b/src/test/unit/services/sample/hmc_nuts_diag_e_adapt_test.cpp index bfcdc14d6a7..071661664f0 100644 --- a/src/test/unit/services/sample/hmc_nuts_diag_e_adapt_test.cpp +++ b/src/test/unit/services/sample/hmc_nuts_diag_e_adapt_test.cpp @@ -1,9 +1,9 @@ #include #include #include +#include #include #include -#include #include class ServicesSampleHmcNutsDiagEAdapt : public testing::Test { @@ -274,8 +274,7 @@ TEST_F(ServicesSampleHmcNutsDiagEAdapt, term_buffer_1) { std::vector messages = parameter.string_values(); for (auto msg : messages) { if (msg.find("Step size") != std::string::npos) { - std::vector toks; - boost::split(toks, msg, boost::is_any_of(" ")); + std::vector toks = stan::io::split(msg, " "); auto adapted = std::stod(toks[toks.size() - 1]); EXPECT_NEAR(draw[2], adapted, 1e-5); } @@ -360,8 +359,7 @@ TEST_F(ServicesSampleHmcNutsDiagEAdapt, schedule_a) { std::vector messages = parameter.string_values(); for (auto msg : messages) { if (msg.find("Step size") != std::string::npos) { - std::vector toks; - boost::split(toks, msg, boost::is_any_of(" ")); + std::vector toks = stan::io::split(msg, " "); auto adapted = std::stod(toks[toks.size() - 1]); EXPECT_NEAR(draw[2], adapted, 1e-5); } @@ -406,8 +404,7 @@ TEST_F(ServicesSampleHmcNutsDiagEAdapt, schedule_b) { std::vector messages = parameter.string_values(); for (auto msg : messages) { if (msg.find("Step size") != std::string::npos) { - std::vector toks; - boost::split(toks, msg, boost::is_any_of(" ")); + std::vector toks = stan::io::split(msg, " "); auto adapted = std::stod(toks[toks.size() - 1]); EXPECT_NEAR(draw[2], adapted, 1e-5); } @@ -452,8 +449,7 @@ TEST_F(ServicesSampleHmcNutsDiagEAdapt, schedule_c) { std::vector messages = parameter.string_values(); for (auto msg : messages) { if (msg.find("Step size") != std::string::npos) { - std::vector toks; - boost::split(toks, msg, boost::is_any_of(" ")); + std::vector toks = stan::io::split(msg, " "); auto adapted = std::stod(toks[toks.size() - 1]); EXPECT_NEAR(draw[2], adapted, 1e-5); } diff --git a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp index 3e8fe43cfe3..137585a4843 100644 --- a/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp +++ b/src/test/unit/services/sample/standalone_gqs_parallel_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/unit/services/sample/standalone_gqs_test.cpp b/src/test/unit/services/sample/standalone_gqs_test.cpp index 653b063e840..d8354d55b3b 100644 --- a/src/test/unit/services/sample/standalone_gqs_test.cpp +++ b/src/test/unit/services/sample/standalone_gqs_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include diff --git a/src/test/unit/util.hpp b/src/test/unit/util.hpp index f47ddcad39a..640e141871f 100644 --- a/src/test/unit/util.hpp +++ b/src/test/unit/util.hpp @@ -3,7 +3,6 @@ #include -#include #include #include #include @@ -49,7 +48,7 @@ void match_csv_columns(const Eigen::MatrixXd& samples, break; } cells.clear(); - boost::algorithm::split(cells, line, boost::is_any_of(",")); + cells = stan::io::split(line, ","); for (size_t i = 0; i < num_columns; ++i) { cell_ss.str(std::string()); cell_ss.clear(); From 9d78b4e79bf69a3ddd8d1961fe4e2d8a833ceed4 Mon Sep 17 00:00:00 2001 From: Stan Jenkins Date: Mon, 24 Aug 2026 10:53:44 -0400 Subject: [PATCH 2/3] [Jenkins] auto-formatting by clang-format version 10.0.0-4ubuntu1 --- src/stan/io/json/json_data_handler.hpp | 4 +--- src/stan/io/string_utils.hpp | 4 ++-- src/test/unit/services/check_adaptation.hpp | 16 ++++++++-------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 764f1187fad..e7f4480c35b 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -146,9 +146,7 @@ class json_data_handler : public stan::json::json_handler { array_start_r = 0; } - inline std::string key_str() { - return stan::io::join(key_stack, "."); - } + inline std::string key_str() { return stan::io::join(key_stack, "."); } std::string outer_key_str() { std::string result; diff --git a/src/stan/io/string_utils.hpp b/src/stan/io/string_utils.hpp index 8d5d31a94fe..1a03046dc0a 100644 --- a/src/stan/io/string_utils.hpp +++ b/src/stan/io/string_utils.hpp @@ -102,7 +102,7 @@ inline void remove_first(std::string& input, } } -} -} +} // namespace io +} // namespace stan #endif diff --git a/src/test/unit/services/check_adaptation.hpp b/src/test/unit/services/check_adaptation.hpp index e5a70acb46b..bfd900f7aa8 100644 --- a/src/test/unit/services/check_adaptation.hpp +++ b/src/test/unit/services/check_adaptation.hpp @@ -28,8 +28,8 @@ void check_adaptation(const size_t& num_params, break; } } - std::vector strs = - stan::io::split(param_strings[offset], ", ", true); + std::vector strs + = stan::io::split(param_strings[offset], ", ", true); EXPECT_EQ(num_params, strs.size()); for (size_t i = 0; i < num_params; i++) { ASSERT_NEAR(param_vals[i], test::unit::stod(strs[i]), err_margin); @@ -50,8 +50,8 @@ void check_adaptation(const size_t& num_rows, const size_t& num_cols, } } for (size_t i = 0, ij = 0; i < num_rows; i++) { - std::vector strs = - stan::io::split(param_strings[offset + i], ", ", true); + std::vector strs + = stan::io::split(param_strings[offset + i], ", ", true); EXPECT_EQ(num_cols, strs.size()); for (size_t j = 0; j < num_cols; j++, ij++) { ASSERT_NEAR(param_vals[ij], test::unit::stod(strs[j]), err_margin); @@ -72,8 +72,8 @@ void check_different(const size_t& num_params, break; } } - std::vector strs = - stan::io::split(param_strings[offset], ", ", true); + std::vector strs + = stan::io::split(param_strings[offset], ", ", true); EXPECT_EQ(num_params, strs.size()); for (size_t i = 0; i < num_params; i++) { ASSERT_GT(fabs(param_vals[i] - test::unit::stod(strs[i])), margin); @@ -94,8 +94,8 @@ void check_different(const size_t& num_rows, const size_t& num_cols, } } for (size_t i = 0, ij = 0; i < num_rows; i++) { - std::vector strs = - stan::io::split(param_strings[offset + i], ", ", true); + std::vector strs + = stan::io::split(param_strings[offset + i], ", ", true); EXPECT_EQ(num_cols, strs.size()); for (size_t j = 0; j < num_cols; j++, ij++) { ASSERT_GT(fabs(param_vals[ij] - test::unit::stod(strs[j])), margin); From 9b0c08bc70c673c7d5a43b41bab83dd28c91e7f1 Mon Sep 17 00:00:00 2001 From: Andrew Johnson Date: Tue, 25 Aug 2026 11:05:21 +0800 Subject: [PATCH 3/3] Fix merge --- src/stan/io/json/json_data_handler.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stan/io/json/json_data_handler.hpp b/src/stan/io/json/json_data_handler.hpp index 57bbbda3855..70177a0bda9 100644 --- a/src/stan/io/json/json_data_handler.hpp +++ b/src/stan/io/json/json_data_handler.hpp @@ -6,7 +6,7 @@ #include #include #include -#include +#include #include #include #include