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