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