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