The Battle for Wesnoth  1.19.17+dev
string_utils.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2005 - 2025
3  by Philippe Plantier <ayin@anathas.org>
4  Copyright (C) 2005 by Guillaume Melquiond <guillaume.melquiond@gmail.com>
5  Copyright (C) 2003 by David White <dave@whitevine.net>
6  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
7 
8  This program is free software; you can redistribute it and/or modify
9  it under the terms of the GNU General Public License as published by
10  the Free Software Foundation; either version 2 of the License, or
11  (at your option) any later version.
12  This program is distributed in the hope that it will be useful,
13  but WITHOUT ANY WARRANTY.
14 
15  See the COPYING file for more details.
16 */
17 
18 /**
19  * @file
20  * Various string-routines.
21  */
22 
23 #include "gettext.hpp"
24 #include "log.hpp"
27 #include "utils/charconv.hpp"
28 #include "utils/general.hpp"
29 #include <array>
30 #include <limits>
31 #include "utils/optional_fwd.hpp"
32 #include <stdexcept>
33 
34 #include <boost/algorithm/string.hpp>
35 
36 static lg::log_domain log_engine("engine");
37 #define ERR_GENERAL LOG_STREAM(err, lg::general())
38 #define ERR_NG LOG_STREAM(err, log_engine)
39 
40 namespace utils {
41 
42 bool isnewline(const char c)
43 {
44  return c == '\r' || c == '\n';
45 }
46 
47 // Make sure that we can use Mac, DOS, or Unix style text files on any system
48 // and they will work, by making sure the definition of whitespace is consistent
49 bool portable_isspace(const char c)
50 {
51  // returns true only on ASCII spaces
52  return c == '\r' || c == '\n' || c == ' ' || c == '\t' || c == '\v' || c == '\f';
53 }
54 
55 // Make sure we regard '\r' and '\n' as a space, since Mac, Unix, and DOS
56 // all consider these differently.
57 bool notspace(const char c)
58 {
59  return !portable_isspace(c);
60 }
61 
62 void trim(std::string_view& s)
63 {
64  s.remove_prefix(std::min(s.find_first_not_of(" \t\r\n"), s.size()));
65  if(s.empty()) {
66  return;
67  }
68  //find_last_not_of never returns npos because !s.empty()
69  std::size_t first_to_trim = s.find_last_not_of(" \t\r\n") + 1;
70  s = s.substr(0, first_to_trim);
71 }
72 
73 /**
74  * Splits a (comma-)separated string into a vector of pieces.
75  * @param[in] s A (comma-)separated string.
76  * @param[in] sep The separator character (usually a comma).
77  * @param[in] flags Flags controlling how the split is done.
78  * This is a bit field with two settings (both on by default):
79  * REMOVE_EMPTY causes empty pieces to be skipped/removed.
80  * STRIP_SPACES causes the leading and trailing spaces of each piece to be ignored/stripped.
81  */
82 std::vector<std::string> split(std::string_view s, const char sep, const int flags)
83 {
84  std::vector<std::string> res;
85  split_foreach(s, sep, flags, [&](std::string_view item) {
86  res.emplace_back(item);
87  });
88  return res;
89 }
90 
91 std::set<std::string> split_set(std::string_view s, char sep, const int flags)
92 {
93  std::set<std::string> res;
94  split_foreach(s, sep, flags, [&](std::string_view item) {
95  res.emplace(item);
96  });
97  return res;
98 }
99 
100 std::vector<std::string_view> split_view(std::string_view s, const char sep, const int flags)
101 {
102  std::vector<std::string_view> res;
103  split_foreach(s, sep, flags, [&](std::string_view item) {
104  res.push_back(item);
105  });
106  return res;
107 }
108 
109 namespace {
110  std::size_t get_padding(std::string_view str)
111  {
112  // A leading '0' signals we want to pad upto the size of the number
113  return (str.size() > 1 && str[0] == '0') ? (str.size() - 1) : 0;
114  }
115 }
116 
117 std::vector<std::string> square_parenthetical_split(const std::string& val,
118  const char separator, const std::string& left,
119  const std::string& right,const int flags)
120 {
121  std::vector< std::string > res;
122  std::vector<char> part;
123  bool in_parenthesis = false;
124  std::vector<std::string::const_iterator> square_left;
125  std::vector<std::string::const_iterator> square_right;
126  std::vector< std::string > square_expansion;
127 
128  std::string lp=left;
129  std::string rp=right;
130 
131  std::string::const_iterator i1 = val.begin();
132  std::string::const_iterator i2;
133  std::string::const_iterator j1;
134  if (flags & STRIP_SPACES) {
135  while (i1 != val.end() && portable_isspace(*i1))
136  ++i1;
137  }
138  i2=i1;
139  j1=i1;
140 
141  if (i1 == val.end()) return res;
142 
143  if (!separator) {
144  ERR_GENERAL << "Separator must be specified for square bracket split function.";
145  return res;
146  }
147 
148  if(left.size()!=right.size()){
149  ERR_GENERAL << "Left and Right Parenthesis lists not same length";
150  return res;
151  }
152 
153  while (true) {
154  if(i2 == val.end() || (!in_parenthesis && *i2 == separator)) {
155  //push back square contents
156  std::size_t size_square_exp = 0;
157  for (std::size_t i=0; i < square_left.size(); i++) {
158  std::string tmp_val(square_left[i]+1,square_right[i]);
159  std::vector< std::string > tmp = split(tmp_val);
160  for(const std::string& piece : tmp) {
161  std::size_t found_tilde = piece.find_first_of('~');
162  if (found_tilde == std::string::npos) {
163  std::size_t found_asterisk = piece.find_first_of('*');
164  if (found_asterisk == std::string::npos) {
165  std::string tmp2(piece);
166  boost::trim(tmp2);
167  square_expansion.push_back(tmp2);
168  }
169  else { //'*' multiple expansion
170  std::string s_begin = piece.substr(0,found_asterisk);
171  boost::trim(s_begin);
172  std::string s_end = piece.substr(found_asterisk+1);
173  boost::trim(s_end);
174  for (int ast=std::stoi(s_end); ast>0; --ast)
175  square_expansion.push_back(s_begin);
176  }
177  }
178  else { //expand number range
179  std::string s_begin = piece.substr(0,found_tilde);
180  boost::trim(s_begin);
181  int begin = std::stoi(s_begin);
182  std::string s_end = piece.substr(found_tilde+1);
183  boost::trim(s_end);
184  int end = std::stoi(s_end);
185 
186  std::size_t padding = std::max(get_padding(s_begin), get_padding(s_end));
187  if (padding > 0 && s_begin.size() != s_end.size()) {
188  ERR_GENERAL << "Square bracket padding sizes not matching: "
189  << s_begin << " and " << s_end <<".";
190  }
191 
192  int increment = (end >= begin ? 1 : -1);
193  end+=increment; //include end in expansion
194  for (int k=begin; k!=end; k+=increment) {
195  std::string pb = std::to_string(k);
196  for (std::size_t p=pb.size(); p<=padding; p++)
197  pb = std::string("0") + pb;
198  square_expansion.push_back(pb);
199  }
200  }
201  }
202  if (i*square_expansion.size() != (i+1)*size_square_exp ) {
203  std::string tmp2(i1, i2);
204  ERR_GENERAL << "Square bracket lengths do not match up: " << tmp2;
205  return res;
206  }
207  size_square_exp = square_expansion.size();
208  }
209 
210  //combine square contents and rest of string for comma zone block
211  std::size_t j = 0;
212  std::size_t j_max = 0;
213  if (!square_left.empty())
214  j_max = square_expansion.size() / square_left.size();
215  do {
216  j1 = i1;
217  std::string new_val;
218  for (std::size_t i=0; i < square_left.size(); i++) {
219  std::string tmp_val(j1, square_left[i]);
220  new_val.append(tmp_val);
221  std::size_t k = j+i*j_max;
222  if (k < square_expansion.size())
223  new_val.append(square_expansion[k]);
224  j1 = square_right[i]+1;
225  }
226  std::string tmp_val(j1, i2);
227  new_val.append(tmp_val);
228  if (flags & STRIP_SPACES)
229  boost::trim_right(new_val);
230  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
231  res.push_back(new_val);
232  j++;
233  } while (j<j_max);
234 
235  if (i2 == val.end()) //escape loop
236  break;
237  ++i2;
238  if (flags & STRIP_SPACES) { //strip leading spaces
239  while (i2 != val.end() && portable_isspace(*i2))
240  ++i2;
241  }
242  i1=i2;
243  square_left.clear();
244  square_right.clear();
245  square_expansion.clear();
246  continue;
247  }
248  if(!part.empty() && *i2 == part.back()) {
249  part.pop_back();
250  if (*i2 == ']') square_right.push_back(i2);
251  if (part.empty())
252  in_parenthesis = false;
253  ++i2;
254  continue;
255  }
256  bool found=false;
257  for(std::size_t i=0; i < lp.size(); i++) {
258  if (*i2 == lp[i]){
259  if (*i2 == '[')
260  square_left.push_back(i2);
261  ++i2;
262  part.push_back(rp[i]);
263  found=true;
264  break;
265  }
266  }
267  if(!found){
268  ++i2;
269  } else
270  in_parenthesis = true;
271  }
272 
273  if(!part.empty()){
274  ERR_GENERAL << "Mismatched parenthesis:\n"<<val;
275  }
276 
277  return res;
278 }
279 
280 std::map<std::string, std::string> map_split(
281  const std::string& val
282  , char major
283  , char minor
284  , int flags
285  , const std::string& default_value)
286 {
287  //first split by major so that we get a vector with the key-value pairs
288  std::vector< std::string > v = split(val, major, flags);
289 
290  //now split by minor to extract keys and values
291  std::map< std::string, std::string > res;
292 
293  for( std::vector< std::string >::iterator i = v.begin(); i != v.end(); ++i) {
294  std::size_t pos = i->find_first_of(minor);
295  std::string key, value;
296 
297  if(pos == std::string::npos) {
298  key = (*i);
299  value = default_value;
300  } else {
301  key = i->substr(0, pos);
302  value = i->substr(pos + 1);
303  }
304 
305  res[key] = value;
306  }
307 
308  return res;
309 }
310 
311 std::vector<std::string> parenthetical_split(std::string_view val,
312  const char separator, std::string_view left,
313  std::string_view right,const int flags)
314 {
315  std::vector< std::string > res;
316  std::vector<char> part;
317  bool in_parenthesis = false;
318 
319  std::string_view::const_iterator i1 = val.begin();
320  std::string_view::const_iterator i2;
321  if (flags & STRIP_SPACES) {
322  while (i1 != val.end() && portable_isspace(*i1))
323  ++i1;
324  }
325  i2=i1;
326 
327  if(left.size()!=right.size()){
328  ERR_GENERAL << "Left and Right Parenthesis lists not same length";
329  return res;
330  }
331 
332  while (i2 != val.end()) {
333  if(!in_parenthesis && separator && *i2 == separator){
334  std::string new_val(i1, i2);
335  if (flags & STRIP_SPACES)
336  boost::trim_right(new_val);
337  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
338  res.push_back(new_val);
339  ++i2;
340  if (flags & STRIP_SPACES) {
341  while (i2 != val.end() && portable_isspace(*i2))
342  ++i2;
343  }
344  i1=i2;
345  continue;
346  }
347  if(!part.empty() && *i2 == part.back()){
348  part.pop_back();
349  if(!separator && part.empty()){
350  std::string new_val(i1, i2);
351  if (flags & STRIP_SPACES)
352  boost::trim(new_val);
353  res.push_back(new_val);
354  ++i2;
355  i1=i2;
356  }else{
357  if (part.empty())
358  in_parenthesis = false;
359  ++i2;
360  }
361  continue;
362  }
363  bool found=false;
364  for(std::size_t i=0; i < left.size(); i++){
365  if (*i2 == left[i]){
366  if (!separator && part.empty()){
367  std::string new_val(i1, i2);
368  if (flags & STRIP_SPACES)
369  boost::trim(new_val);
370  res.push_back(new_val);
371  ++i2;
372  i1=i2;
373  }else{
374  ++i2;
375  }
376  part.push_back(right[i]);
377  found=true;
378  break;
379  }
380  }
381  if(!found){
382  ++i2;
383  } else
384  in_parenthesis = true;
385  }
386 
387  std::string new_val(i1, i2);
388  if (flags & STRIP_SPACES)
389  boost::trim(new_val);
390  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
391  res.push_back(std::move(new_val));
392 
393  if(!part.empty()){
394  ERR_GENERAL << "Mismatched parenthesis:\n"<<val;
395  }
396 
397  return res;
398 }
399 
400 // Modify a number by string representing integer difference, or optionally %
401 int apply_modifier( const int number, const std::string &amount, const int minimum ) {
402  // wassert( amount.empty() == false );
403  int value = 0;
404  try {
405  value = std::stoi(amount);
406  } catch(const std::invalid_argument&) {}
407  if(amount[amount.size()-1] == '%') {
408  value = div100rounded(number * value);
409  }
410  value += number;
411  if (( minimum > 0 ) && ( value < minimum ))
412  value = minimum;
413  return value;
414 }
415 
416 std::string escape(std::string_view str, const char *special_chars)
417 {
418  std::string::size_type pos = str.find_first_of(special_chars);
419  if (pos == std::string::npos) {
420  return std::string(str);
421  }
422  std::string res = std::string(str);
423  do {
424  res.insert(pos, 1, '\\');
425  pos = res.find_first_of(special_chars, pos + 2);
426  } while (pos != std::string::npos);
427  return res;
428 }
429 
430 std::string unescape(std::string_view str)
431 {
432  std::string::size_type pos = str.find('\\');
433  if (pos == std::string::npos) {
434  return std::string(str);
435  }
436  std::string res = std::string(str);
437  do {
438  res.erase(pos, 1);
439  pos = res.find('\\', pos + 1);
440  } while (pos != std::string::npos);
441  return res;
442 }
443 
444 std::string urlencode(std::string_view str)
445 {
446  static const std::string nonresv_str =
447  "-."
448  "0123456789"
449  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
450  "_"
451  "abcdefghijklmnopqrstuvwxyz"
452  "~";
453  static const std::set<char> nonresv(nonresv_str.begin(), nonresv_str.end());
454 
455  std::ostringstream res;
456  res << std::hex;
457  res.fill('0');
458 
459  for(char c : str) {
460  if(nonresv.count(c) != 0) {
461  res << c;
462  continue;
463  }
464 
465  res << '%';
466  res.width(2);
467  res << static_cast<int>(c);
468  }
469 
470  return res.str();
471 }
472 
473 bool string_bool(const std::string& str, bool def) {
474  if (str.empty()) return def;
475 
476  // yes/no is the standard, test it first
477  if (str == "yes") return true;
478  if (str == "no"|| str == "false" || str == "off" || str == "0" || str == "0.0")
479  return false;
480 
481  // all other non-empty string are considered as true
482  return true;
483 }
484 
485 std::string bool_string(const bool value)
486 {
487  std::ostringstream ss;
488  ss << std::boolalpha << value;
489 
490  return ss.str();
491 }
492 
493 std::string signed_value(int val)
494 {
495  std::ostringstream oss;
496  oss << (val >= 0 ? "+" : font::unicode_minus) << std::abs(val);
497  return oss.str();
498 }
499 
500 std::string half_signed_value(int val)
501 {
502  std::ostringstream oss;
503  if (val < 0)
504  oss << font::unicode_minus;
505  oss << std::abs(val);
506  return oss.str();
507 }
508 
509 static void si_string_impl_stream_write(std::stringstream &ss, double input) {
510  std::streamsize oldprec = ss.precision();
511 #ifdef _MSC_VER
512  // For MSVC, default mode misbehaves, so we use fixed instead.
513  ss.precision(1);
514  ss << std::fixed
515  << input;
516 #else
517  // In default mode, precision sets the number of significant figures.
518 
519  // 999.5 and above will render as 1000+, however, only numbers above 1000 will use 4 digits
520  // Rounding everything from 100 up (at which point we stop using decimals anyway) avoids this.
521  if (input >= 100) {
522  input = std::round(input);
523  }
524 
525  // When in binary mode, numbers of up to 1023.9999 can be passed
526  // We should render those with 4 digits, instead of as 1e+3.
527  // Input should be an integer number now, but doubles can do strange things, so check the halfway point instead.
528  if (input >= 999.5) {
529  ss.precision(4);
530  } else {
531  ss.precision(3);
532  }
533  ss << input;
534 #endif
535  ss.precision(oldprec);
536 }
537 
538 std::string si_string(double input, bool base2, const std::string& unit) {
539  const double multiplier = base2 ? 1024 : 1000;
540 
541  typedef std::array<std::string, 9> strings9;
542 
543  if(input < 0){
544  return font::unicode_minus + si_string(std::abs(input), base2, unit);
545  }
546 
547  strings9 prefixes;
548  strings9::const_iterator prefix;
549  if (input == 0.0) {
550  strings9 tmp { { "","","","","","","","","" } };
551  prefixes = tmp;
552  prefix = prefixes.begin();
553  } else if (input < 1.0) {
554  strings9 tmp { {
555  "",
556  _("prefix_milli^m"),
557  _("prefix_micro^µ"),
558  _("prefix_nano^n"),
559  _("prefix_pico^p"),
560  _("prefix_femto^f"),
561  _("prefix_atto^a"),
562  _("prefix_zepto^z"),
563  _("prefix_yocto^y")
564  } };
565  prefixes = tmp;
566  prefix = prefixes.begin();
567  while (input < 1.0 && *prefix != prefixes.back()) {
568  input *= multiplier;
569  ++prefix;
570  }
571  } else {
572  strings9 tmp { {
573  "",
574  (base2 ?
575  // TRANSLATORS: Translate the K in KiB only
576  _("prefix_kibi^K") :
577  _("prefix_kilo^k")
578  ),
579  _("prefix_mega^M"),
580  _("prefix_giga^G"),
581  _("prefix_tera^T"),
582  _("prefix_peta^P"),
583  _("prefix_exa^E"),
584  _("prefix_zetta^Z"),
585  _("prefix_yotta^Y")
586  } };
587  prefixes = tmp;
588  prefix = prefixes.begin();
589  while (input > multiplier && *prefix != prefixes.back()) {
590  input /= multiplier;
591  ++prefix;
592  }
593  }
594 
595  std::stringstream ss;
596  si_string_impl_stream_write(ss, input);
597  ss << ' '
598  << *prefix
599  // TRANSLATORS: Translate the i in (for example) KiB only
600  << (base2 && (!(*prefix).empty()) ? _("infix_binary^i") : "")
601  << unit;
602  return ss.str();
603 }
604 
605 static bool is_username_char(char c) {
606  return ((c == '_') || (c == '-'));
607 }
608 
609 static bool is_wildcard_char(char c) {
610  return ((c == '?') || (c == '*'));
611 }
612 
613 bool isvalid_username(const std::string& username) {
614  const std::size_t alnum = std::count_if(username.begin(), username.end(), isalnum);
615  const std::size_t valid_char =
616  std::count_if(username.begin(), username.end(), is_username_char);
617  if ((alnum + valid_char != username.size())
618  || valid_char == username.size() || username.empty() )
619  {
620  return false;
621  }
622  return true;
623 }
624 
625 bool isvalid_wildcard(const std::string& username) {
626  const std::size_t alnum = std::count_if(username.begin(), username.end(), isalnum);
627  const std::size_t valid_char =
628  std::count_if(username.begin(), username.end(), is_username_char);
629  const std::size_t wild_char =
630  std::count_if(username.begin(), username.end(), is_wildcard_char);
631  if ((alnum + valid_char + wild_char != username.size())
632  || valid_char == username.size() || username.empty() )
633  {
634  return false;
635  }
636  return true;
637 }
638 
639 
640 bool word_completion(std::string& text, std::vector<std::string>& wordlist) {
641  std::vector<std::string> matches;
642  const std::size_t last_space = text.rfind(" ");
643  // If last character is a space return.
644  if (last_space == text.size() -1) {
645  wordlist = matches;
646  return false;
647  }
648 
649  bool text_start;
650  std::string semiword;
651  if (last_space == std::string::npos) {
652  text_start = true;
653  semiword = text;
654  } else {
655  text_start = false;
656  semiword.assign(text, last_space + 1, text.size());
657  }
658 
659  std::string best_match = semiword;
660  for (std::vector<std::string>::const_iterator word = wordlist.begin();
661  word != wordlist.end(); ++word)
662  {
663  if (word->size() < semiword.size()
664  || !std::equal(semiword.begin(), semiword.end(), word->begin(),
665  [](char a, char b) { return tolower(a) == tolower(b); })) // TODO: is this the right approach?
666  {
667  continue;
668  }
669  if (matches.empty()) {
670  best_match = *word;
671  } else {
672  int j = 0;
673  while (toupper(best_match[j]) == toupper((*word)[j])) j++;
674  if (best_match.begin() + j < best_match.end()) {
675  best_match.erase(best_match.begin() + j, best_match.end());
676  }
677  }
678  matches.push_back(*word);
679  }
680  if(!matches.empty()) {
681  text.replace(last_space + 1, best_match.size(), best_match);
682  }
683  wordlist = matches;
684  return text_start;
685 }
686 
687 static bool is_word_boundary(char c) {
688  return (c == ' ' || c == ',' || c == ':' || c == '\'' || c == '"' || c == '-');
689 }
690 
691 bool word_match(const std::string& message, const std::string& word) {
692  std::size_t first = message.find(word);
693  if (first == std::string::npos) return false;
694  if (first == 0 || is_word_boundary(message[first - 1])) {
695  std::size_t next = first + word.size();
696  if (next == message.size() || is_word_boundary(message[next])) {
697  return true;
698  }
699  }
700  return false;
701 }
702 
703 [[nodiscard]] bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
704 {
705  auto s_first = str.cbegin();
706  auto p_first = pat.cbegin();
707  const auto s_last = str.cend();
708  const auto p_last = pat.cend();
709 
710  // First, match the initial characters up to, and including, the first '*'/'+' wildcard.
711  // Matching the first wildcard early allows the `std::equal` to be skipped in some common cases.
712 
713  auto is_star_or_plus = [](char x) noexcept { return x == '*' || x == '+'; };
714  auto match_char = [](char s, char p) noexcept { return s == p || p == '?'; };
715 
716  const auto first_wild = std::find_if(p_first, p_last, is_star_or_plus);
717 
718  if(first_wild == p_last) {
719  return std::equal(s_first, s_last, p_first, p_last, match_char);
720  }
721 
722  const auto wild_cat = static_cast<int>(*first_wild == '+');
723 
724  if(first_wild != p_first) {
725  const auto n_chars = std::distance(p_first, first_wild);
726 
727  if(std::distance(s_first, s_last) < (n_chars + wild_cat)) {
728  return false;
729  }
730 
731  std::tie(s_first, p_first) = std::mismatch(s_first, s_first + n_chars, p_first, match_char);
732 
733  if(p_first != first_wild) {
734  return false;
735  }
736  }
737 
738  if(s_first == s_last) {
739  return std::all_of(p_first, p_last, [](char c) { return c == '*'; });
740  }
741 
742  s_first += wild_cat;
743  p_first += 1;
744 
745  // Then
746 
747  while(true) {
748  const auto next_wild = std::find_if(p_first, p_last, is_star_or_plus);
749 
750  if(next_wild == p_last) {
751  return boost::ends_with(std::pair{s_first, s_last}, std::pair{p_first, p_last}, match_char);
752  }
753 
754  if(next_wild != p_first) {
755  auto [sub_f, sub_l] = std::default_searcher{p_first, next_wild, match_char}(s_first, s_last);
756 
757  if(sub_f == s_last) {
758  return false;
759  }
760 
761  s_first = sub_l;
762  p_first = next_wild;
763  }
764 
765  auto is_wildcard = [](char x) noexcept { return x == '*' || x == '+' || x == '?'; };
766  const auto next_non_wild = std::find_if_not(p_first, p_last, is_wildcard);
767  const auto required_chars = std::distance(p_first, next_non_wild) - std::count(p_first, next_non_wild, '*');
768 
769  if(std::distance(s_first, s_last) < required_chars) {
770  return false;
771  }
772 
773  s_first += required_chars;
774  p_first = next_non_wild;
775 
776  if(p_first == p_last) {
777  return true;
778  }
779  }
780 }
781 
782 void to_sql_wildcards(std::string& str, bool underscores)
783 {
784  std::replace(str.begin(), str.end(), '*', '%');
785  if(underscores)
786  {
787  std::size_t n = 0;
788  while((n = str.find("_", n)) != std::string::npos)
789  {
790  str.replace(n, 1, "\\_");
791  n += 2;
792  }
793  }
794 }
795 
796 std::string indent(const std::string& string, std::size_t indent_size)
797 {
798  if(indent_size == 0) {
799  return string;
800  }
801 
802  std::string indent(indent_size, ' ');
803 
804  if(string.empty()) {
805  return indent;
806  }
807 
808  const std::vector<std::string>& lines = split(string, '\x0A', 0);
809  std::string res;
810 
811  for(std::size_t lno = 0; lno < lines.size();) {
812  const std::string& line = lines[lno];
813 
814  // Lines containing only a carriage return count as empty.
815  if(!line.empty() && line != "\x0D") {
816  res += indent;
817  }
818 
819  res += line;
820 
821  if(++lno < lines.size()) {
822  res += '\x0A';
823  }
824  }
825 
826  return res;
827 }
828 
829 std::vector<std::string> quoted_split(const std::string& val, char c, int flags, char quote)
830 {
831  std::vector<std::string> res;
832 
833  std::string::const_iterator i1 = val.begin();
834  std::string::const_iterator i2 = val.begin();
835 
836  while (i2 != val.end()) {
837  if (*i2 == quote) {
838  // Ignore quoted character
839  ++i2;
840  if (i2 != val.end()) ++i2;
841  } else if (*i2 == c) {
842  std::string new_val(i1, i2);
843  if (flags & STRIP_SPACES)
844  boost::trim(new_val);
845  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
846  res.push_back(std::move(new_val));
847  ++i2;
848  if (flags & STRIP_SPACES) {
849  while(i2 != val.end() && *i2 == ' ')
850  ++i2;
851  }
852 
853  i1 = i2;
854  } else {
855  ++i2;
856  }
857  }
858 
859  std::string new_val(i1, i2);
860  if (flags & STRIP_SPACES)
861  boost::trim(new_val);
862  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
863  res.push_back(new_val);
864 
865  return res;
866 }
867 
868 namespace
869 {
870 /**
871  * Internal common code for parse_range and parse_range_real.
872  *
873  * If str contains two elements and a separator such as "a-b", returns a and b.
874  * Otherwise, returns the original string and utils::nullopt.
875  */
876 std::pair<std::string_view, utils::optional<std::string_view>> parse_range_internal_separator(std::string_view str)
877 {
878  // If turning this into a list with additional options, ensure that "-" (if present) is last. Otherwise a
879  // range such as "-2..-1" might be incorrectly split as "-2..", "1".
880  static const auto separator = std::string{"-"};
881 
882  // Starting from the second character means that it won't interpret the minus
883  // sign on a negative number as the separator.
884  // No need to check the string length first, as str.find() already does that.
885  auto pos = str.find(separator, 1);
886  auto length = separator.size();
887 
888  if(pos != std::string::npos && pos + length < str.size()) {
889  return {str.substr(0, pos), str.substr(pos + length)};
890  }
891 
892  return {str, utils::nullopt};
893 }
894 } // namespace
895 
896 std::pair<int, int> parse_range(std::string_view str)
897 {
898  auto [a, b] = parse_range_internal_separator(str);
899  std::pair<int, int> res{0, 0};
900  try {
901  if(a == "-infinity" && b) {
902  // The "&& b" is so that we treat parse_range("-infinity") the same as parse_range("infinity"),
903  // both of those will report an invalid range.
904  res.first = std::numeric_limits<int>::min();
905  } else {
906  res.first = utils::stoi(a);
907  }
908 
909  if(!b) {
910  res.second = res.first;
911  } else if(*b == "infinity") {
912  res.second = std::numeric_limits<int>::max();
913  } else {
914  res.second = utils::stoi(*b);
915  if(res.second < res.first) {
916  res.second = res.first;
917  }
918  }
919  } catch(const std::invalid_argument&) {
920  ERR_GENERAL << "Invalid range: " << str;
921  }
922 
923  return res;
924 }
925 
926 std::pair<double, double> parse_range_real(std::string_view str)
927 {
928  auto [a, b] = parse_range_internal_separator(str);
929  std::pair<double, double> res{0, 0};
930  try {
931  if(a == "-infinity" && b) {
932  // There's already a static-assert for is_iec559 in random.cpp, so this isn't limiting the architectures
933  // that Wesnoth can run on.
934  static_assert(std::numeric_limits<double>::is_iec559,
935  "Don't know how negative infinity is treated on this architecture");
936  res.first = -std::numeric_limits<double>::infinity();
937  } else {
938  res.first = utils::stod(a);
939  }
940 
941  if(!b) {
942  res.second = res.first;
943  } else if(*b == "infinity") {
944  res.second = std::numeric_limits<double>::infinity();
945  } else {
946  res.second = utils::stod(*b);
947  if(res.second < res.first) {
948  res.second = res.first;
949  }
950  }
951  } catch(const std::invalid_argument&) {
952  ERR_GENERAL << "Invalid range: " << str;
953  }
954 
955  return res;
956 }
957 
958 std::vector<std::pair<int, int>> parse_ranges_unsigned(const std::string& str)
959 {
960  auto to_return = parse_ranges_int(str);
961  if(std::any_of(to_return.begin(), to_return.end(), [](const std::pair<int, int>& r) { return r.first < 0; })) {
962  ERR_GENERAL << "Invalid range (expected values to be zero or positive): " << str;
963  return {};
964  }
965 
966  return to_return;
967 }
968 
969 std::vector<std::pair<double, double>> parse_ranges_real(const std::string& str)
970 {
971  std::vector<std::pair<double, double>> to_return;
972  for(const std::string& r : utils::split(str)) {
973  to_return.push_back(parse_range_real(r));
974  }
975 
976  return to_return;
977 }
978 
979 std::vector<std::pair<int, int>> parse_ranges_int(const std::string& str)
980 {
981  std::vector<std::pair<int, int>> to_return;
982  for(const std::string& r : utils::split(str)) {
983  to_return.push_back(parse_range(r));
984  }
985 
986  return to_return;
987 }
988 
989 void ellipsis_truncate(std::string& str, const std::size_t size)
990 {
991  const std::size_t prev_size = str.length();
992 
993  utf8::truncate(str, size);
994 
995  if(str.length() != prev_size) {
996  str += font::ellipsis;
997  }
998 }
999 
1000 } // end namespace utils
This class represents a single unit of a specific type.
Definition: unit.hpp:132
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
Standard logging facilities (interface).
constexpr int div100rounded(int num)
Guarantees portable results for division by 100; round half up, to the nearest integer.
Definition: math.hpp:37
void line(int from_x, int from_y, int to_x, int to_y)
Draw a line.
Definition: draw.cpp:189
const std::string ellipsis
Definition: constants.cpp:39
const std::string unicode_minus
Definition: constants.cpp:42
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
std::string & truncate(std::string &str, const std::size_t size)
Truncates a UTF-8 string to the specified number of characters.
Definition: unicode.cpp:118
@ STRIP_SPACES
REMOVE_EMPTY: remove empty elements.
@ REMOVE_EMPTY
std::vector< std::string_view > split_view(std::string_view s, const char sep, const int flags)
std::string si_string(double input, bool base2, const std::string &unit)
Convert into a string with an SI-postfix.
void trim(std::string_view &s)
std::string indent(const std::string &string, std::size_t indent_size)
Indent a block of text.
std::map< std::string, std::string > map_split(const std::string &val, char major, char minor, int flags, const std::string &default_value)
Splits a string based on two separators into a map.
bool isvalid_wildcard(const std::string &username)
Check if the username pattern contains only valid characters.
std::set< std::string > split_set(std::string_view s, char sep, const int flags)
std::vector< std::string > quoted_split(const std::string &val, char c, int flags, char quote)
This function is identical to split(), except it does not split when it otherwise would if the previo...
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:156
static void si_string_impl_stream_write(std::stringstream &ss, double input)
std::vector< std::pair< int, int > > parse_ranges_int(const std::string &str)
Handles a comma-separated list of inputs to parse_range.
std::vector< std::string > parenthetical_split(std::string_view val, const char separator, std::string_view left, std::string_view right, const int flags)
Splits a string based either on a separator, except then the text appears within specified parenthesi...
bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
Performs pattern matching with wildcards.
std::string half_signed_value(int val)
Sign with Unicode "−" if negative.
std::string bool_string(const bool value)
Converts a bool value to 'true' or 'false'.
static bool is_word_boundary(char c)
void ellipsis_truncate(std::string &str, const std::size_t size)
Truncates a string to a given utf-8 character count and then appends an ellipsis.
std::vector< std::pair< int, int > > parse_ranges_unsigned(const std::string &str)
Handles a comma-separated list of inputs to parse_range, in a context that does not expect negative v...
std::string urlencode(std::string_view str)
Percent-escape characters in a UTF-8 string intended to be part of a URL.
void to_sql_wildcards(std::string &str, bool underscores)
Converts '*' to '' and optionally escapes '_'.
void split_foreach(std::string_view s, char sep, const int flags, const F &f)
bool string_bool(const std::string &str, bool def)
Convert no, false, off, 0, 0.0 to false, empty to def, and others to true.
bool isvalid_username(const std::string &username)
Check if the username contains only valid characters.
double stod(std::string_view str)
Same interface as std::stod and meant as a drop in replacement, except:
Definition: charconv.hpp:142
bool portable_isspace(const char c)
int apply_modifier(const int number, const std::string &amount, const int minimum)
std::vector< std::string > square_parenthetical_split(const std::string &val, const char separator, const std::string &left, const std::string &right, const int flags)
Similar to parenthetical_split, but also expands embedded square brackets.
std::pair< int, int > parse_range(std::string_view str)
Recognises the following patterns, and returns a {min, max} pair.
bool isnewline(const char c)
bool notspace(const char c)
std::string unescape(std::string_view str)
Remove all escape characters (backslash)
std::vector< std::pair< double, double > > parse_ranges_real(const std::string &str)
bool word_match(const std::string &message, const std::string &word)
Check if a message contains a word.
std::string escape(std::string_view str, const char *special_chars)
Prepends a configurable set of characters with a backslash.
std::string signed_value(int val)
Convert into a signed value (using the Unicode "−" and +0 convention.
std::pair< double, double > parse_range_real(std::string_view str)
Recognises similar patterns to parse_range, and returns a {min, max} pair.
std::vector< std::string > split(const config_attribute_value &val)
bool word_completion(std::string &text, std::vector< std::string > &wordlist)
Try to complete the last word of 'text' with the 'wordlist'.
static bool is_wildcard_char(char c)
static bool is_username_char(char c)
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
#define ERR_GENERAL
static lg::log_domain log_engine("engine")
mock_char c
mock_party p
static map_location::direction n
static map_location::direction s
#define b