diff --git a/AK/String.cpp b/AK/String.cpp index 790efc2a21b..e9ab733f01f 100644 --- a/AK/String.cpp +++ b/AK/String.cpp @@ -389,6 +389,11 @@ String String::to_uppercase() const return m_impl->to_uppercase(); } +String String::to_snakecase() const +{ + return StringUtils::to_snakecase(*this); +} + bool operator<(const char* characters, const String& string) { if (!characters) diff --git a/AK/String.h b/AK/String.h index 877cc97b711..737b7118f95 100644 --- a/AK/String.h +++ b/AK/String.h @@ -131,6 +131,7 @@ public: String to_lowercase() const; String to_uppercase() const; + String to_snakecase() const; bool is_whitespace() const { return StringUtils::is_whitespace(*this); } diff --git a/AK/StringUtils.cpp b/AK/StringUtils.cpp index 7e65caab857..90171e308ba 100644 --- a/AK/StringUtils.cpp +++ b/AK/StringUtils.cpp @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -350,6 +351,33 @@ Optional find(const StringView& haystack, const StringView& needle) haystack.characters_without_null_termination(), haystack.length(), needle.characters_without_null_termination(), needle.length()); } + +String to_snakecase(const StringView& str) +{ + auto should_insert_underscore = [&](auto i, auto current_char) { + if (i == 0) + return false; + auto previous_ch = str[i - 1]; + if (islower(previous_ch) && isupper(current_char)) + return true; + if (i >= str.length() - 1) + return false; + auto next_ch = str[i + 1]; + if (isupper(current_char) && islower(next_ch)) + return true; + return false; + }; + + StringBuilder builder; + for (size_t i = 0; i < str.length(); ++i) { + auto ch = str[i]; + if (should_insert_underscore(i, ch)) + builder.append('_'); + builder.append(tolower(ch)); + } + return builder.to_string(); +} + } } diff --git a/AK/StringUtils.h b/AK/StringUtils.h index 1f830caa6ed..d3bcaf39ded 100644 --- a/AK/StringUtils.h +++ b/AK/StringUtils.h @@ -72,6 +72,8 @@ bool contains(const StringView&, const StringView&, CaseSensitivity); bool is_whitespace(const StringView&); StringView trim_whitespace(const StringView&, TrimMode mode); Optional find(const StringView& haystack, const StringView& needle); +String to_snakecase(const StringView&); + } } diff --git a/AK/Tests/TestStringUtils.cpp b/AK/Tests/TestStringUtils.cpp index d1edda37edf..f5760f4b0f0 100644 --- a/AK/Tests/TestStringUtils.cpp +++ b/AK/Tests/TestStringUtils.cpp @@ -310,4 +310,18 @@ TEST_CASE(find) EXPECT_EQ(AK::StringUtils::find(test_string, "78").has_value(), false); } +TEST_CASE(to_snakecase) +{ + EXPECT_EQ(AK::StringUtils::to_snakecase("foobar"), "foobar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("Foobar"), "foobar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("FOOBAR"), "foobar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("fooBar"), "foo_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("FooBar"), "foo_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("fooBAR"), "foo_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("FOOBar"), "foo_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("foo_bar"), "foo_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("FBar"), "f_bar"); + EXPECT_EQ(AK::StringUtils::to_snakecase("FooB"), "foo_b"); +} + TEST_MAIN(StringUtils)