Skip to content

Commit 4027601

Browse files
authored
refactor(utils): consolidate string and option handling (#249)
1 parent b2e5fba commit 4027601

30 files changed

Lines changed: 313 additions & 335 deletions

src/paimon/common/data/variant/variant_access_utils.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include "fmt/format.h"
2727
#include "paimon/common/data/variant/variant_defs.h"
2828
#include "paimon/common/types/data_field.h"
29+
#include "paimon/common/utils/string_utils.h"
2930

3031
namespace paimon {
3132

@@ -61,7 +62,7 @@ std::vector<std::string> SplitDescription(const std::string& description) {
6162
}
6263

6364
bool HasAccessDescription(const std::shared_ptr<arrow::Field>& field) {
64-
return GetDescription(field).rfind(VariantAccessUtils::kMetadataKey, 0) == 0;
65+
return StringUtils::StartsWith(GetDescription(field), VariantAccessUtils::kMetadataKey);
6566
}
6667

6768
} // namespace

src/paimon/common/types/data_type_json_parser.cpp

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919

2020
#include "paimon/common/types/data_type_json_parser.h"
2121

22-
#include <algorithm>
2322
#include <cctype>
2423
#include <cstddef>
2524
#include <cstdint>
@@ -331,10 +330,7 @@ std::vector<Token> Tokenize(const std::string& chars) {
331330
builder.clear();
332331
cursor = ConsumeIdentifier(chars, cursor, builder);
333332
auto token = builder.str();
334-
auto normalized_token = token;
335-
std::transform(normalized_token.begin(), normalized_token.end(),
336-
normalized_token.begin(),
337-
[](unsigned char c) { return std::toupper(c); });
333+
std::string normalized_token = StringUtils::ToUpperCase(token);
338334
if (Keywords().find(normalized_token) != Keywords().end()) {
339335
tokens.emplace_back(TokenType::KEYWORD, cursor, normalized_token);
340336
} else {

src/paimon/common/utils/options_utils.h

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,29 @@ class OptionsUtils {
8989
return value.status();
9090
}
9191

92+
static Result<std::string> GetNonEmptyValueFromMap(
93+
const std::map<std::string, std::string>& key_value_map, const std::string& key) {
94+
Result<std::string> value = GetValueFromMap<std::string>(key_value_map, key);
95+
if (!value.ok()) {
96+
return value.status();
97+
}
98+
if (value.value().empty()) {
99+
return Status::Invalid(fmt::format("value for key {} must not be empty", key));
100+
}
101+
return value.value();
102+
}
103+
92104
/// Fetch options with specific prefix and remove prefix for key.
105+
/// @param prefix Prefix used to select options and removed from the returned keys.
106+
/// @param options Options to select from.
107+
/// @return Options whose keys start with and are longer than `prefix`, with the prefix removed
108+
/// from each key.
93109
static std::map<std::string, std::string> FetchOptionsWithPrefix(
94110
const std::string& prefix, const std::map<std::string, std::string>& options) {
95111
std::map<std::string, std::string> options_with_prefix;
96-
int64_t prefix_len = prefix.size();
112+
const std::string::size_type prefix_len = prefix.size();
97113
for (const auto& [key, value] : options) {
98-
if (StringUtils::StartsWith(key, prefix)) {
114+
if (key.size() > prefix_len && StringUtils::StartsWith(key, prefix)) {
99115
options_with_prefix[key.substr(prefix_len)] = value;
100116
}
101117
}

src/paimon/common/utils/options_utils_test.cpp

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,19 @@ TEST(OptionsUtilsTest, TestGetOptionalValueFromMap) {
8484
}
8585

8686
TEST(OptionsUtilsTest, TestFetchOptionsWithPrefix) {
87-
std::map<std::string, std::string> options = {{"key1", "value1"}, {"test.key2", "value2"}};
87+
std::map<std::string, std::string> options = {
88+
{"key1", "value1"}, {"test.", "empty-key"}, {"test.key2", "value2"}};
8889
auto new_options = OptionsUtils::FetchOptionsWithPrefix("test.", options);
8990
std::map<std::string, std::string> expected = {{"key2", "value2"}};
9091
ASSERT_EQ(expected, new_options);
9192
}
93+
94+
TEST(OptionsUtilsTest, TestGetNonEmptyValueFromMap) {
95+
std::map<std::string, std::string> options = {{"present", "value"}, {"empty", ""}};
96+
ASSERT_OK_AND_ASSIGN(std::string value,
97+
OptionsUtils::GetNonEmptyValueFromMap(options, "present"));
98+
ASSERT_EQ("value", value);
99+
ASSERT_TRUE(OptionsUtils::GetNonEmptyValueFromMap(options, "missing").status().IsNotExist());
100+
ASSERT_TRUE(OptionsUtils::GetNonEmptyValueFromMap(options, "empty").status().IsInvalid());
101+
}
92102
} // namespace paimon::test

src/paimon/common/utils/string_utils.cpp

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,28 @@
3030
#include "paimon/status.h"
3131

3232
namespace paimon {
33+
namespace {
34+
35+
bool IsTrimCharacter(unsigned char c) {
36+
// Match the characters removed by Java String::trim for the ASCII strings handled here.
37+
return c <= 0x20;
38+
}
39+
40+
char ToAsciiLower(unsigned char c) {
41+
return c >= 'A' && c <= 'Z' ? static_cast<char>(c + ('a' - 'A')) : static_cast<char>(c);
42+
}
43+
44+
char ToAsciiUpper(unsigned char c) {
45+
return c >= 'a' && c <= 'z' ? static_cast<char>(c - ('a' - 'A')) : static_cast<char>(c);
46+
}
47+
48+
} // namespace
49+
3350
std::string StringUtils::Replace(const std::string& text, const std::string& search_string,
3451
const std::string& replacement, int32_t max) {
52+
if (text.empty() || search_string.empty() || max == 0) {
53+
return text;
54+
}
3555
std::string str = text;
3656
size_t pos = str.find(search_string);
3757
int32_t count = 0;
@@ -45,6 +65,9 @@ std::string StringUtils::Replace(const std::string& text, const std::string& sea
4565

4666
std::string StringUtils::ReplaceLast(const std::string& text, const std::string& old_str,
4767
const std::string& new_str) {
68+
if (text.empty() || old_str.empty()) {
69+
return text;
70+
}
4871
std::string str = text;
4972
size_t pos = str.rfind(old_str);
5073
if (pos != std::string::npos) {
@@ -54,7 +77,8 @@ std::string StringUtils::ReplaceLast(const std::string& text, const std::string&
5477
}
5578

5679
bool StringUtils::StartsWith(const std::string& str, const std::string& prefix, size_t start_pos) {
57-
return (str.size() >= prefix.size()) && (str.compare(start_pos, prefix.size(), prefix) == 0);
80+
return start_pos <= str.size() && prefix.size() <= str.size() - start_pos &&
81+
str.compare(start_pos, prefix.size(), prefix) == 0;
5882
}
5983
bool StringUtils::EndsWith(const std::string& str, const std::string& suffix) {
6084
size_t s1 = str.size();
@@ -74,26 +98,45 @@ bool StringUtils::IsNullOrWhitespaceOnly(const std::string& str) {
7498
}
7599

76100
void StringUtils::Trim(std::string* str) {
77-
str->erase(str->find_last_not_of(' ') + 1);
78-
str->erase(0, str->find_first_not_of(' '));
101+
auto first = std::find_if_not(str->begin(), str->end(),
102+
[](unsigned char c) { return IsTrimCharacter(c); });
103+
auto last = std::find_if_not(str->rbegin(), str->rend(), [](unsigned char c) {
104+
return IsTrimCharacter(c);
105+
}).base();
106+
if (first >= last) {
107+
str->clear();
108+
return;
109+
}
110+
*str = std::string(first, last);
79111
}
80112

81113
std::string StringUtils::ToLowerCase(const std::string& str) {
82114
std::string result;
83115
result.reserve(str.length());
84-
std::transform(str.begin(), str.end(), std::back_inserter(result),
85-
[](unsigned char c) { return std::tolower(c); });
116+
std::transform(str.begin(), str.end(), std::back_inserter(result), ToAsciiLower);
86117
return result;
87118
}
88119

89120
std::string StringUtils::ToUpperCase(const std::string& str) {
90121
std::string result;
91122
result.reserve(str.length());
92-
std::transform(str.begin(), str.end(), std::back_inserter(result),
93-
[](unsigned char c) { return std::toupper(c); });
123+
std::transform(str.begin(), str.end(), std::back_inserter(result), ToAsciiUpper);
94124
return result;
95125
}
96126

127+
bool StringUtils::EqualsIgnoreCase(const std::string& left, const std::string& right) {
128+
if (left.size() != right.size()) {
129+
return false;
130+
}
131+
for (size_t i = 0; i < left.size(); ++i) {
132+
if (ToAsciiLower(static_cast<unsigned char>(left[i])) !=
133+
ToAsciiLower(static_cast<unsigned char>(right[i]))) {
134+
return false;
135+
}
136+
}
137+
return true;
138+
}
139+
97140
std::vector<std::string> StringUtils::Split(const std::string& text, const std::string& sep_str,
98141
bool ignore_empty) {
99142
std::vector<std::string> vec;

src/paimon/common/utils/string_utils.h

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -50,24 +50,18 @@ class PAIMON_EXPORT StringUtils {
5050
public:
5151
/// Replaces all occurrences of a string within another string.
5252
///
53-
/// A `null` reference passed to this method is a no-op.
54-
///
5553
/// <pre>
56-
/// StringUtils::Replace(null, *, *) = null
5754
/// StringUtils::Replace("", *, *) = ""
58-
/// StringUtils::Replace("any", null, *) = "any"
59-
/// StringUtils::Replace("any", *, null) = "any"
6055
/// StringUtils::Replace("any", "", *) = "any"
61-
/// StringUtils::Replace("aba", "a", null) = "aba"
6256
/// StringUtils::Replace("aba", "a", "") = "b"
6357
/// StringUtils::Replace("aba", "a", "z") = "zbz"
6458
/// </pre>
6559
///
6660
/// @see #replace(string text, string search_string, string replacement, int max)
67-
/// @param text text to search and replace in, may be null
68-
/// @param search_string the String to search for, may be null
69-
/// @param replacement the String to replace it with, may be null
70-
/// @return the text with any replacements processed, `null` if null string input
61+
/// @param text text to search and replace in
62+
/// @param search_string the String to search for
63+
/// @param replacement the String to replace it with
64+
/// @return the text with any replacements processed
7165
static std::string Replace(const std::string& text, const std::string& search_string,
7266
const std::string& replacement) {
7367
return Replace(text, search_string, replacement, -1);
@@ -76,28 +70,22 @@ class PAIMON_EXPORT StringUtils {
7670
/// Replaces a String with another String inside a larger String, for the first `max` values of
7771
/// the search String.
7872
///
79-
/// A `null` reference passed to this method is a no-op.
80-
///
8173
/// <pre>
82-
/// StringUtils::Replace(null, *, *, *) = null
8374
/// StringUtils::Replace("", *, *, *) = ""
84-
/// StringUtils::Replace("any", null, *, *) = "any"
85-
/// StringUtils::Replace("any", *, null, *) = "any"
8675
/// StringUtils::Replace("any", "", *, *) = "any"
8776
/// StringUtils::Replace("any", *, *, 0) = "any"
88-
/// StringUtils::Replace("abaa", "a", null, -1) = "abaa"
8977
/// StringUtils::Replace("abaa", "a", "", -1) = "b"
9078
/// StringUtils::Replace("abaa", "a", "z", 0) = "abaa"
9179
/// StringUtils::Replace("abaa", "a", "z", 1) = "zbaa"
9280
/// StringUtils::Replace("abaa", "a", "z", 2) = "zbza"
9381
/// StringUtils::Replace("abaa", "a", "z", -1) = "zbzz"
9482
/// </pre>
9583
///
96-
/// @param text text to search and replace in, may be null
97-
/// @param search_string the String to search for, may be null
98-
/// @param replacement the String to replace it with, may be null
84+
/// @param text text to search and replace in
85+
/// @param search_string the String to search for
86+
/// @param replacement the String to replace it with
9987
/// @param max maximum number of values to replace, or `-1` if no maximum
100-
/// @return the text with any replacements processed, `null` if null string input
88+
/// @return the text with any replacements processed
10189
static std::string Replace(const std::string& text, const std::string& search_string,
10290
const std::string& replacement, int32_t max);
10391

@@ -115,6 +103,9 @@ class PAIMON_EXPORT StringUtils {
115103
static std::string ToLowerCase(const std::string& str);
116104
static std::string ToUpperCase(const std::string& str);
117105

106+
/// Compares two strings using ASCII case folding.
107+
static bool EqualsIgnoreCase(const std::string& left, const std::string& right);
108+
118109
template <typename T>
119110
static std::string VectorToString(const std::vector<T>& vec) {
120111
std::vector<std::string> strs;

src/paimon/common/utils/string_utils_test.cpp

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ void StringUtilsTest::CheckOverFlowAndUnderFlow(const std::string& over_flow,
7373
}
7474

7575
TEST_F(StringUtilsTest, TestReplaceAll) {
76+
ASSERT_EQ("abc", StringUtils::Replace("abc", "", "x"));
77+
ASSERT_EQ("", StringUtils::Replace("", "a", "b"));
7678
{
7779
std::string origin = "how is is you";
7880
std::string expect = "how are are you";
@@ -118,6 +120,8 @@ TEST_F(StringUtilsTest, TestReplaceAll) {
118120
}
119121

120122
TEST_F(StringUtilsTest, TestReplaceLast) {
123+
ASSERT_EQ("abc", StringUtils::ReplaceLast("abc", "", "x"));
124+
ASSERT_EQ("", StringUtils::ReplaceLast("", "a", "b"));
121125
{
122126
std::string origin = "a/b/c//";
123127
std::string expect = "a/b/c/_";
@@ -140,6 +144,7 @@ TEST_F(StringUtilsTest, TestReplaceLast) {
140144
}
141145

142146
TEST_F(StringUtilsTest, TestReplaceWithMaxCount) {
147+
ASSERT_EQ("abc", StringUtils::Replace("abc", "a", "b", 0));
143148
{
144149
std::string origin = "how is is you";
145150
std::string expect = "how are is you";
@@ -236,6 +241,13 @@ TEST_F(StringUtilsTest, TestToUpperCase) {
236241
}
237242
}
238243

244+
TEST_F(StringUtilsTest, TestEqualsIgnoreCase) {
245+
ASSERT_TRUE(StringUtils::EqualsIgnoreCase("", ""));
246+
ASSERT_TRUE(StringUtils::EqualsIgnoreCase("AbC-123", "aBc-123"));
247+
ASSERT_FALSE(StringUtils::EqualsIgnoreCase("abc", "abcd"));
248+
ASSERT_FALSE(StringUtils::EqualsIgnoreCase("abc", "abx"));
249+
}
250+
239251
TEST_F(StringUtilsTest, TestStartsWith) {
240252
{
241253
std::string str = "abcde";
@@ -261,6 +273,26 @@ TEST_F(StringUtilsTest, TestStartsWith) {
261273
std::string str = "";
262274
ASSERT_TRUE(StringUtils::StartsWith(str, ""));
263275
}
276+
{
277+
std::string str = "abc";
278+
ASSERT_TRUE(StringUtils::StartsWith(str, "", /*start_pos=*/3));
279+
ASSERT_FALSE(StringUtils::StartsWith(str, "", /*start_pos=*/4));
280+
ASSERT_FALSE(StringUtils::StartsWith(str, "a", /*start_pos=*/4));
281+
}
282+
}
283+
284+
TEST_F(StringUtilsTest, TestTrim) {
285+
std::string value = " \tabc\r\n";
286+
StringUtils::Trim(&value);
287+
ASSERT_EQ("abc", value);
288+
289+
value = "\t\r\n";
290+
StringUtils::Trim(&value);
291+
ASSERT_TRUE(value.empty());
292+
293+
value.clear();
294+
StringUtils::Trim(&value);
295+
ASSERT_TRUE(value.empty());
264296
}
265297
TEST_F(StringUtilsTest, TestEndsWith) {
266298
{

0 commit comments

Comments
 (0)