From 25e7225782d133e463ca26892bfe732720ccf9f7 Mon Sep 17 00:00:00 2001 From: Tom Date: Tue, 20 Oct 2020 15:07:03 -0600 Subject: [PATCH] AK: Enhance String::contains to allow case-insensitive searches --- AK/String.cpp | 6 ++---- AK/String.h | 2 +- AK/StringUtils.cpp | 28 ++++++++++++++++++++++++++++ AK/StringUtils.h | 1 + AK/StringView.cpp | 4 ++-- AK/StringView.h | 2 +- AK/Tests/TestStringUtils.cpp | 19 +++++++++++++++++++ 7 files changed, 54 insertions(+), 8 deletions(-) diff --git a/AK/String.cpp b/AK/String.cpp index 19309e0f4b..3782ad3f58 100644 --- a/AK/String.cpp +++ b/AK/String.cpp @@ -275,11 +275,9 @@ bool String::matches(const StringView& mask, CaseSensitivity case_sensitivity) c return StringUtils::matches(*this, mask, case_sensitivity); } -bool String::contains(const String& needle) const +bool String::contains(const StringView& needle, CaseSensitivity case_sensitivity) const { - if (is_null() || needle.is_null()) - return false; - return strstr(characters(), needle.characters()); + return StringUtils::contains(*this, needle, case_sensitivity); } Optional String::index_of(const String& needle, size_t start) const diff --git a/AK/String.h b/AK/String.h index 8a124a5223..434584fe73 100644 --- a/AK/String.h +++ b/AK/String.h @@ -128,7 +128,7 @@ public: bool equals_ignoring_case(const StringView&) const; - bool contains(const String&) const; + bool contains(const StringView&, CaseSensitivity = CaseSensitivity::CaseSensitive) const; Optional index_of(const String&, size_t start = 0) const; Vector split_limit(char separator, size_t limit, bool keep_empty = false) const; diff --git a/AK/StringUtils.cpp b/AK/StringUtils.cpp index c4af681388..858e1642fb 100644 --- a/AK/StringUtils.cpp +++ b/AK/StringUtils.cpp @@ -227,6 +227,34 @@ bool starts_with(const StringView& str, const StringView& start, CaseSensitivity return true; } +bool contains(const StringView& str, const StringView& needle, CaseSensitivity case_sensitivity) +{ + if (str.is_null() || needle.is_null() || str.is_empty() || needle.length() > str.length()) + return false; + if (needle.is_empty()) + return true; + auto str_chars = str.characters_without_null_termination(); + auto needle_chars = needle.characters_without_null_termination(); + if (case_sensitivity == CaseSensitivity::CaseSensitive) + return memmem(str_chars, str.length(), needle_chars, needle.length()) != nullptr; + + auto needle_first = to_lowercase(needle_chars[0]); + size_t slen = str.length() - needle.length(); + for (size_t si = 0; si < slen; si++) { + if (to_lowercase(str_chars[si]) != needle_first) + continue; + size_t ni = 1; + while (ni < needle.length()) { + if (to_lowercase(str_chars[si + ni]) != to_lowercase(needle_chars[ni])) + break; + ni++; + } + if (ni == needle.length()) + return true; + } + return false; +} + StringView trim_whitespace(const StringView& str, TrimMode mode) { auto is_whitespace_character = [](char ch) -> bool { diff --git a/AK/StringUtils.h b/AK/StringUtils.h index e30345e3b0..7c532175c3 100644 --- a/AK/StringUtils.h +++ b/AK/StringUtils.h @@ -51,6 +51,7 @@ Optional convert_to_uint_from_hex(const StringView&); bool equals_ignoring_case(const StringView&, const StringView&); bool ends_with(const StringView& a, const StringView& b, CaseSensitivity); bool starts_with(const StringView&, const StringView&, CaseSensitivity); +bool contains(const StringView&, const StringView&, CaseSensitivity); StringView trim_whitespace(const StringView&, TrimMode mode); } diff --git a/AK/StringView.cpp b/AK/StringView.cpp index 57f476d0da..ba8f82218a 100644 --- a/AK/StringView.cpp +++ b/AK/StringView.cpp @@ -178,9 +178,9 @@ bool StringView::contains(char needle) const return false; } -bool StringView::contains(const StringView& needle) const +bool StringView::contains(const StringView& needle, CaseSensitivity case_sensitivity) const { - return memmem(m_characters, m_length, needle.m_characters, needle.m_length) != nullptr; + return StringUtils::contains(*this, needle, case_sensitivity); } bool StringView::equals_ignoring_case(const StringView& other) const diff --git a/AK/StringView.h b/AK/StringView.h index 6a0293561f..5cdf7285f7 100644 --- a/AK/StringView.h +++ b/AK/StringView.h @@ -88,7 +88,7 @@ public: bool ends_with(char) const; bool matches(const StringView& mask, CaseSensitivity = CaseSensitivity::CaseInsensitive) const; bool contains(char) const; - bool contains(const StringView&) const; + bool contains(const StringView&, CaseSensitivity = CaseSensitivity::CaseSensitive) const; bool equals_ignoring_case(const StringView& other) const; StringView trim_whitespace(TrimMode mode = TrimMode::Both) const { return StringUtils::trim_whitespace(*this, mode); } diff --git a/AK/Tests/TestStringUtils.cpp b/AK/Tests/TestStringUtils.cpp index a34c91c253..c726100133 100644 --- a/AK/Tests/TestStringUtils.cpp +++ b/AK/Tests/TestStringUtils.cpp @@ -175,4 +175,23 @@ TEST_CASE(starts_with) EXPECT(!AK::StringUtils::starts_with(test_string, "abc", CaseSensitivity::CaseSensitive)); } +TEST_CASE(contains) +{ + String test_string = "ABCDEFABCXYZ"; + EXPECT(AK::StringUtils::contains(test_string, "ABC", CaseSensitivity::CaseSensitive)); + EXPECT(AK::StringUtils::contains(test_string, "ABC", CaseSensitivity::CaseInsensitive)); + EXPECT(AK::StringUtils::contains(test_string, "AbC", CaseSensitivity::CaseInsensitive)); + EXPECT(AK::StringUtils::contains(test_string, "BCX", CaseSensitivity::CaseSensitive)); + EXPECT(AK::StringUtils::contains(test_string, "BCX", CaseSensitivity::CaseInsensitive)); + EXPECT(AK::StringUtils::contains(test_string, "BcX", CaseSensitivity::CaseInsensitive)); + EXPECT(!AK::StringUtils::contains(test_string, "EFG", CaseSensitivity::CaseSensitive)); + EXPECT(!AK::StringUtils::contains(test_string, "EfG", CaseSensitivity::CaseInsensitive)); + EXPECT(AK::StringUtils::contains(test_string, "", CaseSensitivity::CaseSensitive)); + EXPECT(AK::StringUtils::contains(test_string, "", CaseSensitivity::CaseInsensitive)); + EXPECT(!AK::StringUtils::contains("", test_string, CaseSensitivity::CaseSensitive)); + EXPECT(!AK::StringUtils::contains("", test_string, CaseSensitivity::CaseInsensitive)); + EXPECT(!AK::StringUtils::contains(test_string, "L", CaseSensitivity::CaseSensitive)); + EXPECT(!AK::StringUtils::contains(test_string, "L", CaseSensitivity::CaseInsensitive)); +} + TEST_MAIN(StringUtils)