diff --git a/AK/String.h b/AK/String.h index 94920fe23c..b4fe68f6d7 100644 --- a/AK/String.h +++ b/AK/String.h @@ -132,6 +132,9 @@ public: Optional find_byte_offset(u32 code_point, size_t from_byte_offset = 0) const; Optional find_byte_offset(StringView substring, size_t from_byte_offset = 0) const; + // Using this method requires linking LibUnicode into your application. + Optional find_byte_offset_ignoring_case(StringView, size_t from_byte_offset = 0) const; + [[nodiscard]] bool operator==(String const&) const = default; [[nodiscard]] bool operator==(FlyString const&) const; [[nodiscard]] bool operator==(StringView) const; diff --git a/Tests/AK/TestString.cpp b/Tests/AK/TestString.cpp index d5253bb40e..bbc0942893 100644 --- a/Tests/AK/TestString.cpp +++ b/Tests/AK/TestString.cpp @@ -920,6 +920,133 @@ TEST_CASE(find_byte_offset) } } +TEST_CASE(find_byte_offset_ignoring_case) +{ + { + auto string = ""_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case(""sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("1"sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("2"sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("23"sv).has_value(), false); + } + { + auto string = "1234567"_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case(""sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("1"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("2"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("3"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("4"sv), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("5"sv), 4u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("6"sv), 5u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("7"sv), 6u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("34"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("45"sv), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("56"sv), 4u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("67"sv), 5u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("a"sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("8"sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("78"sv).has_value(), false); + EXPECT_EQ(string.find_byte_offset_ignoring_case("46"sv).has_value(), false); + } + { + auto string = "abCDef"_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case("A"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("B"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("c"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("d"sv), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("e"sv), 4u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("f"sv), 5u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("AbC"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("BcdE"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("cd"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("cD"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("E"sv), 4u); + } + { + auto string = "abßcd"_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case("SS"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("Ss"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ss"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ß"sv), 2u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSS"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSs"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bss"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bS"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bs"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bß"sv), 1u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSSc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSsc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bssc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bßc"sv), 1u); + EXPECT(!string.find_byte_offset_ignoring_case("bSc"sv).has_value()); + EXPECT(!string.find_byte_offset_ignoring_case("bsc"sv).has_value()); + } + { + auto string = "abSScd"_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case("SS"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("Ss"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ss"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ß"sv), 2u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSS"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSs"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bss"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bS"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bs"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bß"sv), 1u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSSc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bSsc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bssc"sv), 1u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("bßc"sv), 1u); + EXPECT(!string.find_byte_offset_ignoring_case("bSc"sv).has_value()); + EXPECT(!string.find_byte_offset_ignoring_case("bsc"sv).has_value()); + } + { + auto string = "ßSßs"_string; + + EXPECT_EQ(string.find_byte_offset_ignoring_case("SS"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("Ss"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ss"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv), 0u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ß"sv), 0u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("SS"sv, 2), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("Ss"sv, 2), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ss"sv, 2), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv, 2), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv, 2), 2u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ß"sv, 2), 2u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("SS"sv, 3), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("Ss"sv, 3), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ss"sv, 3), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv, 3), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv, 3), 3u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("ß"sv, 3), 3u); + + EXPECT_EQ(string.find_byte_offset_ignoring_case("S"sv, 5), 5u); + EXPECT_EQ(string.find_byte_offset_ignoring_case("s"sv, 5), 5u); + EXPECT(!string.find_byte_offset_ignoring_case("SS"sv, 5).has_value()); + EXPECT(!string.find_byte_offset_ignoring_case("Ss"sv, 5).has_value()); + EXPECT(!string.find_byte_offset_ignoring_case("ss"sv, 5).has_value()); + EXPECT(!string.find_byte_offset_ignoring_case("ß"sv, 5).has_value()); + } +} + TEST_CASE(repeated) { { diff --git a/Userland/Libraries/LibUnicode/CharacterTypes.cpp b/Userland/Libraries/LibUnicode/CharacterTypes.cpp index f68b2affd3..b6a5d81f0f 100644 --- a/Userland/Libraries/LibUnicode/CharacterTypes.cpp +++ b/Userland/Libraries/LibUnicode/CharacterTypes.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -55,6 +56,18 @@ public: return !m_casefolded_code_points.is_empty() || (m_it != m_string.end()); } + size_t index() const + { + if constexpr (IsSame) + return m_string.byte_offset_of(m_it); + else if constexpr (IsSame) + return m_string.code_unit_offset_of(m_it); + else if constexpr (IsSame) + return m_string.iterator_offset(m_it); + else + static_assert(DependentFalse); + } + u32 next_code_point() { VERIFY(has_more_data()); @@ -103,6 +116,38 @@ template bool equals_ignoring_case(Utf8View, Utf8View); template bool equals_ignoring_case(Utf16View, Utf16View); template bool equals_ignoring_case(Utf32View, Utf32View); +template +Optional find_ignoring_case(ViewType lhs, ViewType rhs) +{ + CasefoldStringComparator lhs_comparator { lhs }; + + while (lhs_comparator.has_more_data()) { + CasefoldStringComparator rhs_comparator { rhs }; + + auto saved_state = lhs_comparator; + auto matches = true; + + while (lhs_comparator.has_more_data() && rhs_comparator.has_more_data()) { + if (lhs_comparator.next_code_point() != rhs_comparator.next_code_point()) { + matches = false; + break; + } + } + + if (matches && !rhs_comparator.has_more_data()) + return saved_state.index(); + + lhs_comparator = move(saved_state); + lhs_comparator.next_code_point(); + } + + return {}; +} + +template Optional find_ignoring_case(Utf8View, Utf8View); +template Optional find_ignoring_case(Utf16View, Utf16View); +template Optional find_ignoring_case(Utf32View, Utf32View); + Optional __attribute__((weak)) general_category_from_string(StringView) { return {}; } bool __attribute__((weak)) code_point_has_general_category(u32, GeneralCategory) { return {}; } Optional __attribute__((weak)) property_from_string(StringView) { return {}; } diff --git a/Userland/Libraries/LibUnicode/CharacterTypes.h b/Userland/Libraries/LibUnicode/CharacterTypes.h index e0060f8c86..53a85209ef 100644 --- a/Userland/Libraries/LibUnicode/CharacterTypes.h +++ b/Userland/Libraries/LibUnicode/CharacterTypes.h @@ -51,6 +51,9 @@ u32 to_unicode_titlecase(u32 code_point); template bool equals_ignoring_case(ViewType, ViewType); +template +Optional find_ignoring_case(ViewType, ViewType); + Optional general_category_from_string(StringView); bool code_point_has_general_category(u32 code_point, GeneralCategory general_category); diff --git a/Userland/Libraries/LibUnicode/String.cpp b/Userland/Libraries/LibUnicode/String.cpp index d1f2918130..b7aa405e7b 100644 --- a/Userland/Libraries/LibUnicode/String.cpp +++ b/Userland/Libraries/LibUnicode/String.cpp @@ -47,4 +47,14 @@ bool String::equals_ignoring_case(String const& other) const return Unicode::equals_ignoring_case(code_points(), other.code_points()); } +Optional String::find_byte_offset_ignoring_case(StringView needle, size_t from_byte_offset) const +{ + auto haystack = code_points().substring_view(from_byte_offset); + + if (auto index = Unicode::find_ignoring_case(haystack, Utf8View { needle }); index.has_value()) + return *index + from_byte_offset; + + return {}; +} + }