[API CHANGE] rtl/string: Add startsWith/endsWith for a single character

A lot of places in the code are calling OUString::endsWith with a string
containing a single character. Similar to lastIndexOf, this patch
provides overloads to the two methods to have an optimised version for
the case when there is only one character.

It would be kind of nice if the compiler could handle this optimisation
itself, but it’s unlikely to work because the string version of endsWith
ends up calling rtl_ustr_shortenedCompare_WithLength which isn’t inline
and is defined outside of a header.

The main incentive to do this is to make it easier to change calls to
lastIndexOf that should really be endsWith, as in tdf#171853.

Change-Id: Iaba5a7db3211eb12b0a83f441c0a81e664da6eea
Reviewed-on: https://gerrit.libreoffice.org/c/core/+/204363
Reviewed-by: Neil Roberts <nroberts@libreoffice.org>
Tested-by: Jenkins
This commit is contained in:
Neil Roberts
2026-04-30 06:33:28 +02:00
parent 323b77a6ca
commit ab871d3af9
5 changed files with 384 additions and 0 deletions
+120
View File
@@ -1221,6 +1221,66 @@ public:
}
#endif
/**
Check whether this string starts with a given character.
@param ch the character to be compared
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(char ch) const
{
return !isEmpty() && pData->buffer[0] == ch;
}
/**
Check whether this string starts with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(char ch, OString* rest) const {
assert(rest);
bool b = startsWith(ch);
if (b)
*rest = copy(1);
return b;
}
#if defined LIBO_INTERNAL_ONLY
/**
Check whether this string starts with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(char ch, std::string_view* rest) const {
assert(rest);
bool b = startsWith(ch);
if (b)
*rest = subView(1);
return b;
}
#endif
#if defined LIBO_INTERNAL_ONLY
/**
Check whether this string starts with a given string, ignoring the case of
@@ -1595,6 +1655,66 @@ public:
&& matchL(str, strLength, getLength() - strLength);
}
/**
Check whether this string ends with a given character.
@param ch the character to be compared
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(char ch) const
{
return !isEmpty() && pData->buffer[pData->length - 1] == ch;
}
/**
Check whether this string ends with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(char ch, OString* rest) const {
assert(rest);
bool b = endsWith(ch);
if (b)
*rest = copy(0, pData->length - 1);
return b;
}
#if defined LIBO_INTERNAL_ONLY
/**
Check whether this string ends with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(char ch, std::string_view* rest) const {
assert(rest);
bool b = endsWith(ch);
if (b)
*rest = subView(0, pData->length - 1);
return b;
}
#endif
friend bool operator == ( const OString& rStr1, const OString& rStr2 )
{ return rStr1.equals(rStr2); }
friend bool operator != ( const OString& rStr1, const OString& rStr2 )
+120
View File
@@ -1618,6 +1618,66 @@ public:
}
#endif
/**
Check whether this string starts with a given character.
@param str the character to be compared
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(sal_Unicode ch) const
{
return !isEmpty() && pData->buffer[0] == ch;
}
/**
Check whether this string starts with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(sal_Unicode ch, OUString* rest) const {
assert(rest);
bool b = startsWith(ch);
if (b)
*rest = copy(1);
return b;
}
#if defined LIBO_INTERNAL_ONLY
/**
Check whether this string starts with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the start of this string
@since LibreOffice 26.8
*/
bool startsWith(sal_Unicode ch, std::u16string_view* rest) const {
assert(rest);
bool b = startsWith(ch);
if (b)
*rest = subView(1);
return b;
}
#endif
/**
Check whether this string starts with a given string, ignoring the case of
ASCII letters.
@@ -1825,6 +1885,66 @@ public:
}
#endif
/**
Check whether this string ends with a given character.
@param str the character to be compared
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(sal_Unicode ch) const
{
return !isEmpty() && pData->buffer[pData->length - 1] == ch;
}
/**
Check whether this string ends with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(sal_Unicode ch, OUString* rest) const {
assert(rest);
bool b = endsWith(ch);
if (b)
*rest = copy(0, pData->length - 1);
return b;
}
#if defined LIBO_INTERNAL_ONLY
/**
Check whether this string ends with a given character.
@param str the character to be compared
@param rest if this function returns true, then assign a
copy of the remainder of this string to *rest.
@return true if and only if the given character appears at the end of this string
@since LibreOffice 26.8
*/
bool endsWith(sal_Unicode ch, std::u16string_view* rest) const {
assert(rest);
bool b = endsWith(ch);
if (b)
*rest = subView(0, pData->length - 1);
return b;
}
#endif
#if defined LIBO_INTERNAL_ONLY
/**
@overload
+70
View File
@@ -21,11 +21,15 @@ private:
void testStartsWithIgnoreAsciiCase();
void testCompareTo();
void testUtf8StringLiterals();
void testStartsWithChar();
void testEndsWithChar();
CPPUNIT_TEST_SUITE(Test);
CPPUNIT_TEST(testStartsWithIgnoreAsciiCase);
CPPUNIT_TEST(testCompareTo);
CPPUNIT_TEST(testUtf8StringLiterals);
CPPUNIT_TEST(testStartsWithChar);
CPPUNIT_TEST(testEndsWithChar);
CPPUNIT_TEST_SUITE_END();
};
@@ -116,6 +120,72 @@ void Test::testUtf8StringLiterals()
CPPUNIT_ASSERT_EQUAL(97, int(static_cast<unsigned char>(sIn[2])));
}
void Test::testStartsWithChar()
{
CPPUNIT_ASSERT(!""_ostr.startsWith('.'));
CPPUNIT_ASSERT("."_ostr.startsWith('.'));
CPPUNIT_ASSERT(".foo"_ostr.startsWith('.'));
CPPUNIT_ASSERT(!"."_ostr.startsWith('?'));
CPPUNIT_ASSERT(!".foo"_ostr.startsWith('?'));
{
OString rest = "not_changed"_ostr;
CPPUNIT_ASSERT(!"foo"_ostr.startsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL("not_changed"_ostr, rest);
}
{
OString rest = "must_change"_ostr;
CPPUNIT_ASSERT("/removed_slash"_ostr.startsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL("removed_slash"_ostr, rest);
}
{
std::string_view rest = "not_changed";
CPPUNIT_ASSERT(!"foo"_ostr.startsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL("not_changed"_ostr, OString(rest));
}
{
std::string_view rest = "must_change";
CPPUNIT_ASSERT("/removed_slash"_ostr.startsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL("removed_slash"_ostr, OString(rest));
}
}
void Test::testEndsWithChar()
{
CPPUNIT_ASSERT(!""_ostr.endsWith('.'));
CPPUNIT_ASSERT("."_ostr.endsWith('.'));
CPPUNIT_ASSERT("foo."_ostr.endsWith('.'));
CPPUNIT_ASSERT(!"."_ostr.endsWith('?'));
CPPUNIT_ASSERT(!"foo."_ostr.endsWith('?'));
{
OString rest = "not_changed"_ostr;
CPPUNIT_ASSERT(!"foo"_ostr.endsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL("not_changed"_ostr, rest);
}
{
OString rest = "must_change"_ostr;
CPPUNIT_ASSERT("removed_slash/"_ostr.endsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL("removed_slash"_ostr, rest);
}
{
std::string_view rest = "not_changed";
CPPUNIT_ASSERT(!"foo"_ostr.endsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL("not_changed"_ostr, OString(rest));
}
{
std::string_view rest = "must_change";
CPPUNIT_ASSERT("removed_slash/"_ostr.endsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL("removed_slash"_ostr, OString(rest));
}
}
CPPUNIT_TEST_SUITE_REGISTRATION(Test);
}
@@ -33,9 +33,11 @@ class EndsWith: public CppUnit::TestFixture
{
private:
void endsWith();
void endsWithChar();
CPPUNIT_TEST_SUITE(EndsWith);
CPPUNIT_TEST(endsWith);
CPPUNIT_TEST(endsWithChar);
CPPUNIT_TEST_SUITE_END();
};
@@ -107,4 +109,39 @@ void test::oustring::EndsWith::endsWith()
}
}
void test::oustring::EndsWith::endsWithChar()
{
CPPUNIT_ASSERT(!u""_ustr.endsWith('.'));
CPPUNIT_ASSERT(u"."_ustr.endsWith('.'));
CPPUNIT_ASSERT(u"foo."_ustr.endsWith('.'));
CPPUNIT_ASSERT(u"aĥ"_ustr.endsWith(u'ĥ'));
CPPUNIT_ASSERT(!u"."_ustr.endsWith('?'));
CPPUNIT_ASSERT(!u"foo."_ustr.endsWith('?'));
CPPUNIT_ASSERT(!u"aĥ"_ustr.endsWith(u'ĉ'));
{
OUString rest = u"not_changed"_ustr;
CPPUNIT_ASSERT(!u"foo"_ustr.endsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL(u"not_changed"_ustr, rest);
}
{
OUString rest = u"must_change"_ustr;
CPPUNIT_ASSERT(u"removed_slash/"_ustr.endsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL(u"removed_slash"_ustr, rest);
}
{
std::u16string_view rest = u"not_changed";
CPPUNIT_ASSERT(!u"foo"_ustr.endsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL(u"not_changed"_ustr, OUString(rest));
}
{
std::u16string_view rest = u"must_change";
CPPUNIT_ASSERT(u"removed_slash/"_ustr.endsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL(u"removed_slash"_ustr, OUString(rest));
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */
@@ -17,9 +17,11 @@ class StartsWith: public CppUnit::TestFixture
{
private:
void startsWith();
void startsWithChar();
CPPUNIT_TEST_SUITE(StartsWith);
CPPUNIT_TEST(startsWith);
CPPUNIT_TEST(startsWithChar);
CPPUNIT_TEST_SUITE_END();
};
@@ -34,4 +36,39 @@ void test::oustring::StartsWith::startsWith()
CPPUNIT_ASSERT( !u"foobar"_ustr.startsWith( "oo" ));
}
void test::oustring::StartsWith::startsWithChar()
{
CPPUNIT_ASSERT(!u""_ustr.startsWith('.'));
CPPUNIT_ASSERT(u"."_ustr.startsWith('.'));
CPPUNIT_ASSERT(u".foo"_ustr.startsWith('.'));
CPPUNIT_ASSERT(u"ĉu?"_ustr.startsWith(u'ĉ'));
CPPUNIT_ASSERT(!u"."_ustr.startsWith('?'));
CPPUNIT_ASSERT(!u".foo"_ustr.startsWith('?'));
CPPUNIT_ASSERT(!u"ĉu?"_ustr.startsWith(u'ĥ'));
{
OUString rest = u"not_changed"_ustr;
CPPUNIT_ASSERT(!u"foo"_ustr.startsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL(u"not_changed"_ustr, rest);
}
{
OUString rest = u"must_change"_ustr;
CPPUNIT_ASSERT(u"/removed_slash"_ustr.startsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL(u"removed_slash"_ustr, rest);
}
{
std::u16string_view rest = u"not_changed";
CPPUNIT_ASSERT(!u"foo"_ustr.startsWith('p', &rest));
CPPUNIT_ASSERT_EQUAL(u"not_changed"_ustr, OUString(rest));
}
{
std::u16string_view rest = u"must_change";
CPPUNIT_ASSERT(u"/removed_slash"_ustr.startsWith('/', &rest));
CPPUNIT_ASSERT_EQUAL(u"removed_slash"_ustr, OUString(rest));
}
}
/* vim:set shiftwidth=4 softtabstop=4 expandtab: */