Skip to content

[pull] main from facebook:main #341

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions folly/Conv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -225,20 +225,38 @@ constexpr const std::array<
ErrorString,
static_cast<std::size_t>(ConversionCode::NUM_ERROR_CODES)>
kErrorStrings{{
// SUCCESS
{"Success", true},
// EMPTY_INPUT_STRING
{"Empty input string", true},
// NO_DIGITS
{"No digits found in input string", true},
// BOOL_OVERFLOW
{"Integer overflow when parsing bool (must be 0 or 1)", true},
// BOOL_INVALID_VALUE
{"Invalid value for bool", true},
// NON_DIGIT_CHAR
{"Non-digit character found", true},
// INVALID_LEADING_CHAR
{"Invalid leading character", true},
// POSITIVE_OVERFLOW
{"Overflow during conversion", true},
// NEGATIVE_OVERFLOW
{"Negative overflow during conversion", true},
// STRING_TO_FLOAT_ERROR
{"Unable to convert string to floating point value", true},
// NON_WHITESPACE_AFTER_END
{"Non-whitespace character found after end of conversion", true},
// ARITH_POSITIVE_OVERFLOW
{"Overflow during arithmetic conversion", false},
// ARITH_NEGATIVE_OVERFLOW
{"Negative overflow during arithmetic conversion", false},
// ARITH_LOSS_OF_PRECISION
{"Loss of precision during arithmetic conversion", false},
// SPLIT_ERROR,
{"Unexpected number of fields resulting from a split", true},
// CUSTOM,
{"Custom conversion failed", true},
}};

// Check if ASCII is really ASCII
Expand Down
2 changes: 2 additions & 0 deletions folly/Conv.h
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ enum class ConversionCode : unsigned char {
ARITH_POSITIVE_OVERFLOW,
ARITH_NEGATIVE_OVERFLOW,
ARITH_LOSS_OF_PRECISION,
SPLIT_ERROR,
CUSTOM,
NUM_ERROR_CODES, // has to be the last entry
};

Expand Down
2 changes: 1 addition & 1 deletion folly/Expected.h
Original file line number Diff line number Diff line change
Expand Up @@ -1557,7 +1557,7 @@ makeExpected(Value&& val) {
std::in_place, static_cast<Value&&>(val)};
}

// Suppress comparability of Optional<T> with T, despite implicit conversion.
// Suppress comparability of Expected<T> with T, despite implicit conversion.
template <class Value, class Error>
bool operator==(const Expected<Value, Error>&, const Value& other) = delete;
template <class Value, class Error>
Expand Down
80 changes: 80 additions & 0 deletions folly/String-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,78 @@ bool splitFixed(
return false;
}

// Overload for no remaining output fields; requires empty input.
template <class Delim>
Expected<Unit, SubstringConversionCode> trySplitTo(
StringPiece input, const Delim&) {
if (input.empty()) {
return unit;
}
return makeUnexpected(
SubstringConversionCode{input, ConversionCode::SPLIT_ERROR});
}

// Replace custom conversion codes with folly::ConversionCode::CUSTOM_OTHER.
template <class CustomCode>
inline ConversionCode convertError(CustomCode&&) {
return ConversionCode::CUSTOM;
}

inline ConversionCode convertError(ConversionCode code) {
return code;
}

// tryFieldTo helpers, wrapping tryTo<>, but adding support for std::ignore and
// replacing custom error types with ConversionCode::CUSTOM.
template <class Output>
Expected<Output, ConversionCode> tryFieldTo(folly::StringPiece input) {
if (auto result = tryTo<Output>(input)) {
return std::move(result.value());
} else {
return makeUnexpected(convertError(result.error()));
}
}

template <>
inline Expected<decltype(std::ignore), ConversionCode>
tryFieldTo<decltype(std::ignore)>(folly::StringPiece /*input*/) {
return std::ignore;
}

template <class Delim, class Output, class... Outputs>
Expected<Unit, SubstringConversionCode> trySplitTo(
StringPiece input,
const Delim& delim,
Output& output,
Outputs&... outputs) {
auto pos = input.find(delim);
if ((pos == std::string::npos) != (sizeof...(outputs) == 0)) {
return makeUnexpected(
SubstringConversionCode{input, ConversionCode::SPLIT_ERROR});
}
StringPiece head, tail;
if (pos == std::string::npos) {
head = input;
} else {
head = input.subpiece(0, pos);
tail = input.subpiece(pos + delimSize(delim));
}
// Eagerly attempt parsing the head value, but only assign on the way back
// from the recursive calls to ensure all outputs are untouched on failure.
if (auto headResult = tryFieldTo<Output>(head)) {
if (auto tailResult = trySplitTo(tail, delim, outputs...)) {
output = *headResult;
return unit;

} else {
return makeUnexpected(tailResult.error());
}
} else {
// First failure (left-to-right) is returned.
return makeUnexpected(SubstringConversionCode{head, headResult.error()});
}
}

} // namespace detail

//////////////////////////////////////////////////////////////////////
Expand Down Expand Up @@ -442,6 +514,14 @@ void splitTo(
detail::prepareDelim(delimiter), StringPiece(input), out, ignoreEmpty);
}

template <class Delim, class... OutputTypes>
typename std::enable_if<
StrictConjunction<IsConvertible<OutputTypes>...>::value,
Expected<Unit, SubstringConversionCode>>::type
trySplitTo(StringPiece input, const Delim& delim, OutputTypes&... outputs) {
return detail::trySplitTo(input, detail::prepareDelim(delim), outputs...);
}

template <bool exact, class Delim, class... OutputTypes>
typename std::enable_if<
StrictConjunction<IsConvertible<OutputTypes>...>::value &&
Expand Down
5 changes: 5 additions & 0 deletions folly/String.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,11 @@ std::string stripLeftMargin(std::string s) {
return join("\n", piecer);
}

bool SubstringConversionCode::operator==(
const SubstringConversionCode& other) const {
return this->code == other.code && this->substring == other.substring;
}

} // namespace folly

#ifdef FOLLY_DEFINED_DMGL
Expand Down
37 changes: 37 additions & 0 deletions folly/String.h
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,43 @@ typename std::enable_if<
bool>::type
split(const Delim& delimiter, StringPiece input, OutputTypes&... outputs);

// Error type for trySplitTo(), below.
struct SubstringConversionCode {
StringPiece substring;
ConversionCode code;
bool operator==(const SubstringConversionCode& other) const;
};

/**
* Try to split a string into a fixed number of fields by delimiter, using
* folly::tryTo<> for conversions. types by delimiter.
* - On success, all output values will be initialized and the 'Unit{}' value is
* returned. Arguments are assigned in reverse order.
* - On failure, the first failing 'ConversionCode' is returned with its
* associated substring in a 'SubstringConversionCode'.
* - String splitting is performed prior to each conversion; field values will
* not contain the delimiter.
* - All custom error codes are mapped to ConversionCode::CUSTOM.
*
* Examples:
*
* folly::StringPiece name, key, value;
* if (folly::trySplitTo(line, '\t', name, key, value))
* ...
*
* folly::StringPiece name;
* double value;
* int id;
* if (folly::trySplitTo(line, '\t', name, value, id))
* ...
*
*/
template <class Delim, class... OutputTypes>
typename std::enable_if<
StrictConjunction<IsConvertible<OutputTypes>...>::value,
Expected<Unit, SubstringConversionCode>>::type
trySplitTo(StringPiece input, const Delim& delimiter, OutputTypes&... outputs);

/**
* Join list of tokens.
*
Expand Down
96 changes: 94 additions & 2 deletions folly/test/StringTest.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1016,9 +1016,9 @@ ColorError makeConversionError(ColorErrorCode, StringPiece sp) {

Expected<StringPiece, ColorErrorCode> parseTo(
StringPiece in, Color& out) noexcept {
if (in == "R") {
if (in.startsWith('R')) {
out = Color::Red;
} else if (in == "B") {
} else if (in.startsWith('B')) {
out = Color::Blue;
} else {
return makeUnexpected(ColorErrorCode::INVALID_COLOR);
Expand All @@ -1037,6 +1037,98 @@ TEST(Split, fixedConvertCustom) {
EXPECT_THROW(folly::split(',', "B,G", c1, c2), my::ColorError);
}

namespace folly {

void PrintTo(SubstringConversionCode x, std::ostream* os) {
*os << makeConversionError(x.code, x.substring).what();
}

} // namespace folly

TEST(Split, trySplitTo) {
StringPiece sp1 = "start", sp2 = "start";
int i32 = -111;
double f64 = -222;

// Failure cases
EXPECT_EQ(
(SubstringConversionCode{"z.x", ConversionCode::STRING_TO_FLOAT_ERROR}),
folly::trySplitTo("b:z.x", ':', sp1, f64).error());
EXPECT_EQ(
(SubstringConversionCode{
"14.x", ConversionCode::NON_WHITESPACE_AFTER_END}),
folly::trySplitTo("b:14.x", ':', sp2, f64).error());
EXPECT_EQ( // Earliest errors are returned first
(SubstringConversionCode{
"thirteen", ConversionCode::INVALID_LEADING_CHAR}),
folly::trySplitTo("a:thirteen:b:14.7x", ':', sp1, i32, sp2, f64).error());
EXPECT_EQ(
(SubstringConversionCode{"14.7:extra", ConversionCode::SPLIT_ERROR}),
folly::trySplitTo("a:13:b:14.7:extra", ':', sp1, i32, sp2, f64).error());

// Original arguments untouched on all failures.
EXPECT_EQ(sp1, "start");
EXPECT_EQ(i32, -111);
EXPECT_EQ(f64, -222);
EXPECT_EQ(sp2, "start");

// Simple success case.
EXPECT_TRUE(folly::trySplitTo("a:12:b:13.4", ':', sp1, i32, sp2, f64));
EXPECT_EQ(sp1, "a");
EXPECT_EQ(i32, 12);
EXPECT_EQ(sp2, "b");
EXPECT_EQ(f64, 13.4);

// Verifies that a line contains exactly one field.
EXPECT_EQ(folly::unit, folly::trySplitTo("hello", ' ', sp1).value());

// Field count must exactly match, even if the whole input could be
// represented by the output type.
EXPECT_EQ(
(SubstringConversionCode{"hello world", ConversionCode::SPLIT_ERROR}),
folly::trySplitTo("hello world", ' ', sp1).error());

// Trailing delimiters are not stripped, this is just an empty last field.
EXPECT_EQ(
(SubstringConversionCode{"hello ", ConversionCode::SPLIT_ERROR}),
folly::trySplitTo("hello ", ' ', sp1).error());
EXPECT_TRUE(folly::trySplitTo("hello ", ' ', sp1, sp2));
EXPECT_EQ(sp1, "hello");
EXPECT_EQ(sp2, "");

// Likewise for leading delimiters.
EXPECT_EQ(
(SubstringConversionCode{" hello", ConversionCode::SPLIT_ERROR}),
folly::trySplitTo(" hello", ' ', sp1).error());
EXPECT_TRUE(folly::trySplitTo(" hello", ' ', sp1, sp2));
EXPECT_EQ(sp1, "");
EXPECT_EQ(sp2, "hello");

// Assignment order is reversed.
EXPECT_TRUE(folly::trySplitTo("1:2:3:4", ':', i32, f64, f64, i32));
EXPECT_EQ(i32, 1);
EXPECT_EQ(f64, 2);

// std::ignore is supported.
EXPECT_TRUE(folly::trySplitTo(
"a:15:ab:cd:ef:16.7", ':', std::ignore, i32, sp1, sp2, std::ignore, f64));
EXPECT_EQ(i32, 15);
EXPECT_EQ(sp1, "ab");
EXPECT_EQ(sp2, "cd");
EXPECT_EQ(f64, 16.7);

// Customizations via 'parseTo()' are supported.
my::Color c1, c2;
EXPECT_EQ(
(SubstringConversionCode{"New", ConversionCode::CUSTOM}),
folly::trySplitTo(
"Black:Blue:Old:New", ':', std::ignore, c1, std::ignore, c2)
.error());
EXPECT_TRUE(folly::trySplitTo("1:Red:2:Blue", ':', i32, c1, i32, c2));
EXPECT_EQ(c1, my::Color::Red);
EXPECT_EQ(c2, my::Color::Blue);
}

TEST(String, join) {
string output;

Expand Down