swift
stringutils.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2015 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include "misc/stringutils.h"
7 
8 #include <any>
9 
10 #include <QChar>
11 #include <QRegularExpression>
12 #include <QStringBuilder>
13 
14 namespace swift::misc
15 {
16  QString removeDateTimeSeparators(const QString &s)
17  {
18  return removeChars(s, [](QChar c) { return c == u' ' || c == u':' || c == u'_' || c == u'-' || c == u'.'; });
19  }
20 
21  QList<QStringView> splitLinesRefs(const QString &s)
22  {
23  return splitStringRefs(s, [](QChar c) { return c == '\n' || c == '\r'; });
24  }
25 
26  QStringList splitLines(const QString &s)
27  {
28  return splitString(s, [](QChar c) { return c == '\n' || c == '\r'; });
29  }
30 
31  QByteArray utfToPercentEncoding(const QString &s, const QByteArray &allow, char percent)
32  {
33  QByteArray result;
34  for (const QChar &c : s)
35  {
36  if (const char latin = c.toLatin1())
37  {
38  if ((latin >= 'a' && latin <= 'z') || (latin >= 'A' && latin <= 'Z') ||
39  (latin >= '0' && latin <= '9') || allow.contains(latin))
40  {
41  result += c.toLatin1();
42  }
43  else
44  {
45  result += percent;
46  if (latin < 0x10) { result += '0'; }
47  result += QByteArray::number(static_cast<int>(latin), 16);
48  }
49  }
50  else
51  {
52  result += percent;
53  result += 'x';
54  const ushort unicode = c.unicode();
55  if (unicode < 0x0010) { result += '0'; }
56  if (unicode < 0x0100) { result += '0'; }
57  if (unicode < 0x1000) { result += '0'; }
58  result += QByteArray::number(unicode, 16);
59  }
60  }
61  return result;
62  }
63 
64  QString utfFromPercentEncoding(const QByteArray &ba, char percent)
65  {
66  QString result;
67  for (int i = 0; i < ba.size(); ++i)
68  {
69  if (ba[i] == percent)
70  {
71  ++i;
72  Q_ASSERT(i < ba.size());
73  if (ba[i] == 'x')
74  {
75  ++i;
76  Q_ASSERT(i < ba.size());
77  result += QChar(ba.mid(i, 4).toInt(nullptr, 16));
78  i += 3;
79  }
80  else
81  {
82  result += static_cast<char>(ba.mid(i, 2).toInt(nullptr, 16));
83  ++i;
84  }
85  }
86  else { result += ba[i]; }
87  }
88  return result;
89  }
90 
91  const QString &boolToOnOff(bool v)
92  {
93  static const QString on("on");
94  static const QString off("off");
95  return v ? on : off;
96  }
97 
98  const QString &boolToYesNo(bool v)
99  {
100  static const QString yes("yes");
101  static const QString no("no");
102  return v ? yes : no;
103  }
104 
105  const QString &boolToTrueFalse(bool v)
106  {
107  static const QString t("true");
108  static const QString f("false");
109  return v ? t : f;
110  }
111 
112  const QString &boolToEnabledDisabled(bool v)
113  {
114  static const QString e("enabled");
115  static const QString d("disabled");
116  return v ? e : d;
117  }
118 
119  const QString &boolToNullNotNull(bool isNull)
120  {
121  static const QString n("null");
122  static const QString nn("not null");
123  return isNull ? n : nn;
124  }
125 
126  bool stringToBool(const QString &string)
127  {
128  QString s(string.trimmed().toLower());
129  if (s.isEmpty()) { return false; }
130 
131  // 1 char values
132  const QChar c = s.at(0);
133  if (c == '1' || c == 't' || c == 'y' || c == 'x') { return true; }
134  if (c == '0' || c == 'f' || c == 'n' || c == '_') { return false; }
135 
136  if (c == 'e') { return true; } // enabled
137  if (c == 'd') { return false; } // disabled
138 
139  // full words
140  if (s == "on") { return true; }
141  return false;
142  }
143 
144  int fuzzyShortStringComparision(const QString &str1, const QString &str2, Qt::CaseSensitivity cs)
145  {
146  // same
147  if (cs == Qt::CaseInsensitive)
148  {
149  if (caseInsensitiveStringCompare(str1, str2)) { return 100; }
150  }
151  else if (str1 == str2) { return 100; }
152 
153  // one string is empty
154  if (str1.isEmpty() || str2.isEmpty()) { return 0; }
155 
156  // make sure aStr is not shorter
157  const QString aStr = str1.length() >= str2.length() ? str1 : str2;
158  const QString bStr = str1.length() >= str2.length() ? str2 : str1;
159 
160  // starts/ends with
161  const auto s1 = static_cast<double>(aStr.length());
162  const auto s2 = static_cast<double>(bStr.length());
163  if (aStr.endsWith(bStr, cs)) { return qRound(s1 / s2 * 100); }
164  if (aStr.startsWith(bStr, cs)) { return qRound(s1 / s2 * 100); }
165 
166  // contains
167  if (aStr.contains(bStr, cs)) { return qRound(s1 / s2 * 100); }
168 
169  // char by char
170  double points = 0;
171  for (int p = 0; p < aStr.length(); p++)
172  {
173  if (p < bStr.length() && aStr[p] == bStr[p])
174  {
175  points += 1.0;
176  continue;
177  }
178 
179  // char after
180  const int after = p + 1;
181  if (after < bStr.length() && aStr[p] == bStr[after])
182  {
183  points += 0.5;
184  continue;
185  }
186 
187  // char before
188  const int before = p - 1;
189  if (before >= 0 && before < bStr.length() && aStr[p] == bStr[before])
190  {
191  points += 0.5;
192  continue;
193  }
194  }
195  return qRound(points / s1 * 100);
196  }
197 
198  QString intToHex(int value, int digits)
199  {
200  const QString hex(QString::number(value, 16).toUpper());
201  const qsizetype l = hex.length();
202  if (l >= digits) { return hex.right(digits); }
203  const qsizetype d = digits - l;
204  return QString(d, '0') + hex;
205  }
206 
207  QString stripDesignatorFromCompleterString(const QString &candidate)
208  {
209  const QString s(candidate.trimmed().toUpper());
210  if (s.isEmpty()) { return {}; }
211  return s.contains(' ') ? s.left(s.indexOf(' ')) : s;
212  }
213 
214  // http://www.codegur.online/14009522/how-to-remove-accents-diacritic-marks-from-a-string-in-qt
215  // https://stackoverflow.com/questions/14009522/how-to-remove-accents-diacritic-marks-from-a-string-in-qt
216  // https://german.stackexchange.com/questions/4992/conversion-table-for-diacritics-e-g-%C3%BC-%E2%86%92-ue
217  QString simplifyAccents(const QString &candidate)
218  {
219  static const QString diacriticLetters =
220  QString::fromUtf8("ŠŒŽšœžŸ¥µÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýÿ");
221  static const QStringList noDiacriticLetters(
222  { "S", "OE", "Z", "s", "oe", "z", "Y", "Y", "u", "A", "A", "A", "A", "A", "A", "AE", "C", "E",
223  "E", "E", "E", "I", "I", "I", "I", "D", "N", "O", "O", "O", "O", "O", "O", "U", "U", "U",
224  "U", "Y", "s", "a", "a", "a", "a", "a", "a", "ae", "c", "e", "e", "e", "e", "i", "i", "i",
225  "i", "o", "n", "o", "o", "o", "o", "o", "o", "u", "u", "u", "u", "y", "y" });
226 
227  QString output = "";
228  for (int i = 0; i < candidate.length(); i++)
229  {
230  const QChar c = candidate[i];
231  const qsizetype dIndex = diacriticLetters.indexOf(c);
232  if (dIndex < 0) { output.append(c); }
233  else
234  {
235  const QString replacement = noDiacriticLetters[dIndex];
236  output.append(replacement);
237  }
238  }
239  return output;
240  }
241 
242  QString simplifyByDecomposition(const QString &s)
243  {
244  QString result;
245  // QChar c (NOT QChar &c), see
246  // https://discordapp.com/channels/539048679160676382/539925070550794240/686321311076581440
247  for (const QChar &c : s)
248  {
249  if (c.decompositionTag() == QChar::NoDecomposition) { result.push_back(c); }
250  else
251  {
252  for (const QChar &dc : c.decomposition())
253  {
254  if (!dc.isMark()) { result.push_back(dc); }
255  }
256  }
257  }
258  return result;
259  }
260 
261  bool caseInsensitiveStringCompare(const QString &c1, const QString &c2)
262  {
263  return c1.length() == c2.length() && c1.startsWith(c2, Qt::CaseInsensitive);
264  }
265 
266  QString simplifyNameForSearch(const QString &name)
267  {
268  return removeChars(name.toUpper(), [](QChar c) { return !c.isUpper(); });
269  }
270 
271  QDateTime fromStringUtc(const QString &dateTimeString, const QString &format)
272  {
273  if (dateTimeString.isEmpty() || format.isEmpty()) { return {}; }
274  QDateTime dt = QDateTime::fromString(dateTimeString, format);
275  if (!dt.isValid()) { return dt; }
276  dt.setOffsetFromUtc(0); // must only be applied to valid timestamps
277  return dt;
278  }
279 
280  QDateTime fromStringUtc(const QString &dateTimeString, Qt::DateFormat format)
281  {
282  if (dateTimeString.isEmpty()) { return {}; }
283  QDateTime dt = QDateTime::fromString(dateTimeString, format);
284  if (!dt.isValid()) { return dt; }
285  dt.setOffsetFromUtc(0); // must only be applied to valid timestamps
286  return dt;
287  }
288 
289  QDateTime fromStringUtc(const QString &dateTimeString, const QLocale &locale, QLocale::FormatType format)
290  {
291  if (dateTimeString.isEmpty()) { return {}; }
292  QDateTime dt = locale.toDateTime(dateTimeString, format);
293  if (!dt.isValid()) { return dt; }
294  dt.setOffsetFromUtc(0); // must only be applied to valid timestamps
295  return dt;
296  }
297 
298  QDateTime parseMultipleDateTimeFormats(const QString &dateTimeString)
299  {
300  if (dateTimeString.isEmpty()) { return {}; }
301  if (isDigitsOnlyString(dateTimeString))
302  {
303  // 2017 0301 124421 321
304  if (dateTimeString.length() == 17) { return fromStringUtc(dateTimeString, "yyyyMMddHHmmsszzz"); }
305  if (dateTimeString.length() == 14) { return fromStringUtc(dateTimeString, "yyyyMMddHHmmss"); }
306  if (dateTimeString.length() == 12) { return fromStringUtc(dateTimeString, "yyyyMMddHHmm"); }
307  if (dateTimeString.length() == 8) { return fromStringUtc(dateTimeString, "yyyyMMdd"); }
308  return {};
309  }
310 
311  // remove simple separators and check if digits only again
312  const QString simpleSeparatorsRemoved = removeDateTimeSeparators(dateTimeString);
313  if (isDigitsOnlyString(simpleSeparatorsRemoved))
314  {
315  return parseMultipleDateTimeFormats(simpleSeparatorsRemoved);
316  }
317 
318  // stupid trial and error
319  QDateTime ts = fromStringUtc(dateTimeString, Qt::ISODateWithMs);
320  if (ts.isValid()) return ts;
321 
322  ts = fromStringUtc(dateTimeString, Qt::ISODate);
323  if (ts.isValid()) return ts;
324 
325  ts = fromStringUtc(dateTimeString, Qt::TextDate);
326  if (ts.isValid()) return ts;
327 
328  ts = fromStringUtc(dateTimeString, QLocale(), QLocale::LongFormat);
329  if (ts.isValid()) return ts;
330 
331  ts = fromStringUtc(dateTimeString, QLocale(), QLocale::ShortFormat);
332  if (ts.isValid()) return ts;
333 
334  // SystemLocaleShortDate,
335  // SystemLocaleLongDate,
336  return {};
337  }
338 
339  QDateTime parseDateTimeStringOptimized(const QString &dateTimeString)
340  {
341  if (dateTimeString.length() < 8) { return {}; }
342 
343  // yyyyMMddHHmmsszzz
344  // 01234567890123456
345  int year(QStringView { dateTimeString }.left(4).toInt());
346  int month(QStringView { dateTimeString }.mid(4, 2).toInt());
347  int day(QStringView { dateTimeString }.mid(6, 2).toInt());
348  QDate date;
349  date.setDate(year, month, day);
350  QDateTime dt;
351  dt.setOffsetFromUtc(0);
352  dt.setDate(date);
353  if (dateTimeString.length() < 12) { return dt; }
354 
355  QTime t;
356  const int hour(QStringView { dateTimeString }.mid(8, 2).toInt());
357  const int minute(QStringView { dateTimeString }.mid(10, 2).toInt());
358  const int second(dateTimeString.length() < 14 ? 0 : QStringView { dateTimeString }.mid(12, 2).toInt());
359  const int ms(dateTimeString.length() < 17 ? 0 : QStringView { dateTimeString }.right(3).toInt());
360 
361  t.setHMS(hour, minute, second, ms);
362  dt.setTime(t);
363  return dt;
364  }
365 
366  QString dotToLocaleDecimalPoint(QString &input) { return input.replace('.', QLocale::system().decimalPoint()); }
367 
368  QString dotToLocaleDecimalPoint(const QString &input)
369  {
370  QString copy(input);
371  return copy.replace('.', QLocale::system().decimalPoint());
372  }
373 
374  bool stringCompare(const QString &c1, const QString &c2, Qt::CaseSensitivity cs)
375  {
376  if (cs == Qt::CaseSensitive) { return c1 == c2; }
377  return caseInsensitiveStringCompare(c1, c2);
378  }
379 
380  QString inApostrophes(const QString &in, bool ignoreEmpty)
381  {
382  if (in.isEmpty()) { return ignoreEmpty ? QString() : QStringLiteral("''"); }
383  return u'\'' % in % u'\'';
384  }
385 
386  QString inQuotes(const QString &in, bool ignoreEmpty)
387  {
388  if (in.isEmpty()) { return ignoreEmpty ? QString() : QStringLiteral("\"\""); }
389  return u'"' % in % u'"';
390  }
391 
392  QString withQuestionMark(const QString &question)
393  {
394  if (question.endsWith("?")) { return question; }
395  return question % u'?';
396  }
397 
398  qsizetype nthIndexOf(const QString &string, QChar ch, int nth, Qt::CaseSensitivity cs)
399  {
400  if (nth < 1 || string.isEmpty() || nth > string.length()) { return -1; }
401 
402  qsizetype from = 0;
403  qsizetype ci = -1;
404  for (int t = 0; t < nth; ++t)
405  {
406  ci = string.indexOf(ch, from, cs);
407  if (ci < 0) { return -1; }
408  from = ci + 1;
409  if (from >= string.length()) { return -1; }
410  }
411  return ci;
412  }
413 
414  QString joinStringSet(const QSet<QString> &set, const QString &separator)
415  {
416  if (set.isEmpty()) { return {}; }
417  if (set.size() == 1) { return *set.begin(); }
418  return set.values().join(separator);
419  }
420 
421  QMap<QString, QString> parseIniValues(const QString &data)
422  {
424  QList<QStringView> lines = splitLinesRefs(data);
425  for (const QStringView l : lines)
426  {
427  if (l.isEmpty()) { continue; }
428  const qsizetype i = l.indexOf('=');
429  if (i < 0 || i >= l.length() + 1) { continue; }
430 
431  const QString key = l.left(i).trimmed().toString();
432  const QString value = l.mid(i + 1).toString();
433  if (value.isEmpty()) { continue; }
434  map.insert(key, value);
435  }
436  return map;
437  }
438 
439  QString removeSurroundingApostrophes(const QString &in)
440  {
441  if (in.size() < 2) { return in; }
442  if (in.startsWith("'") && in.endsWith("'")) { return in.mid(1, in.length() - 2); }
443  return in;
444  }
445 
446  QString removeSurroundingQuotes(const QString &in)
447  {
448  if (in.size() < 2) { return in; }
449  if (in.startsWith("\"") && in.endsWith("\"")) { return in.mid(1, in.length() - 2); }
450  return in;
451  }
452 
453  QString removeComments(const QString &in, bool removeSlashStar, bool removeDoubleSlash)
454  {
455  QString copy(in);
456 
457  thread_local const QRegularExpression re1(R"(\/\*(.|\n)*?\*\/)");
458  if (removeSlashStar) { copy.remove(re1); }
459 
460  thread_local const QRegularExpression re2("\\/\\/.*");
461  if (removeDoubleSlash) { copy.remove(re2); }
462 
463  return copy;
464  }
465 
466  bool containsAny(const QString &testString, const QStringList &any, Qt::CaseSensitivity cs)
467  {
468  if (testString.isEmpty() || any.isEmpty()) { return false; }
469  return std::any_of(any.begin(), any.end(), [&](const QString &a) { return testString.contains(a, cs); });
470  }
471 
472  bool hasBalancedQuotes(const QString &in, char quote)
473  {
474  if (in.isEmpty()) { return true; }
475  const qsizetype c = in.count(quote);
476  return (c % 2) == 0;
477  }
478 
479  double parseFraction(const QString &fraction, double failDefault)
480  {
481  if (fraction.isEmpty()) { return failDefault; }
482  bool ok {};
483 
484  double r = failDefault;
485  if (fraction.contains('/'))
486  {
487  const QStringList parts = fraction.split('/');
488  if (parts.size() != 2) { return failDefault; }
489  const double c = parts.front().trimmed().toDouble(&ok);
490  if (!ok) { return failDefault; }
491 
492  const double d = parts.last().trimmed().toDouble(&ok);
493  if (!ok) { return failDefault; }
494  if (qFuzzyCompare(0.0, d)) { return failDefault; }
495  r = c / d;
496  }
497  else
498  {
499  r = fraction.trimmed().toDouble(&ok);
500  if (!ok) { return failDefault; }
501  }
502  return r;
503  }
504 
505  QString cleanNumber(const QString &number)
506  {
507  QString n = number.trimmed();
508  if (n.isEmpty()) { return {}; }
509 
510  qsizetype dp = n.indexOf('.');
511  if (dp < 0) { dp = n.indexOf(','); }
512 
513  // clean all trailing stuff
514  while (dp >= 0 && !n.isEmpty())
515  {
516  const QChar l = n.at(n.size() - 1);
517  if (l == '0')
518  {
519  n.chop(1);
520  continue;
521  }
522  if (l == '.' || l == ',') { n.chop(1); }
523  break;
524  }
525 
526  while (n.startsWith("00")) { n.remove(0, 1); }
527 
528  return n;
529  }
530 
531 } // namespace swift::misc
532 
Free functions in swift::misc.
SWIFT_MISC_EXPORT QString withQuestionMark(const QString &question)
Add a question mark at the end if not existing.
SWIFT_MISC_EXPORT QString simplifyNameForSearch(const QString &name)
Get a simplified upper case name for searching by removing all characters except A-Z.
SWIFT_MISC_EXPORT qsizetype nthIndexOf(const QString &string, QChar ch, int nth=1, Qt::CaseSensitivity cs=Qt::CaseInsensitive)
nth index of ch
SWIFT_MISC_EXPORT QString cleanNumber(const QString &number)
Remove leading 0, trailing 0, " ", and "." from a number.
SWIFT_MISC_EXPORT QString inApostrophes(const QString &in, bool ignoreEmpty=false)
Return string in apostrophes.
SWIFT_MISC_EXPORT QString intToHex(int value, int digits=2)
Int to hex value.
SWIFT_MISC_EXPORT QByteArray utfToPercentEncoding(const QString &s, const QByteArray &allow={}, char percent='%')
Extended percent encoding supporting UTF-16.
SWIFT_MISC_EXPORT QString stripDesignatorFromCompleterString(const QString &candidate)
Strip a designator from a combined string.
SWIFT_MISC_EXPORT QDateTime parseMultipleDateTimeFormats(const QString &dateTimeString)
Parse multiple date time formats.
SWIFT_MISC_EXPORT QString utfFromPercentEncoding(const QByteArray &ba, char percent='%')
Reverse utfFromPercentEncoding.
SWIFT_MISC_EXPORT int fuzzyShortStringComparision(const QString &str1, const QString &str2, Qt::CaseSensitivity cs=Qt::CaseSensitive)
Fuzzy compare for short strings (like ICAO designators)
QString removeChars(const QString &s, F predicate)
Return a string with characters removed that match the given predicate.
Definition: stringutils.h:34
SWIFT_MISC_EXPORT QList< QStringView > splitLinesRefs(const QString &s)
Split a string into multiple lines. Blank lines are skipped.
SWIFT_MISC_EXPORT QString removeDateTimeSeparators(const QString &s)
Remove the typical separators such as "-", " ".
SWIFT_MISC_EXPORT QMap< QString, QString > parseIniValues(const QString &data)
Obtain ini file like values, e.g. foo=bar.
SWIFT_MISC_EXPORT QString simplifyByDecomposition(const QString &candidate)
Remove accents / diacritic marks from a string by doing a Unicode decomposition and removing mark cha...
SWIFT_MISC_EXPORT bool caseInsensitiveStringCompare(const QString &c1, const QString &c2)
Case insensitive string compare.
QList< QStringView > splitStringRefs(const QString &s, F predicate)
Split a string into multiple strings, using a predicate function to identify the split points.
Definition: stringutils.h:91
SWIFT_MISC_EXPORT const QString & boolToOnOff(bool v)
Bool to on/off.
SWIFT_MISC_EXPORT QString removeSurroundingQuotes(const QString &in)
Remove surrounding quotes "foo" -> foo.
SWIFT_MISC_EXPORT double parseFraction(const QString &fraction, double failDefault=std::numeric_limits< double >::quiet_NaN())
Parse a fraction like 2/3.
SWIFT_MISC_EXPORT bool hasBalancedQuotes(const QString &in, char quote='"')
Has balanced quotes.
SWIFT_MISC_EXPORT QString dotToLocaleDecimalPoint(QString &input)
Replace dot '.' by locale decimal point.
SWIFT_MISC_EXPORT QString simplifyAccents(const QString &candidate)
Remove accents / diacritic marks from a string.
SWIFT_MISC_EXPORT bool stringCompare(const QString &c1, const QString &c2, Qt::CaseSensitivity cs)
String compare.
SWIFT_MISC_EXPORT QString inQuotes(const QString &in, bool ignoreEmpty=false)
Return string in quotes.
SWIFT_MISC_EXPORT QString removeSurroundingApostrophes(const QString &in)
Remove surrounding apostrophes 'foo' -> foo.
SWIFT_MISC_EXPORT QString joinStringSet(const QSet< QString > &set, const QString &separator)
Convert string to bool.
SWIFT_MISC_EXPORT const QString & boolToNullNotNull(bool isNull)
Bool isNull to null/no null.
SWIFT_MISC_EXPORT QStringList splitLines(const QString &s)
Split a string into multiple lines. Blank lines are skipped.
SWIFT_MISC_EXPORT QString removeComments(const QString &in, bool removeSlash, bool removeDoubleSlash)
Remove comments such as /‍** **‍/ or //.
SWIFT_MISC_EXPORT const QString & boolToEnabledDisabled(bool v)
Bool to enabled/disabled.
QStringList splitString(const QString &s, F predicate)
Split a string into multiple strings, using a predicate function to identify the split points.
Definition: stringutils.h:119
SWIFT_MISC_EXPORT bool stringToBool(const QString &boolString)
Convert string to bool.
SWIFT_MISC_EXPORT bool containsAny(const QString &testString, const QStringList &any, Qt::CaseSensitivity cs)
Contains any string of the list?
SWIFT_MISC_EXPORT const QString & boolToTrueFalse(bool v)
Bool to true/false.
bool isDigitsOnlyString(const QString &testString)
String with digits only.
Definition: stringutils.h:167
SWIFT_MISC_EXPORT QDateTime fromStringUtc(const QString &dateTimeString, const QString &format)
Same as QDateTime::fromString but QDateTime will be set to UTC.
SWIFT_MISC_EXPORT const QString & boolToYesNo(bool v)
Bool to yes/no.
SWIFT_MISC_EXPORT QDateTime parseDateTimeStringOptimized(const QString &dateTimeString)
Parse yyyyMMddHHmmsszzz strings optimized.
std::string toLower(std::string s)
String to lower case.
Definition: qtfreeutils.h:113