LCOV - code coverage report
Current view: top level - core - qgsstringutils.cpp (source / functions) Hit Total Coverage
Test: coverage.info.cleaned Lines: 0 452 0.0 %
Date: 2021-04-10 08:29:14 Functions: 0 0 -
Branches: 0 0 -

           Branch data     Line data    Source code
       1                 :            : /***************************************************************************
       2                 :            :     qgsstringutils.cpp
       3                 :            :     ------------------
       4                 :            :     begin                : June 2015
       5                 :            :     copyright            : (C) 2015 by Nyall Dawson
       6                 :            :     email                : nyall dot dawson at gmail dot com
       7                 :            :  ***************************************************************************
       8                 :            :  *                                                                         *
       9                 :            :  *   This program is free software; you can redistribute it and/or modify  *
      10                 :            :  *   it under the terms of the GNU General Public License as published by  *
      11                 :            :  *   the Free Software Foundation; either version 2 of the License, or     *
      12                 :            :  *   (at your option) any later version.                                   *
      13                 :            :  *                                                                         *
      14                 :            :  ***************************************************************************/
      15                 :            : 
      16                 :            : #include "qgsstringutils.h"
      17                 :            : #include "qgslogger.h"
      18                 :            : #include <QVector>
      19                 :            : #include <QRegExp>
      20                 :            : #include <QStringList>
      21                 :            : #include <QTextBoundaryFinder>
      22                 :            : #include <QRegularExpression>
      23                 :            : #include <cstdlib> // for std::abs
      24                 :            : 
      25                 :          0 : QString QgsStringUtils::capitalize( const QString &string, QgsStringUtils::Capitalization capitalization )
      26                 :            : {
      27                 :          0 :   if ( string.isEmpty() )
      28                 :          0 :     return QString();
      29                 :            : 
      30                 :          0 :   switch ( capitalization )
      31                 :            :   {
      32                 :            :     case MixedCase:
      33                 :          0 :       return string;
      34                 :            : 
      35                 :            :     case AllUppercase:
      36                 :          0 :       return string.toUpper();
      37                 :            : 
      38                 :            :     case AllLowercase:
      39                 :          0 :       return string.toLower();
      40                 :            : 
      41                 :            :     case ForceFirstLetterToCapital:
      42                 :            :     {
      43                 :          0 :       QString temp = string;
      44                 :            : 
      45                 :          0 :       QTextBoundaryFinder wordSplitter( QTextBoundaryFinder::Word, string.constData(), string.length(), nullptr, 0 );
      46                 :          0 :       QTextBoundaryFinder letterSplitter( QTextBoundaryFinder::Grapheme, string.constData(), string.length(), nullptr, 0 );
      47                 :            : 
      48                 :          0 :       wordSplitter.setPosition( 0 );
      49                 :          0 :       bool first = true;
      50                 :          0 :       while ( ( first && wordSplitter.boundaryReasons() & QTextBoundaryFinder::StartOfItem )
      51                 :          0 :               || wordSplitter.toNextBoundary() >= 0 )
      52                 :            :       {
      53                 :          0 :         first = false;
      54                 :          0 :         letterSplitter.setPosition( wordSplitter.position() );
      55                 :          0 :         letterSplitter.toNextBoundary();
      56                 :          0 :         QString substr = string.mid( wordSplitter.position(), letterSplitter.position() - wordSplitter.position() );
      57                 :          0 :         temp.replace( wordSplitter.position(), substr.length(), substr.toUpper() );
      58                 :          0 :       }
      59                 :          0 :       return temp;
      60                 :          0 :     }
      61                 :            : 
      62                 :            :     case TitleCase:
      63                 :            :     {
      64                 :            :       // yes, this is MASSIVELY simplifying the problem!!
      65                 :            : 
      66                 :          0 :       static QStringList smallWords;
      67                 :          0 :       static QStringList newPhraseSeparators;
      68                 :          0 :       static QRegularExpression splitWords;
      69                 :          0 :       if ( smallWords.empty() )
      70                 :            :       {
      71                 :          0 :         smallWords = QObject::tr( "a|an|and|as|at|but|by|en|for|if|in|nor|of|on|or|per|s|the|to|vs.|vs|via" ).split( '|' );
      72                 :          0 :         newPhraseSeparators = QObject::tr( ".|:" ).split( '|' );
      73                 :          0 :         splitWords = QRegularExpression( QStringLiteral( "\\b" ), QRegularExpression::UseUnicodePropertiesOption );
      74                 :          0 :       }
      75                 :            : 
      76                 :          0 :       const bool allSameCase = string.toLower() == string || string.toUpper() == string;
      77                 :            : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 0)
      78                 :            :       const QStringList parts = ( allSameCase ? string.toLower() : string ).split( splitWords, QString::SkipEmptyParts );
      79                 :            : #else
      80                 :          0 :       const QStringList parts = ( allSameCase ? string.toLower() : string ).split( splitWords, Qt::SkipEmptyParts );
      81                 :            : #endif
      82                 :          0 :       QString result;
      83                 :          0 :       bool firstWord = true;
      84                 :          0 :       int i = 0;
      85                 :          0 :       int lastWord = parts.count() - 1;
      86                 :          0 :       for ( const QString &word : std::as_const( parts ) )
      87                 :            :       {
      88                 :          0 :         if ( newPhraseSeparators.contains( word.trimmed() ) )
      89                 :            :         {
      90                 :          0 :           firstWord = true;
      91                 :          0 :           result += word;
      92                 :          0 :         }
      93                 :          0 :         else if ( firstWord || ( i == lastWord ) || !smallWords.contains( word ) )
      94                 :            :         {
      95                 :          0 :           result += word.at( 0 ).toUpper() + word.mid( 1 );
      96                 :          0 :           firstWord = false;
      97                 :          0 :         }
      98                 :            :         else
      99                 :            :         {
     100                 :          0 :           result += word;
     101                 :            :         }
     102                 :          0 :         i++;
     103                 :            :       }
     104                 :          0 :       return result;
     105                 :          0 :     }
     106                 :            : 
     107                 :            :     case UpperCamelCase:
     108                 :          0 :       QString result = QgsStringUtils::capitalize( string.toLower(), QgsStringUtils::ForceFirstLetterToCapital ).simplified();
     109                 :          0 :       result.remove( ' ' );
     110                 :          0 :       return result;
     111                 :          0 :   }
     112                 :            :   // no warnings
     113                 :          0 :   return string;
     114                 :          0 : }
     115                 :            : 
     116                 :            : // original code from http://www.qtcentre.org/threads/52456-HTML-Unicode-ampersand-encoding
     117                 :          0 : QString QgsStringUtils::ampersandEncode( const QString &string )
     118                 :            : {
     119                 :          0 :   QString encoded;
     120                 :          0 :   for ( int i = 0; i < string.size(); ++i )
     121                 :            :   {
     122                 :          0 :     QChar ch = string.at( i );
     123                 :          0 :     if ( ch.unicode() > 160 )
     124                 :          0 :       encoded += QStringLiteral( "&#%1;" ).arg( static_cast< int >( ch.unicode() ) );
     125                 :          0 :     else if ( ch.unicode() == 38 )
     126                 :          0 :       encoded += QLatin1String( "&amp;" );
     127                 :          0 :     else if ( ch.unicode() == 60 )
     128                 :          0 :       encoded += QLatin1String( "&lt;" );
     129                 :          0 :     else if ( ch.unicode() == 62 )
     130                 :          0 :       encoded += QLatin1String( "&gt;" );
     131                 :            :     else
     132                 :          0 :       encoded += ch;
     133                 :          0 :   }
     134                 :          0 :   return encoded;
     135                 :          0 : }
     136                 :            : 
     137                 :          0 : int QgsStringUtils::levenshteinDistance( const QString &string1, const QString &string2, bool caseSensitive )
     138                 :            : {
     139                 :          0 :   int length1 = string1.length();
     140                 :          0 :   int length2 = string2.length();
     141                 :            : 
     142                 :            :   //empty strings? solution is trivial...
     143                 :          0 :   if ( string1.isEmpty() )
     144                 :            :   {
     145                 :          0 :     return length2;
     146                 :            :   }
     147                 :          0 :   else if ( string2.isEmpty() )
     148                 :            :   {
     149                 :          0 :     return length1;
     150                 :            :   }
     151                 :            : 
     152                 :            :   //handle case sensitive flag (or not)
     153                 :          0 :   QString s1( caseSensitive ? string1 : string1.toLower() );
     154                 :          0 :   QString s2( caseSensitive ? string2 : string2.toLower() );
     155                 :            : 
     156                 :          0 :   const QChar *s1Char = s1.constData();
     157                 :          0 :   const QChar *s2Char = s2.constData();
     158                 :            : 
     159                 :            :   //strip out any common prefix
     160                 :          0 :   int commonPrefixLen = 0;
     161                 :          0 :   while ( length1 > 0 && length2 > 0 && *s1Char == *s2Char )
     162                 :            :   {
     163                 :          0 :     commonPrefixLen++;
     164                 :          0 :     length1--;
     165                 :          0 :     length2--;
     166                 :          0 :     s1Char++;
     167                 :          0 :     s2Char++;
     168                 :            :   }
     169                 :            : 
     170                 :            :   //strip out any common suffix
     171                 :          0 :   while ( length1 > 0 && length2 > 0 && s1.at( commonPrefixLen + length1 - 1 ) == s2.at( commonPrefixLen + length2 - 1 ) )
     172                 :            :   {
     173                 :          0 :     length1--;
     174                 :          0 :     length2--;
     175                 :            :   }
     176                 :            : 
     177                 :            :   //fully checked either string? if so, the answer is easy...
     178                 :          0 :   if ( length1 == 0 )
     179                 :            :   {
     180                 :          0 :     return length2;
     181                 :            :   }
     182                 :          0 :   else if ( length2 == 0 )
     183                 :            :   {
     184                 :          0 :     return length1;
     185                 :            :   }
     186                 :            : 
     187                 :            :   //ensure the inner loop is longer
     188                 :          0 :   if ( length1 > length2 )
     189                 :            :   {
     190                 :          0 :     std::swap( s1, s2 );
     191                 :          0 :     std::swap( length1, length2 );
     192                 :          0 :   }
     193                 :            : 
     194                 :            :   //levenshtein algorithm begins here
     195                 :          0 :   QVector< int > col;
     196                 :          0 :   col.fill( 0, length2 + 1 );
     197                 :          0 :   QVector< int > prevCol;
     198                 :          0 :   prevCol.reserve( length2 + 1 );
     199                 :          0 :   for ( int i = 0; i < length2 + 1; ++i )
     200                 :            :   {
     201                 :          0 :     prevCol << i;
     202                 :          0 :   }
     203                 :          0 :   const QChar *s2start = s2Char;
     204                 :          0 :   for ( int i = 0; i < length1; ++i )
     205                 :            :   {
     206                 :          0 :     col[0] = i + 1;
     207                 :          0 :     s2Char = s2start;
     208                 :          0 :     for ( int j = 0; j < length2; ++j )
     209                 :            :     {
     210                 :          0 :       col[j + 1] = std::min( std::min( 1 + col[j], 1 + prevCol[1 + j] ), prevCol[j] + ( ( *s1Char == *s2Char ) ? 0 : 1 ) );
     211                 :          0 :       s2Char++;
     212                 :          0 :     }
     213                 :          0 :     col.swap( prevCol );
     214                 :          0 :     s1Char++;
     215                 :          0 :   }
     216                 :          0 :   return prevCol[length2];
     217                 :          0 : }
     218                 :            : 
     219                 :          0 : QString QgsStringUtils::longestCommonSubstring( const QString &string1, const QString &string2, bool caseSensitive )
     220                 :            : {
     221                 :          0 :   if ( string1.isEmpty() || string2.isEmpty() )
     222                 :            :   {
     223                 :            :     //empty strings, solution is trivial...
     224                 :          0 :     return QString();
     225                 :            :   }
     226                 :            : 
     227                 :            :   //handle case sensitive flag (or not)
     228                 :          0 :   QString s1( caseSensitive ? string1 : string1.toLower() );
     229                 :          0 :   QString s2( caseSensitive ? string2 : string2.toLower() );
     230                 :            : 
     231                 :          0 :   if ( s1 == s2 )
     232                 :            :   {
     233                 :            :     //another trivial case, identical strings
     234                 :          0 :     return s1;
     235                 :            :   }
     236                 :            : 
     237                 :          0 :   int *currentScores = new int [ s2.length()];
     238                 :          0 :   int *previousScores = new int [ s2.length()];
     239                 :          0 :   int maxCommonLength = 0;
     240                 :          0 :   int lastMaxBeginIndex = 0;
     241                 :            : 
     242                 :          0 :   const QChar *s1Char = s1.constData();
     243                 :          0 :   const QChar *s2Char = s2.constData();
     244                 :          0 :   const QChar *s2Start = s2Char;
     245                 :            : 
     246                 :          0 :   for ( int i = 0; i < s1.length(); ++i )
     247                 :            :   {
     248                 :          0 :     for ( int j = 0; j < s2.length(); ++j )
     249                 :            :     {
     250                 :          0 :       if ( *s1Char != *s2Char )
     251                 :            :       {
     252                 :          0 :         currentScores[j] = 0;
     253                 :          0 :       }
     254                 :            :       else
     255                 :            :       {
     256                 :          0 :         if ( i == 0 || j == 0 )
     257                 :            :         {
     258                 :          0 :           currentScores[j] = 1;
     259                 :          0 :         }
     260                 :            :         else
     261                 :            :         {
     262                 :          0 :           currentScores[j] = 1 + previousScores[j - 1];
     263                 :            :         }
     264                 :            : 
     265                 :          0 :         if ( maxCommonLength < currentScores[j] )
     266                 :            :         {
     267                 :          0 :           maxCommonLength = currentScores[j];
     268                 :          0 :           lastMaxBeginIndex = i;
     269                 :          0 :         }
     270                 :            :       }
     271                 :          0 :       s2Char++;
     272                 :          0 :     }
     273                 :          0 :     std::swap( currentScores, previousScores );
     274                 :          0 :     s1Char++;
     275                 :          0 :     s2Char = s2Start;
     276                 :          0 :   }
     277                 :          0 :   delete [] currentScores;
     278                 :          0 :   delete [] previousScores;
     279                 :          0 :   return string1.mid( lastMaxBeginIndex - maxCommonLength + 1, maxCommonLength );
     280                 :          0 : }
     281                 :            : 
     282                 :          0 : int QgsStringUtils::hammingDistance( const QString &string1, const QString &string2, bool caseSensitive )
     283                 :            : {
     284                 :          0 :   if ( string1.isEmpty() && string2.isEmpty() )
     285                 :            :   {
     286                 :            :     //empty strings, solution is trivial...
     287                 :          0 :     return 0;
     288                 :            :   }
     289                 :            : 
     290                 :          0 :   if ( string1.length() != string2.length() )
     291                 :            :   {
     292                 :            :     //invalid inputs
     293                 :          0 :     return -1;
     294                 :            :   }
     295                 :            : 
     296                 :            :   //handle case sensitive flag (or not)
     297                 :          0 :   QString s1( caseSensitive ? string1 : string1.toLower() );
     298                 :          0 :   QString s2( caseSensitive ? string2 : string2.toLower() );
     299                 :            : 
     300                 :          0 :   if ( s1 == s2 )
     301                 :            :   {
     302                 :            :     //another trivial case, identical strings
     303                 :          0 :     return 0;
     304                 :            :   }
     305                 :            : 
     306                 :          0 :   int distance = 0;
     307                 :          0 :   const QChar *s1Char = s1.constData();
     308                 :          0 :   const QChar *s2Char = s2.constData();
     309                 :            : 
     310                 :          0 :   for ( int i = 0; i < string1.length(); ++i )
     311                 :            :   {
     312                 :          0 :     if ( *s1Char != *s2Char )
     313                 :          0 :       distance++;
     314                 :          0 :     s1Char++;
     315                 :          0 :     s2Char++;
     316                 :          0 :   }
     317                 :            : 
     318                 :          0 :   return distance;
     319                 :          0 : }
     320                 :            : 
     321                 :          0 : QString QgsStringUtils::soundex( const QString &string )
     322                 :            : {
     323                 :          0 :   if ( string.isEmpty() )
     324                 :          0 :     return QString();
     325                 :            : 
     326                 :          0 :   QString tmp = string.toUpper();
     327                 :            : 
     328                 :            :   //strip non character codes, and vowel like characters after the first character
     329                 :          0 :   QChar *char1 = tmp.data();
     330                 :          0 :   QChar *char2 = tmp.data();
     331                 :          0 :   int outLen = 0;
     332                 :          0 :   for ( int i = 0; i < tmp.length(); ++i, ++char2 )
     333                 :            :   {
     334                 :          0 :     if ( ( *char2 ).unicode() >= 0x41 && ( *char2 ).unicode() <= 0x5A && ( i == 0 || ( ( *char2 ).unicode() != 0x41 && ( *char2 ).unicode() != 0x45
     335                 :          0 :          && ( *char2 ).unicode() != 0x48 && ( *char2 ).unicode() != 0x49
     336                 :          0 :          && ( *char2 ).unicode() != 0x4F && ( *char2 ).unicode() != 0x55
     337                 :          0 :          && ( *char2 ).unicode() != 0x57 && ( *char2 ).unicode() != 0x59 ) ) )
     338                 :            :     {
     339                 :          0 :       *char1 = *char2;
     340                 :          0 :       char1++;
     341                 :          0 :       outLen++;
     342                 :          0 :     }
     343                 :          0 :   }
     344                 :          0 :   tmp.truncate( outLen );
     345                 :            : 
     346                 :          0 :   QChar *tmpChar = tmp.data();
     347                 :          0 :   tmpChar++;
     348                 :          0 :   for ( int i = 1; i < tmp.length(); ++i, ++tmpChar )
     349                 :            :   {
     350                 :          0 :     switch ( ( *tmpChar ).unicode() )
     351                 :            :     {
     352                 :            :       case 0x42:
     353                 :            :       case 0x46:
     354                 :            :       case 0x50:
     355                 :            :       case 0x56:
     356                 :          0 :         tmp.replace( i, 1, QChar( 0x31 ) );
     357                 :          0 :         break;
     358                 :            : 
     359                 :            :       case 0x43:
     360                 :            :       case 0x47:
     361                 :            :       case 0x4A:
     362                 :            :       case 0x4B:
     363                 :            :       case 0x51:
     364                 :            :       case 0x53:
     365                 :            :       case 0x58:
     366                 :            :       case 0x5A:
     367                 :          0 :         tmp.replace( i, 1, QChar( 0x32 ) );
     368                 :          0 :         break;
     369                 :            : 
     370                 :            :       case 0x44:
     371                 :            :       case 0x54:
     372                 :          0 :         tmp.replace( i, 1, QChar( 0x33 ) );
     373                 :          0 :         break;
     374                 :            : 
     375                 :            :       case 0x4C:
     376                 :          0 :         tmp.replace( i, 1, QChar( 0x34 ) );
     377                 :          0 :         break;
     378                 :            : 
     379                 :            :       case 0x4D:
     380                 :            :       case 0x4E:
     381                 :          0 :         tmp.replace( i, 1, QChar( 0x35 ) );
     382                 :          0 :         break;
     383                 :            : 
     384                 :            :       case 0x52:
     385                 :          0 :         tmp.replace( i, 1, QChar( 0x36 ) );
     386                 :          0 :         break;
     387                 :            :     }
     388                 :          0 :   }
     389                 :            : 
     390                 :            :   //remove adjacent duplicates
     391                 :          0 :   char1 = tmp.data();
     392                 :          0 :   char2 = tmp.data();
     393                 :          0 :   char2++;
     394                 :          0 :   outLen = 1;
     395                 :          0 :   for ( int i = 1; i < tmp.length(); ++i, ++char2 )
     396                 :            :   {
     397                 :          0 :     if ( *char2 != *char1 )
     398                 :            :     {
     399                 :          0 :       char1++;
     400                 :          0 :       *char1 = *char2;
     401                 :          0 :       outLen++;
     402                 :          0 :       if ( outLen == 4 )
     403                 :          0 :         break;
     404                 :          0 :     }
     405                 :          0 :   }
     406                 :          0 :   tmp.truncate( outLen );
     407                 :          0 :   if ( tmp.length() < 4 )
     408                 :            :   {
     409                 :          0 :     tmp.append( "000" );
     410                 :          0 :     tmp.truncate( 4 );
     411                 :          0 :   }
     412                 :            : 
     413                 :          0 :   return tmp;
     414                 :          0 : }
     415                 :            : 
     416                 :            : 
     417                 :          0 : double QgsStringUtils::fuzzyScore( const QString &candidate, const QString &search )
     418                 :            : {
     419                 :          0 :   QString candidateNormalized = candidate.simplified().normalized( QString:: NormalizationForm_C ).toLower();
     420                 :          0 :   QString searchNormalized = search.simplified().normalized( QString:: NormalizationForm_C ).toLower();
     421                 :            : 
     422                 :          0 :   int candidateLength = candidateNormalized.length();
     423                 :          0 :   int searchLength = searchNormalized.length();
     424                 :          0 :   int score = 0;
     425                 :            : 
     426                 :            :   // if the candidate and the search term are empty, no other option than 0 score
     427                 :          0 :   if ( candidateLength == 0 || searchLength == 0 )
     428                 :          0 :     return score;
     429                 :            : 
     430                 :          0 :   int candidateIdx = 0;
     431                 :          0 :   int searchIdx = 0;
     432                 :            :   // there is always at least one word
     433                 :          0 :   int maxScore = FUZZY_SCORE_WORD_MATCH;
     434                 :            : 
     435                 :          0 :   bool isPreviousIndexMatching = false;
     436                 :          0 :   bool isWordOpen = true;
     437                 :            : 
     438                 :            :   // loop trough each candidate char and calculate the potential max score
     439                 :          0 :   while ( candidateIdx < candidateLength )
     440                 :            :   {
     441                 :          0 :     QChar candidateChar = candidateNormalized[ candidateIdx++ ];
     442                 :          0 :     bool isCandidateCharWordEnd = candidateChar == ' ' || candidateChar.isPunct();
     443                 :            : 
     444                 :            :     // the first char is always the default score
     445                 :          0 :     if ( candidateIdx == 1 )
     446                 :          0 :       maxScore += FUZZY_SCORE_NEW_MATCH;
     447                 :            :     // every space character or underscore is a opportunity for a new word
     448                 :          0 :     else if ( isCandidateCharWordEnd )
     449                 :          0 :       maxScore += FUZZY_SCORE_WORD_MATCH;
     450                 :            :     // potentially we can match every other character
     451                 :            :     else
     452                 :          0 :       maxScore += FUZZY_SCORE_CONSECUTIVE_MATCH;
     453                 :            : 
     454                 :            :     // we looped through all the characters
     455                 :          0 :     if ( searchIdx >= searchLength )
     456                 :          0 :       continue;
     457                 :            : 
     458                 :          0 :     QChar searchChar = searchNormalized[ searchIdx ];
     459                 :          0 :     bool isSearchCharWordEnd = searchChar == ' ' || searchChar.isPunct();
     460                 :            : 
     461                 :            :     // match!
     462                 :          0 :     if ( candidateChar == searchChar || ( isCandidateCharWordEnd && isSearchCharWordEnd ) )
     463                 :            :     {
     464                 :          0 :       searchIdx++;
     465                 :            : 
     466                 :            :       // if we have just successfully finished a word, give higher score
     467                 :          0 :       if ( isSearchCharWordEnd )
     468                 :            :       {
     469                 :          0 :         if ( isWordOpen )
     470                 :          0 :           score += FUZZY_SCORE_WORD_MATCH;
     471                 :          0 :         else if ( isPreviousIndexMatching )
     472                 :          0 :           score += FUZZY_SCORE_CONSECUTIVE_MATCH;
     473                 :            :         else
     474                 :          0 :           score += FUZZY_SCORE_NEW_MATCH;
     475                 :            : 
     476                 :          0 :         isWordOpen = true;
     477                 :          0 :       }
     478                 :            :       // if we have consecutive characters matching, give higher score
     479                 :          0 :       else if ( isPreviousIndexMatching )
     480                 :            :       {
     481                 :          0 :         score += FUZZY_SCORE_CONSECUTIVE_MATCH;
     482                 :          0 :       }
     483                 :            :       // normal score for new independent character that matches
     484                 :            :       else
     485                 :            :       {
     486                 :          0 :         score += FUZZY_SCORE_NEW_MATCH;
     487                 :            :       }
     488                 :            : 
     489                 :          0 :       isPreviousIndexMatching = true;
     490                 :          0 :     }
     491                 :            :     // if the current character does NOT match, we are sure we cannot build a word for now
     492                 :            :     else
     493                 :            :     {
     494                 :          0 :       isPreviousIndexMatching = false;
     495                 :          0 :       isWordOpen = false;
     496                 :            :     }
     497                 :            : 
     498                 :            :     // if the search string is covered, check if the last match is end of word
     499                 :          0 :     if ( searchIdx >= searchLength )
     500                 :            :     {
     501                 :          0 :       bool isEndOfWord = ( candidateIdx >= candidateLength )
     502                 :            :                          ? true
     503                 :          0 :                          : candidateNormalized[candidateIdx] == ' ' || candidateNormalized[candidateIdx].isPunct();
     504                 :            : 
     505                 :          0 :       if ( isEndOfWord )
     506                 :          0 :         score += FUZZY_SCORE_WORD_MATCH;
     507                 :          0 :     }
     508                 :            : 
     509                 :            :     // QgsLogger::debug( QStringLiteral( "TMP: %1 | %2 | %3 | %4 | %5" ).arg( candidateChar, searchChar, QString::number(score), QString::number(isCandidateCharWordEnd), QString::number(isSearchCharWordEnd) ) + QStringLiteral( __FILE__ ) );
     510                 :            :   }
     511                 :            : 
     512                 :            :   // QgsLogger::debug( QStringLiteral( "RES: %1 | %2" ).arg( QString::number(maxScore),  QString::number(score) ) + QStringLiteral( __FILE__ ) );
     513                 :            :   // we didn't loop through all the search chars, it means, that they are not present in the current candidate
     514                 :          0 :   if ( searchIdx < searchLength )
     515                 :          0 :     score = 0;
     516                 :            : 
     517                 :          0 :   return static_cast<float>( std::max( score, 0 ) ) / std::max( maxScore, 1 );
     518                 :          0 : }
     519                 :            : 
     520                 :            : 
     521                 :          0 : QString QgsStringUtils::insertLinks( const QString &string, bool *foundLinks )
     522                 :            : {
     523                 :          0 :   QString converted = string;
     524                 :            : 
     525                 :            :   // http://alanstorm.com/url_regex_explained
     526                 :            :   // note - there's more robust implementations available, but we need one which works within the limitation of QRegExp
     527                 :          0 :   static QRegExp urlRegEx( "(\\b(([\\w-]+://?|www[.])[^\\s()<>]+(?:\\([\\w\\d]+\\)|([^!\"#$%&'()*+,\\-./:;<=>?@[\\\\\\]^_`{|}~\\s]|/))))" );
     528                 :          0 :   static QRegExp protoRegEx( "^(?:f|ht)tps?://|file://" );
     529                 :          0 :   static QRegExp emailRegEx( "([\\w._%+-]+@[\\w.-]+\\.[A-Za-z]+)" );
     530                 :            : 
     531                 :          0 :   int offset = 0;
     532                 :          0 :   bool found = false;
     533                 :          0 :   while ( urlRegEx.indexIn( converted, offset ) != -1 )
     534                 :            :   {
     535                 :          0 :     found = true;
     536                 :          0 :     QString url = urlRegEx.cap( 1 );
     537                 :          0 :     QString protoUrl = url;
     538                 :          0 :     if ( protoRegEx.indexIn( protoUrl ) == -1 )
     539                 :            :     {
     540                 :          0 :       protoUrl.prepend( "http://" );
     541                 :          0 :     }
     542                 :          0 :     QString anchor = QStringLiteral( "<a href=\"%1\">%2</a>" ).arg( protoUrl.toHtmlEscaped(), url.toHtmlEscaped() );
     543                 :          0 :     converted.replace( urlRegEx.pos( 1 ), url.length(), anchor );
     544                 :          0 :     offset = urlRegEx.pos( 1 ) + anchor.length();
     545                 :          0 :   }
     546                 :          0 :   offset = 0;
     547                 :          0 :   while ( emailRegEx.indexIn( converted, offset ) != -1 )
     548                 :            :   {
     549                 :          0 :     found = true;
     550                 :          0 :     QString email = emailRegEx.cap( 1 );
     551                 :          0 :     QString anchor = QStringLiteral( "<a href=\"mailto:%1\">%1</a>" ).arg( email.toHtmlEscaped() );
     552                 :          0 :     converted.replace( emailRegEx.pos( 1 ), email.length(), anchor );
     553                 :          0 :     offset = emailRegEx.pos( 1 ) + anchor.length();
     554                 :          0 :   }
     555                 :            : 
     556                 :          0 :   if ( foundLinks )
     557                 :          0 :     *foundLinks = found;
     558                 :            : 
     559                 :          0 :   return converted;
     560                 :          0 : }
     561                 :            : 
     562                 :          0 : QString QgsStringUtils::htmlToMarkdown( const QString &html )
     563                 :            : {
     564                 :            :   // Any changes in this function must be copied to qgscrashreport.cpp too
     565                 :          0 :   QString converted = html;
     566                 :          0 :   converted.replace( QLatin1String( "<br>" ), QLatin1String( "\n" ) );
     567                 :          0 :   converted.replace( QLatin1String( "<b>" ), QLatin1String( "**" ) );
     568                 :          0 :   converted.replace( QLatin1String( "</b>" ), QLatin1String( "**" ) );
     569                 :            : 
     570                 :          0 :   static QRegExp hrefRegEx( "<a\\s+href\\s*=\\s*([^<>]*)\\s*>([^<>]*)</a>" );
     571                 :          0 :   int offset = 0;
     572                 :          0 :   while ( hrefRegEx.indexIn( converted, offset ) != -1 )
     573                 :            :   {
     574                 :          0 :     QString url = hrefRegEx.cap( 1 ).replace( QLatin1String( "\"" ), QString() );
     575                 :          0 :     url.replace( '\'', QString() );
     576                 :          0 :     QString name = hrefRegEx.cap( 2 );
     577                 :          0 :     QString anchor = QStringLiteral( "[%1](%2)" ).arg( name, url );
     578                 :          0 :     converted.replace( hrefRegEx, anchor );
     579                 :          0 :     offset = hrefRegEx.pos( 1 ) + anchor.length();
     580                 :          0 :   }
     581                 :            : 
     582                 :          0 :   return converted;
     583                 :          0 : }
     584                 :            : 
     585                 :          0 : QString QgsStringUtils::wordWrap( const QString &string, const int length, const bool useMaxLineLength, const QString &customDelimiter )
     586                 :            : {
     587                 :          0 :   if ( string.isEmpty() || length == 0 )
     588                 :          0 :     return string;
     589                 :            : 
     590                 :          0 :   QString newstr;
     591                 :          0 :   QRegExp rx;
     592                 :          0 :   int delimiterLength = 0;
     593                 :            : 
     594                 :          0 :   if ( !customDelimiter.isEmpty() )
     595                 :            :   {
     596                 :          0 :     rx.setPatternSyntax( QRegExp::FixedString );
     597                 :          0 :     rx.setPattern( customDelimiter );
     598                 :          0 :     delimiterLength = customDelimiter.length();
     599                 :          0 :   }
     600                 :            :   else
     601                 :            :   {
     602                 :            :     // \x200B is a ZERO-WIDTH SPACE, needed for worwrap to support a number of complex scripts (Indic, Arabic, etc.)
     603                 :          0 :     rx.setPattern( QStringLiteral( "[\\s\\x200B]" ) );
     604                 :          0 :     delimiterLength = 1;
     605                 :            :   }
     606                 :            : 
     607                 :          0 :   const QStringList lines = string.split( '\n' );
     608                 :            :   int strLength, strCurrent, strHit, lastHit;
     609                 :            : 
     610                 :          0 :   for ( int i = 0; i < lines.size(); i++ )
     611                 :            :   {
     612                 :          0 :     strLength = lines.at( i ).length();
     613                 :          0 :     strCurrent = 0;
     614                 :          0 :     strHit = 0;
     615                 :          0 :     lastHit = 0;
     616                 :            : 
     617                 :          0 :     while ( strCurrent < strLength )
     618                 :            :     {
     619                 :            :       // positive wrap value = desired maximum line width to wrap
     620                 :            :       // negative wrap value = desired minimum line width before wrap
     621                 :          0 :       if ( useMaxLineLength )
     622                 :            :       {
     623                 :            :         //first try to locate delimiter backwards
     624                 :          0 :         strHit = lines.at( i ).lastIndexOf( rx, strCurrent + length );
     625                 :          0 :         if ( strHit == lastHit || strHit == -1 )
     626                 :            :         {
     627                 :            :           //if no new backward delimiter found, try to locate forward
     628                 :          0 :           strHit = lines.at( i ).indexOf( rx, strCurrent + std::abs( length ) );
     629                 :          0 :         }
     630                 :          0 :         lastHit = strHit;
     631                 :          0 :       }
     632                 :            :       else
     633                 :            :       {
     634                 :          0 :         strHit = lines.at( i ).indexOf( rx, strCurrent + std::abs( length ) );
     635                 :            :       }
     636                 :          0 :       if ( strHit > -1 )
     637                 :            :       {
     638                 :            : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
     639                 :            :         newstr.append( lines.at( i ).midRef( strCurrent, strHit - strCurrent ) );
     640                 :            : #else
     641                 :          0 :         newstr.append( QStringView {lines.at( i )}.mid( strCurrent, strHit - strCurrent ) );
     642                 :            : #endif
     643                 :          0 :         newstr.append( '\n' );
     644                 :          0 :         strCurrent = strHit + delimiterLength;
     645                 :          0 :       }
     646                 :            :       else
     647                 :            :       {
     648                 :            : #if QT_VERSION < QT_VERSION_CHECK(5, 15, 2)
     649                 :            :         newstr.append( lines.at( i ).midRef( strCurrent ) );
     650                 :            : #else
     651                 :          0 :         newstr.append( QStringView {lines.at( i )}.mid( strCurrent ) );
     652                 :            : #endif
     653                 :          0 :         strCurrent = strLength;
     654                 :            :       }
     655                 :            :     }
     656                 :          0 :     if ( i < lines.size() - 1 )
     657                 :          0 :       newstr.append( '\n' );
     658                 :          0 :   }
     659                 :            : 
     660                 :          0 :   return newstr;
     661                 :          0 : }
     662                 :            : 
     663                 :          0 : QString QgsStringUtils::substituteVerticalCharacters( QString string )
     664                 :            : {
     665                 :          0 :   string = string.replace( ',', QChar( 65040 ) ).replace( QChar( 8229 ), QChar( 65072 ) ); // comma & two-dot leader
     666                 :          0 :   string = string.replace( QChar( 12289 ), QChar( 65041 ) ).replace( QChar( 12290 ), QChar( 65042 ) ); // ideographic comma & full stop
     667                 :          0 :   string = string.replace( ':', QChar( 65043 ) ).replace( ';', QChar( 65044 ) );
     668                 :          0 :   string = string.replace( '!', QChar( 65045 ) ).replace( '?', QChar( 65046 ) );
     669                 :          0 :   string = string.replace( QChar( 12310 ), QChar( 65047 ) ).replace( QChar( 12311 ), QChar( 65048 ) ); // white lenticular brackets
     670                 :          0 :   string = string.replace( QChar( 8230 ), QChar( 65049 ) ); // three-dot ellipse
     671                 :          0 :   string = string.replace( QChar( 8212 ), QChar( 65073 ) ).replace( QChar( 8211 ), QChar( 65074 ) ); // em & en dash
     672                 :          0 :   string = string.replace( '_', QChar( 65075 ) ).replace( QChar( 65103 ), QChar( 65076 ) ); // low line & wavy low line
     673                 :          0 :   string = string.replace( '(', QChar( 65077 ) ).replace( ')', QChar( 65078 ) );
     674                 :          0 :   string = string.replace( '{', QChar( 65079 ) ).replace( '}', QChar( 65080 ) );
     675                 :          0 :   string = string.replace( '<', QChar( 65087 ) ).replace( '>', QChar( 65088 ) );
     676                 :          0 :   string = string.replace( '[', QChar( 65095 ) ).replace( ']', QChar( 65096 ) );
     677                 :          0 :   string = string.replace( QChar( 12308 ), QChar( 65081 ) ).replace( QChar( 12309 ), QChar( 65082 ) );   // tortoise shell brackets
     678                 :          0 :   string = string.replace( QChar( 12304 ), QChar( 65083 ) ).replace( QChar( 12305 ), QChar( 65084 ) );   // black lenticular brackets
     679                 :          0 :   string = string.replace( QChar( 12298 ), QChar( 65085 ) ).replace( QChar( 12299 ), QChar( 65086 ) ); // double angle brackets
     680                 :          0 :   string = string.replace( QChar( 12300 ), QChar( 65089 ) ).replace( QChar( 12301 ), QChar( 65090 ) );   // corner brackets
     681                 :          0 :   string = string.replace( QChar( 12302 ), QChar( 65091 ) ).replace( QChar( 12303 ), QChar( 65092 ) );   // white corner brackets
     682                 :          0 :   return string;
     683                 :            : }
     684                 :            : 
     685                 :          0 : QgsStringReplacement::QgsStringReplacement( const QString &match, const QString &replacement, bool caseSensitive, bool wholeWordOnly )
     686                 :          0 :   : mMatch( match )
     687                 :          0 :   , mReplacement( replacement )
     688                 :          0 :   , mCaseSensitive( caseSensitive )
     689                 :          0 :   , mWholeWordOnly( wholeWordOnly )
     690                 :            : {
     691                 :          0 :   if ( mWholeWordOnly )
     692                 :          0 :     mRx = QRegExp( QString( "\\b%1\\b" ).arg( mMatch ),
     693                 :          0 :                    mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
     694                 :          0 : }
     695                 :            : 
     696                 :          0 : QString QgsStringReplacement::process( const QString &input ) const
     697                 :            : {
     698                 :          0 :   QString result = input;
     699                 :          0 :   if ( !mWholeWordOnly )
     700                 :            :   {
     701                 :          0 :     return result.replace( mMatch, mReplacement, mCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive );
     702                 :            :   }
     703                 :            :   else
     704                 :            :   {
     705                 :          0 :     return result.replace( mRx, mReplacement );
     706                 :            :   }
     707                 :          0 : }
     708                 :            : 
     709                 :          0 : QgsStringMap QgsStringReplacement::properties() const
     710                 :            : {
     711                 :          0 :   QgsStringMap map;
     712                 :          0 :   map.insert( QStringLiteral( "match" ), mMatch );
     713                 :          0 :   map.insert( QStringLiteral( "replace" ), mReplacement );
     714                 :          0 :   map.insert( QStringLiteral( "caseSensitive" ), mCaseSensitive ? "1" : "0" );
     715                 :          0 :   map.insert( QStringLiteral( "wholeWord" ), mWholeWordOnly ? "1" : "0" );
     716                 :          0 :   return map;
     717                 :          0 : }
     718                 :            : 
     719                 :          0 : QgsStringReplacement QgsStringReplacement::fromProperties( const QgsStringMap &properties )
     720                 :            : {
     721                 :          0 :   return QgsStringReplacement( properties.value( QStringLiteral( "match" ) ),
     722                 :          0 :                                properties.value( QStringLiteral( "replace" ) ),
     723                 :          0 :                                properties.value( QStringLiteral( "caseSensitive" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ),
     724                 :          0 :                                properties.value( QStringLiteral( "wholeWord" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) );
     725                 :          0 : }
     726                 :            : 
     727                 :          0 : QString QgsStringReplacementCollection::process( const QString &input ) const
     728                 :            : {
     729                 :          0 :   QString result = input;
     730                 :          0 :   const auto constMReplacements = mReplacements;
     731                 :          0 :   for ( const QgsStringReplacement &r : constMReplacements )
     732                 :            :   {
     733                 :          0 :     result = r.process( result );
     734                 :            :   }
     735                 :          0 :   return result;
     736                 :          0 : }
     737                 :            : 
     738                 :          0 : void QgsStringReplacementCollection::writeXml( QDomElement &elem, QDomDocument &doc ) const
     739                 :            : {
     740                 :          0 :   const auto constMReplacements = mReplacements;
     741                 :          0 :   for ( const QgsStringReplacement &r : constMReplacements )
     742                 :            :   {
     743                 :          0 :     QgsStringMap props = r.properties();
     744                 :          0 :     QDomElement propEl = doc.createElement( QStringLiteral( "replacement" ) );
     745                 :          0 :     QgsStringMap::const_iterator it = props.constBegin();
     746                 :          0 :     for ( ; it != props.constEnd(); ++it )
     747                 :            :     {
     748                 :          0 :       propEl.setAttribute( it.key(), it.value() );
     749                 :          0 :     }
     750                 :          0 :     elem.appendChild( propEl );
     751                 :          0 :   }
     752                 :          0 : }
     753                 :            : 
     754                 :          0 : void QgsStringReplacementCollection::readXml( const QDomElement &elem )
     755                 :            : {
     756                 :          0 :   mReplacements.clear();
     757                 :          0 :   QDomNodeList nodelist = elem.elementsByTagName( QStringLiteral( "replacement" ) );
     758                 :          0 :   for ( int i = 0; i < nodelist.count(); i++ )
     759                 :            :   {
     760                 :          0 :     QDomElement replacementElem = nodelist.at( i ).toElement();
     761                 :          0 :     QDomNamedNodeMap nodeMap = replacementElem.attributes();
     762                 :            : 
     763                 :          0 :     QgsStringMap props;
     764                 :          0 :     for ( int j = 0; j < nodeMap.count(); ++j )
     765                 :            :     {
     766                 :          0 :       props.insert( nodeMap.item( j ).nodeName(), nodeMap.item( j ).nodeValue() );
     767                 :          0 :     }
     768                 :          0 :     mReplacements << QgsStringReplacement::fromProperties( props );
     769                 :          0 :   }
     770                 :            : 
     771                 :          0 : }

Generated by: LCOV version 1.14