diff --git a/AK/String.h b/AK/String.h index 1a19b6f429c..7bcf722a476 100644 --- a/AK/String.h +++ b/AK/String.h @@ -49,6 +49,10 @@ public: ErrorOr to_lowercase(Optional const& locale = {}) const; ErrorOr to_uppercase(Optional const& locale = {}) const; ErrorOr to_titlecase(Optional const& locale = {}) const; + ErrorOr to_casefold() const; + + // Compare this String against another string with caseless matching. Using this method requires linking LibUnicode into your application. + ErrorOr equals_ignoring_case(String const&) const; // Creates a substring with a deep copy of the specified data window. ErrorOr substring_from_byte_offset(size_t start, size_t byte_count) const; diff --git a/Tests/AK/TestString.cpp b/Tests/AK/TestString.cpp index 42f5ffc63f9..d78854b6269 100644 --- a/Tests/AK/TestString.cpp +++ b/Tests/AK/TestString.cpp @@ -187,6 +187,66 @@ TEST_CASE(to_titlecase) } } +TEST_CASE(equals_ignoring_case) +{ + { + String string1 {}; + String string2 {}; + + EXPECT(MUST(string1.equals_ignoring_case(string2))); + } + { + auto string1 = MUST(String::from_utf8("abcd"sv)); + auto string2 = MUST(String::from_utf8("ABCD"sv)); + auto string3 = MUST(String::from_utf8("AbCd"sv)); + auto string4 = MUST(String::from_utf8("dcba"sv)); + + EXPECT(MUST(string1.equals_ignoring_case(string2))); + EXPECT(MUST(string1.equals_ignoring_case(string3))); + EXPECT(!MUST(string1.equals_ignoring_case(string4))); + + EXPECT(MUST(string2.equals_ignoring_case(string1))); + EXPECT(MUST(string2.equals_ignoring_case(string3))); + EXPECT(!MUST(string2.equals_ignoring_case(string4))); + + EXPECT(MUST(string3.equals_ignoring_case(string1))); + EXPECT(MUST(string3.equals_ignoring_case(string2))); + EXPECT(!MUST(string3.equals_ignoring_case(string4))); + } + { + auto string1 = MUST(String::from_utf8("\u00DF"sv)); // LATIN SMALL LETTER SHARP S + auto string2 = MUST(String::from_utf8("SS"sv)); + auto string3 = MUST(String::from_utf8("Ss"sv)); + auto string4 = MUST(String::from_utf8("ss"sv)); + auto string5 = MUST(String::from_utf8("S"sv)); + auto string6 = MUST(String::from_utf8("s"sv)); + + EXPECT(MUST(string1.equals_ignoring_case(string2))); + EXPECT(MUST(string1.equals_ignoring_case(string3))); + EXPECT(MUST(string1.equals_ignoring_case(string4))); + EXPECT(!MUST(string1.equals_ignoring_case(string5))); + EXPECT(!MUST(string1.equals_ignoring_case(string6))); + + EXPECT(MUST(string2.equals_ignoring_case(string1))); + EXPECT(MUST(string2.equals_ignoring_case(string3))); + EXPECT(MUST(string2.equals_ignoring_case(string4))); + EXPECT(!MUST(string2.equals_ignoring_case(string5))); + EXPECT(!MUST(string2.equals_ignoring_case(string6))); + + EXPECT(MUST(string3.equals_ignoring_case(string1))); + EXPECT(MUST(string3.equals_ignoring_case(string2))); + EXPECT(MUST(string3.equals_ignoring_case(string4))); + EXPECT(!MUST(string3.equals_ignoring_case(string5))); + EXPECT(!MUST(string3.equals_ignoring_case(string6))); + + EXPECT(MUST(string4.equals_ignoring_case(string1))); + EXPECT(MUST(string4.equals_ignoring_case(string2))); + EXPECT(MUST(string4.equals_ignoring_case(string3))); + EXPECT(!MUST(string4.equals_ignoring_case(string5))); + EXPECT(!MUST(string4.equals_ignoring_case(string6))); + } +} + TEST_CASE(is_one_of) { auto foo = MUST(String::from_utf8("foo"sv)); diff --git a/Userland/Libraries/LibUnicode/String.cpp b/Userland/Libraries/LibUnicode/String.cpp index 5251d310bd9..e198058a706 100644 --- a/Userland/Libraries/LibUnicode/String.cpp +++ b/Userland/Libraries/LibUnicode/String.cpp @@ -33,4 +33,19 @@ ErrorOr String::to_titlecase(Optional const& locale) const return builder.to_string(); } +ErrorOr String::to_casefold() const +{ + StringBuilder builder; + TRY(Unicode::Detail::build_casefold_string(code_points(), builder)); + return builder.to_string(); +} + +// https://www.unicode.org/versions/Unicode15.0.0/ch03.pdf#G34145 +ErrorOr String::equals_ignoring_case(String const& other) const +{ + // A string X is a caseless match for a string Y if and only if: + // toCasefold(X) = toCasefold(Y) + return TRY(to_casefold()) == TRY(other.to_casefold()); +} + }