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