Add Latin 1 case-insensitive Boyer-Moore searcher

The std::boyer_moore_searcher is buggy for older verions of Microsoft's
STL, and missing in AppleClang's libc++ with an inefficient fall back.

Fixes: QTBUG-100236
Change-Id: Ic3cc916946546d2ef78456cd15e1425d957b989d
Reviewed-by: Thiago Macieira <thiago.macieira@intel.com>
Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
bb10
Øystein Heskestad 2022-07-27 13:41:54 +02:00
parent 26a73e1b31
commit 15368fb31b
2 changed files with 175 additions and 22 deletions

View File

@ -1385,6 +1385,71 @@ static int latin1nicmp(const char *lhsChar, qsizetype lSize, const char *rhsChar
}
return lencmp(lSize, rSize);
}
namespace {
template<Qt::CaseSensitivity cs>
inline uchar latin1_fold(const uchar c)
{
return c;
}
template<>
inline uchar latin1_fold<Qt::CaseSensitivity::CaseInsensitive>(const uchar c)
{
return latin1Lower[c];
}
template<Qt::CaseSensitivity cs>
inline void bm_latin1_init_skiptable(const uchar *cc, qsizetype len, uchar *skiptable)
{
int l = int(qMin(len, qsizetype(255)));
memset(skiptable, l, 256 * sizeof(uchar));
cc += len - l;
while (l--)
skiptable[latin1_fold<cs>(*cc++)] = l;
}
template<Qt::CaseSensitivity cs>
inline qsizetype bm_latin1_find(const uchar *cc, qsizetype l, qsizetype index, const uchar *puc,
qsizetype pl, const uchar *skiptable)
{
if (pl == 0)
return index > l ? -1 : index;
const qsizetype pl_minus_one = pl - 1;
const uchar *current = cc + index + pl_minus_one;
const uchar *end = cc + l;
while (current < end) {
qsizetype skip = skiptable[latin1_fold<cs>(*current)];
if (!skip) {
// possible match
while (skip < pl) {
if (latin1_fold<cs>(*(current - skip)) != latin1_fold<cs>(puc[pl_minus_one - skip]))
break;
skip++;
}
if (skip > pl_minus_one) // we have a match
return (current - cc) - skip + 1;
// in case we don't have a match we are a bit inefficient as we only skip by one
// when we have the non matching char in the string.
if (skiptable[latin1_fold<cs>(*(current - skip))] == pl)
skip = pl - skip;
else
skip = 1;
}
if (current > end - skip)
break;
current += skip;
}
return -1; // not found
}
}
bool QtPrivate::equalStrings(QStringView lhs, QStringView rhs) noexcept
{
return ucstreq(lhs.utf16(), lhs.size(), rhs.utf16(), rhs.size());
@ -10752,28 +10817,12 @@ qsizetype QtPrivate::findString(QLatin1StringView haystack, qsizetype from, QLat
}
return -1;
}
#ifdef __cpp_lib_boyer_moore_searcher
const auto ciHasher = [](char a) { return latin1Lower[uchar(a)]; };
const auto ciEqual = [](char a, char b) {
return latin1Lower[uchar(a)] == latin1Lower[uchar(b)];
};
const auto it =
std::search(haystack.begin() + from, haystack.end(),
std::boyer_moore_searcher(needle.begin(), needle.end(), ciHasher, ciEqual));
return it == haystack.end() ? -1 : std::distance(haystack.begin(), it);
#else
QVarLengthArray<char16_t> h(adjustedSize);
const auto begin = haystack.end() - adjustedSize;
qt_from_latin1(h.data(), begin, adjustedSize);
QVarLengthArray<char16_t> n(needle.size());
qt_from_latin1(n.data(), needle.latin1(), needle.size());
qsizetype res = QtPrivate::findString(QStringView(h.constData(), h.size()), 0,
QStringView(n.constData(), n.size()), cs);
if (res == -1)
return -1;
return res + std::distance(haystack.begin(), begin);
#endif
uchar skiptable[256];
bm_latin1_init_skiptable<Qt::CaseSensitivity::CaseInsensitive>(
reinterpret_cast<const unsigned char *>(needle.begin()), needle.size(), skiptable);
return bm_latin1_find<Qt::CaseSensitivity::CaseInsensitive>(
reinterpret_cast<const unsigned char *>(haystack.begin()), haystack.size(), from,
reinterpret_cast<const unsigned char *>(needle.begin()), needle.size(), skiptable);
}
qsizetype QtPrivate::lastIndexOf(QStringView haystack, qsizetype from, QStringView needle, Qt::CaseSensitivity cs) noexcept

View File

@ -31,6 +31,8 @@ private Q_SLOTS:
void relationalOperators_data();
void relationalOperators();
void count();
void indexOf_data();
void indexOf();
};
void tst_QLatin1StringView::constExpr()
@ -410,6 +412,108 @@ void tst_QLatin1StringView::count()
QCOMPARE("a\0b"_L1.count(QChar::SpecialCharacter::LineSeparator), 0);
}
void tst_QLatin1StringView::indexOf_data()
{
using namespace Qt::Literals::StringLiterals;
QTest::addColumn<QLatin1StringView>("needle");
QTest::addColumn<QLatin1StringView>("haystack");
QTest::addColumn<int>("from");
QTest::addColumn<int>("indexCaseSensitive");
QTest::addColumn<int>("indexCaseInsensitive");
// Should never trigger Boyer Moore algorithm
QTest::newRow("Single letter match start")
<< QLatin1StringView("A") << QLatin1StringView("ABCDEF") << 0 << 0 << 0;
QTest::newRow("Single letter match second letter")
<< QLatin1StringView("B") << QLatin1StringView("ABCDEF") << 0 << 1 << 1;
QTest::newRow("Single letter mismatch")
<< QLatin1StringView("G") << QLatin1StringView("ABCDEF") << 0 << -1 << -1;
QTest::newRow("Single letter case sensitive start")
<< QLatin1StringView("a") << QLatin1StringView("ABCDEF") << 0 << -1 << 0;
QTest::newRow("Single letter case sensitive end")
<< QLatin1StringView("f") << QLatin1StringView("ABCDEF") << 0 << -1 << 5;
QTest::newRow("Single letter different match depending on case")
<< QLatin1StringView("a") << QLatin1StringView("ABCabc") << 0 << 3 << 0;
QTest::newRow("Single letter different match depending on case from 2")
<< QLatin1StringView("a") << QLatin1StringView("ABCABCabc") << 2 << 6 << 3;
QTest::newRow("Single letter negative from")
<< QLatin1StringView("a") << QLatin1StringView("abcabc") << -3 << 3 << 3;
QTest::newRow("Single letter non-ASCII") // searching for "ø" in "Øø"
<< "\xf8"_L1
<< "\xd8\xf8"_L1 << 0 << 1 << 0;
QTest::newRow("Single uppercase letter")
<< QLatin1StringView("A") << QLatin1StringView("aA") << 0 << 1 << 0;
// Might trigger Boyer Moore algorithm
QTest::newRow("Small match start")
<< QLatin1StringView("ABC") << QLatin1StringView("ABCDEF") << 0 << 0 << 0;
QTest::newRow("Small match second letter")
<< QLatin1StringView("BCD") << QLatin1StringView("ABCDEF") << 0 << 1 << 1;
QTest::newRow("Small mismatch")
<< QLatin1StringView("EFG") << QLatin1StringView("ABCDEF") << 0 << -1 << -1;
QTest::newRow("Small case sensitive start")
<< QLatin1StringView("abc") << QLatin1StringView("ABCDEF") << 0 << -1 << 0;
QTest::newRow("Small case sensitive end")
<< QLatin1StringView("DEF") << QLatin1StringView("abcdef") << 0 << -1 << 3;
QTest::newRow("Small different match depending on case")
<< QLatin1StringView("abcabc") << QLatin1StringView("!!ABCabcabcABC") << 0 << 5 << 2;
QTest::newRow("Small different match depending on case from 2")
<< QLatin1StringView("abcabc") << QLatin1StringView("ABCABCabcabcABC") << 2 << 6 << 3;
QTest::newRow("Small negative from") << QLatin1StringView("negative")
<< QLatin1StringView("negativenegative") << -8 << 8 << 8;
QTest::newRow("Small non-ASCII") // searching for "løve" in "LØVEløve"
<< "l\xf8ve"_L1
<< "L\xd8VEl\xf8ve"_L1 << 0 << 4 << 0;
QTest::newRow("Small skip test")
<< QLatin1StringView("ABBB") << QLatin1StringView("ABABBB") << 0 << 2 << 2;
QTest::newRow("Small uppercase needle")
<< QLatin1StringView("ABCDEF") << QLatin1StringView("abcdefABCDEF") << 0 << 6 << 0;
// Should trigger Boyer Moore algorithm
QTest::newRow("Medium match start")
<< QLatin1StringView("ABCDEFGHIJKLMNOP")
<< QLatin1StringView("ABCDEFGHIJKLMNOPQRSTUVWXYZ") << 0 << 0 << 0;
QTest::newRow("Medium match second letter")
<< QLatin1StringView("BCDEFGHIJKLMNOPQ")
<< QLatin1StringView("ABCDEFGHIJKLMNOPQRSTUVWXYZ") << 0 << 1 << 1;
QTest::newRow("Medium mismatch")
<< QLatin1StringView("PONMLKJIHGFEDCBA")
<< QLatin1StringView("ABCDEFGHIJKLMNOPQRSTUVWXYZ") << 0 << -1 << -1;
QTest::newRow("Medium case sensitive start")
<< QLatin1StringView("abcdefghijklmnopq")
<< QLatin1StringView("ABCDEFGHIJKLMNOPQRSTUVWXYZ") << 0 << -1 << 0;
QTest::newRow("Medium case sensitive second letter")
<< QLatin1StringView("BCDEFGHIJKLMNOPQR")
<< QLatin1StringView("abcdefghijklmnopqrstuvxyz") << 0 << -1 << 1;
QTest::newRow("Medium different match depending on case")
<< QLatin1StringView("testingtesting")
<< QLatin1StringView("TESTINGtestingtestingTESTING") << 0 << 7 << 0;
QTest::newRow("Medium different match depending on case from 2")
<< QLatin1StringView("testingtesting")
<< QLatin1StringView("TESTINGTESTINGtestingtestingTESTING") << 2 << 14 << 7;
QTest::newRow("Medium negative from")
<< QLatin1StringView("abcdefghijklmnop")
<< QLatin1StringView("abcdefghijklmnopabcdefghijklmnop") << -16 << 16 << 16;
QTest::newRow("Medium non-ASCII") // searching for "dampfschiffahrtsgesellschaftskapitän"
<< "dampfschiffahrtsgesellschaftskapit\xe4n"_L1
<< "DAMPFSCHIFFAHRTSGESELLSCHAFTSKAPIT\xc4Ndampfschiffahrtsgesellschaftskapit\xe4n"_L1
<< 0 << 36 << 0;
QTest::newRow("Medium skip test") << QLatin1StringView("ABBBBBBBBBBBBBBB")
<< QLatin1StringView("ABABBBBBBBBBBBBBBB") << 0 << 2 << 2;
}
void tst_QLatin1StringView::indexOf()
{
QFETCH(QLatin1StringView, needle);
QFETCH(QLatin1StringView, haystack);
QFETCH(int, from);
QFETCH(int, indexCaseSensitive);
QFETCH(int, indexCaseInsensitive);
QCOMPARE(haystack.indexOf(needle, from, Qt::CaseSensitive), (qsizetype)indexCaseSensitive);
QCOMPARE(haystack.indexOf(needle, from, Qt::CaseInsensitive), (qsizetype)indexCaseInsensitive);
}
QTEST_APPLESS_MAIN(tst_QLatin1StringView)
#include "tst_qlatin1stringview.moc"