Skip to content

Commit

Permalink
Add QgsStringUtils.containsByWord method
Browse files Browse the repository at this point in the history
Given a candidate string, returns true if the candidate contains
all the individual words from another string, regardless of their order.
  • Loading branch information
nyalldawson committed Dec 11, 2024
1 parent f5447dc commit fe45e21
Show file tree
Hide file tree
Showing 8 changed files with 137 additions and 1 deletion.
1 change: 1 addition & 0 deletions python/PyQt6/core/auto_additions/qgsstringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
QgsStringUtils.htmlToMarkdown = staticmethod(QgsStringUtils.htmlToMarkdown)
QgsStringUtils.qRegExpEscape = staticmethod(QgsStringUtils.qRegExpEscape)
QgsStringUtils.truncateMiddleOfString = staticmethod(QgsStringUtils.truncateMiddleOfString)
QgsStringUtils.containsByWord = staticmethod(QgsStringUtils.containsByWord)
except (NameError, AttributeError):
pass
13 changes: 13 additions & 0 deletions python/PyQt6/core/auto_generated/qgsstringutils.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ will be truncated by removing characters from the middle of the string
and replacing them with a horizontal ellipsis character.

.. versionadded:: 3.22
%End

static bool containsByWord( const QString &candidate, const QString &words, Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive );
%Docstring
Given a ``candidate`` string, returns ``True`` if the ``candidate`` contains
all the individual words from another string, regardless of their order.

.. note::

The search does NOT need to match whole words in the ``candidate`` string,
so eg a candidate string of "Worldmap_Winkel_II" will return ``True`` for ``words`` "winkle world"

.. versionadded:: 3.42
%End

};
Expand Down
1 change: 1 addition & 0 deletions python/core/auto_additions/qgsstringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,6 @@
QgsStringUtils.htmlToMarkdown = staticmethod(QgsStringUtils.htmlToMarkdown)
QgsStringUtils.qRegExpEscape = staticmethod(QgsStringUtils.qRegExpEscape)
QgsStringUtils.truncateMiddleOfString = staticmethod(QgsStringUtils.truncateMiddleOfString)
QgsStringUtils.containsByWord = staticmethod(QgsStringUtils.containsByWord)
except (NameError, AttributeError):
pass
13 changes: 13 additions & 0 deletions python/core/auto_generated/qgsstringutils.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,19 @@ will be truncated by removing characters from the middle of the string
and replacing them with a horizontal ellipsis character.

.. versionadded:: 3.22
%End

static bool containsByWord( const QString &candidate, const QString &words, Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive );
%Docstring
Given a ``candidate`` string, returns ``True`` if the ``candidate`` contains
all the individual words from another string, regardless of their order.

.. note::

The search does NOT need to match whole words in the ``candidate`` string,
so eg a candidate string of "Worldmap_Winkel_II" will return ``True`` for ``words`` "winkle world"

.. versionadded:: 3.42
%End

};
Expand Down
17 changes: 17 additions & 0 deletions src/core/qgsstringutils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -745,6 +745,23 @@ QString QgsStringUtils::truncateMiddleOfString( const QString &string, int maxLe
#endif
}

bool QgsStringUtils::containsByWord( const QString &candidate, const QString &words, Qt::CaseSensitivity sensitivity )
{
if ( candidate.trimmed().isEmpty() )
return false;

const thread_local QRegularExpression rxWhitespace( QStringLiteral( "\\s+" ) );
const QStringList parts = words.split( rxWhitespace, Qt::SkipEmptyParts );
if ( parts.empty() )
return false;
for ( const QString &word : parts )
{
if ( !candidate.contains( word, sensitivity ) )
return false;
}
return true;
}

QgsStringReplacement::QgsStringReplacement( const QString &match, const QString &replacement, bool caseSensitive, bool wholeWordOnly )
: mMatch( match )
, mReplacement( replacement )
Expand Down
11 changes: 11 additions & 0 deletions src/core/qgsstringutils.h
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,17 @@ class CORE_EXPORT QgsStringUtils
*/
static QString truncateMiddleOfString( const QString &string, int maxLength );

/**
* Given a \a candidate string, returns TRUE if the \a candidate contains
* all the individual words from another string, regardless of their order.
*
* \note The search does NOT need to match whole words in the \a candidate string,
* so eg a candidate string of "Worldmap_Winkel_II" will return TRUE for \a words "winkle world"
*
* \since QGIS 3.42
*/
static bool containsByWord( const QString &candidate, const QString &words, Qt::CaseSensitivity sensitivity = Qt::CaseInsensitive );

};

#endif //QGSSTRINGUTILS_H
3 changes: 2 additions & 1 deletion src/gui/proj/qgscoordinatereferencesystemmodel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include "moc_qgscoordinatereferencesystemmodel.cpp"
#include "qgscoordinatereferencesystemutils.h"
#include "qgsapplication.h"
#include "qgsstringutils.h"

#include <QFont>

Expand Down Expand Up @@ -706,7 +707,7 @@ bool QgsCoordinateReferenceSystemProxyModel::filterAcceptsRow( int sourceRow, co
if ( !mFilterString.trimmed().isEmpty() )
{
const QString name = sourceModel()->data( sourceIndex, static_cast<int>( QgsCoordinateReferenceSystemModel::CustomRole::Name ) ).toString();
if ( !( name.contains( mFilterString, Qt::CaseInsensitive )
if ( !( QgsStringUtils::containsByWord( name, mFilterString )
|| authid.contains( mFilterString, Qt::CaseInsensitive ) ) )
return false;
}
Expand Down
79 changes: 79 additions & 0 deletions tests/src/python/test_qgsstringutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
__date__ = "30/08/2016"
__copyright__ = "Copyright 2016, The QGIS Project"

from qgis.PyQt.QtCore import Qt
from qgis.PyQt.QtXml import QDomDocument
from qgis.core import (
QgsStringReplacement,
Expand Down Expand Up @@ -371,6 +372,84 @@ def test_truncate_from_middle(self):
QgsStringUtils.truncateMiddleOfString("this is a test", 0), "…"
)

def test_contains_by_word(self):
"""
Test QgsStringUtils.containsByWord
"""
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "world hello"))
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "hello\tworld"))
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "hello"))
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "world"))
self.assertFalse(QgsStringUtils.containsByWord("Hello World", "goodbye"))

# Case insensitive (default)
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "WORLD"))
self.assertTrue(QgsStringUtils.containsByWord("HELLO WORLD", "world"))

# Case sensitive
self.assertFalse(
QgsStringUtils.containsByWord(
"Hello World", "WORLD", Qt.CaseSensitivity.CaseSensitive
)
)
self.assertTrue(
QgsStringUtils.containsByWord(
"Hello World", "World", Qt.CaseSensitivity.CaseSensitive
)
)

# Test that parts of words can match
self.assertTrue(
QgsStringUtils.containsByWord("Worldmap_Winkel_II", "winkel world")
)
self.assertTrue(QgsStringUtils.containsByWord("HelloWorld", "hello world"))
self.assertTrue(
QgsStringUtils.containsByWord("SuperCalifragilistic", "super cal fragi")
)

# empty strings
self.assertFalse(QgsStringUtils.containsByWord("Hello World", ""))
self.assertFalse(QgsStringUtils.containsByWord("Hello World", " "))
self.assertFalse(QgsStringUtils.containsByWord("", "hello"))
self.assertFalse(QgsStringUtils.containsByWord(" ", "hello"))
self.assertFalse(QgsStringUtils.containsByWord("", ""))
self.assertFalse(QgsStringUtils.containsByWord(" ", ""))
self.assertFalse(QgsStringUtils.containsByWord(" ", " "))

self.assertTrue(QgsStringUtils.containsByWord("Hello, World!", "hello world"))
self.assertTrue(QgsStringUtils.containsByWord("Hello-World", "hello world"))
self.assertTrue(QgsStringUtils.containsByWord("Hello_World", "hello world"))
self.assertTrue(QgsStringUtils.containsByWord("Hello\tWorld\n", "hello world"))

# test multiple words in different orders
self.assertTrue(
QgsStringUtils.containsByWord("The Quick Brown Fox", "fox quick")
)
self.assertTrue(
QgsStringUtils.containsByWord("The Quick Brown Fox", "brown the fox")
)
self.assertFalse(
QgsStringUtils.containsByWord("The Quick Brown Fox", "fox quick jumping")
)

# test handling of unicode characters"""
self.assertTrue(QgsStringUtils.containsByWord("École Primaire", "école"))
self.assertTrue(QgsStringUtils.containsByWord("München Stadt", "münchen"))
self.assertTrue(QgsStringUtils.containsByWord("北京市", "北京"))

# test handling of various whitespace scenarios
self.assertTrue(QgsStringUtils.containsByWord("Hello World", "hello world"))
self.assertTrue(QgsStringUtils.containsByWord("Hello\tWorld", "hello world"))
self.assertTrue(QgsStringUtils.containsByWord("Hello\nWorld", "hello world"))
self.assertTrue(
QgsStringUtils.containsByWord(" Hello World ", "hello world")
)

# Test handling of word boundaries
self.assertTrue(QgsStringUtils.containsByWord("HelloWorld", "hello"))
self.assertTrue(QgsStringUtils.containsByWord("WorldHello", "hello"))
self.assertTrue(QgsStringUtils.containsByWord("TheHelloWorld", "hello world"))


if __name__ == "__main__":
unittest.main()

0 comments on commit fe45e21

Please sign in to comment.