The Battle for Wesnoth  1.19.18+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 
123  if (val.empty()) {
124  return res;
125  }
126  if (!separator) {
127  ERR_GENERAL << "Separator must be specified for square bracket split function.";
128  return res;
129  }
130  if (left.size() != right.size()) {
131  ERR_GENERAL << "Left and Right Parenthesis lists not same length";
132  return res;
133  }
134 
135  std::string lp = left;
136  std::string rp = right;
137  std::vector<char> part;
138  bool in_parenthesis = false;
139  std::vector<std::string::const_iterator> square_left;
140  std::vector<std::string::const_iterator> square_right;
141  std::vector< std::string > square_expansion;
142 
143  std::string::const_iterator i1 = val.begin();
144  std::string::const_iterator i2;
145  std::string::const_iterator j1;
146  if (flags & STRIP_SPACES) {
147  while (i1 != val.end() && portable_isspace(*i1))
148  ++i1;
149  }
150  if (i1 == val.end()) return res;
151  i2=i1;
152  j1=i1;
153 
154  // If the string contains no animation markers, return the string immediately.
155  // Added since many static images are treated as animations and run through here.
156  const std::string complex_markers = separator + std::string("[");
157  if (val.find_first_of(complex_markers) == std::string::npos)
158  {
159  std::string mutable_val(i1, val.end());
160  if (flags & STRIP_SPACES) {
161  boost::trim_right(mutable_val);
162  }
163  return { mutable_val };
164  }
165 
166  while (true) {
167  if(i2 == val.end() || (!in_parenthesis && *i2 == separator)) {
168  //push back square contents
169  std::size_t size_square_exp = 0;
170  for (std::size_t i=0; i < square_left.size(); i++) {
171  std::string tmp_val(square_left[i]+1,square_right[i]);
172  std::vector< std::string > tmp = split(tmp_val);
173  for(const std::string& piece : tmp) {
174  std::size_t found_tilde = piece.find_first_of('~');
175  if (found_tilde == std::string::npos) {
176  std::size_t found_asterisk = piece.find_first_of('*');
177  if (found_asterisk == std::string::npos) {
178  std::string tmp2(piece);
179  boost::trim(tmp2);
180  square_expansion.push_back(tmp2);
181  }
182  else { //'*' multiple expansion
183  std::string s_begin = piece.substr(0,found_asterisk);
184  boost::trim(s_begin);
185  std::string s_end = piece.substr(found_asterisk+1);
186  boost::trim(s_end);
187  for (int ast=std::stoi(s_end); ast>0; --ast)
188  square_expansion.push_back(s_begin);
189  }
190  }
191  else { //expand number range
192  std::string s_begin = piece.substr(0,found_tilde);
193  boost::trim(s_begin);
194  int begin = std::stoi(s_begin);
195  std::string s_end = piece.substr(found_tilde+1);
196  boost::trim(s_end);
197  int end = std::stoi(s_end);
198 
199  std::size_t padding = std::max(get_padding(s_begin), get_padding(s_end));
200  if (padding > 0 && s_begin.size() != s_end.size()) {
201  ERR_GENERAL << "Square bracket padding sizes not matching: "
202  << s_begin << " and " << s_end <<".";
203  }
204 
205  int increment = (end >= begin ? 1 : -1);
206  end+=increment; //include end in expansion
207  for (int k=begin; k!=end; k+=increment) {
208  std::string pb = std::to_string(k);
209  for (std::size_t p=pb.size(); p<=padding; p++)
210  pb = std::string("0") + pb;
211  square_expansion.push_back(pb);
212  }
213  }
214  }
215  if (i*square_expansion.size() != (i+1)*size_square_exp ) {
216  std::string tmp2(i1, i2);
217  ERR_GENERAL << "Square bracket lengths do not match up: " << tmp2;
218  return res;
219  }
220  size_square_exp = square_expansion.size();
221  }
222 
223  //combine square contents and rest of string for comma zone block
224  std::size_t j = 0;
225  std::size_t j_max = 0;
226  if (!square_left.empty())
227  j_max = square_expansion.size() / square_left.size();
228  do {
229  j1 = i1;
230  std::string new_val;
231  for (std::size_t i=0; i < square_left.size(); i++) {
232  std::string tmp_val(j1, square_left[i]);
233  new_val.append(tmp_val);
234  std::size_t k = j+i*j_max;
235  if (k < square_expansion.size())
236  new_val.append(square_expansion[k]);
237  j1 = square_right[i]+1;
238  }
239  std::string tmp_val(j1, i2);
240  new_val.append(tmp_val);
241  if (flags & STRIP_SPACES)
242  boost::trim_right(new_val);
243  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
244  res.push_back(new_val);
245  j++;
246  } while (j<j_max);
247 
248  if (i2 == val.end()) //escape loop
249  break;
250  ++i2;
251  if (flags & STRIP_SPACES) { //strip leading spaces
252  while (i2 != val.end() && portable_isspace(*i2))
253  ++i2;
254  }
255  i1=i2;
256  square_left.clear();
257  square_right.clear();
258  square_expansion.clear();
259  continue;
260  }
261  if(!part.empty() && *i2 == part.back()) {
262  part.pop_back();
263  if (*i2 == ']') square_right.push_back(i2);
264  if (part.empty())
265  in_parenthesis = false;
266  ++i2;
267  continue;
268  }
269  bool found=false;
270  for(std::size_t i=0; i < lp.size(); i++) {
271  if (*i2 == lp[i]){
272  if (*i2 == '[')
273  square_left.push_back(i2);
274  ++i2;
275  part.push_back(rp[i]);
276  found=true;
277  break;
278  }
279  }
280  if(!found){
281  ++i2;
282  } else
283  in_parenthesis = true;
284  }
285 
286  if(!part.empty()){
287  ERR_GENERAL << "Mismatched parenthesis:\n"<<val;
288  }
289 
290  return res;
291 }
292 
293 std::map<std::string, std::string> map_split(
294  const std::string& val
295  , char major
296  , char minor
297  , int flags
298  , const std::string& default_value)
299 {
300  //first split by major so that we get a vector with the key-value pairs
301  std::vector< std::string > v = split(val, major, flags);
302 
303  //now split by minor to extract keys and values
304  std::map< std::string, std::string > res;
305 
306  for( std::vector< std::string >::iterator i = v.begin(); i != v.end(); ++i) {
307  std::size_t pos = i->find_first_of(minor);
308  std::string key, value;
309 
310  if(pos == std::string::npos) {
311  key = (*i);
312  value = default_value;
313  } else {
314  key = i->substr(0, pos);
315  value = i->substr(pos + 1);
316  }
317 
318  res[key] = value;
319  }
320 
321  return res;
322 }
323 
324 std::vector<std::string> parenthetical_split(std::string_view val,
325  const char separator, std::string_view left,
326  std::string_view right,const int flags)
327 {
328  std::vector< std::string > res;
329  std::vector<char> part;
330  bool in_parenthesis = false;
331 
332  std::string_view::const_iterator i1 = val.begin();
333  std::string_view::const_iterator i2;
334  if (flags & STRIP_SPACES) {
335  while (i1 != val.end() && portable_isspace(*i1))
336  ++i1;
337  }
338  i2=i1;
339 
340  if(left.size()!=right.size()){
341  ERR_GENERAL << "Left and Right Parenthesis lists not same length";
342  return res;
343  }
344 
345  while (i2 != val.end()) {
346  if(!in_parenthesis && separator && *i2 == separator){
347  std::string new_val(i1, i2);
348  if (flags & STRIP_SPACES)
349  boost::trim_right(new_val);
350  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
351  res.push_back(new_val);
352  ++i2;
353  if (flags & STRIP_SPACES) {
354  while (i2 != val.end() && portable_isspace(*i2))
355  ++i2;
356  }
357  i1=i2;
358  continue;
359  }
360  if(!part.empty() && *i2 == part.back()){
361  part.pop_back();
362  if(!separator && part.empty()){
363  std::string new_val(i1, i2);
364  if (flags & STRIP_SPACES)
365  boost::trim(new_val);
366  res.push_back(new_val);
367  ++i2;
368  i1=i2;
369  }else{
370  if (part.empty())
371  in_parenthesis = false;
372  ++i2;
373  }
374  continue;
375  }
376  bool found=false;
377  for(std::size_t i=0; i < left.size(); i++){
378  if (*i2 == left[i]){
379  if (!separator && part.empty()){
380  std::string new_val(i1, i2);
381  if (flags & STRIP_SPACES)
382  boost::trim(new_val);
383  res.push_back(new_val);
384  ++i2;
385  i1=i2;
386  }else{
387  ++i2;
388  }
389  part.push_back(right[i]);
390  found=true;
391  break;
392  }
393  }
394  if(!found){
395  ++i2;
396  } else
397  in_parenthesis = true;
398  }
399 
400  std::string new_val(i1, i2);
401  if (flags & STRIP_SPACES)
402  boost::trim(new_val);
403  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
404  res.push_back(std::move(new_val));
405 
406  if(!part.empty()){
407  ERR_GENERAL << "Mismatched parenthesis:\n"<<val;
408  }
409 
410  return res;
411 }
412 
413 // Modify a number by string representing integer difference, or optionally %
414 int apply_modifier( const int number, const std::string &amount, const int minimum ) {
415  // wassert( amount.empty() == false );
416  int value = 0;
417  try {
418  value = std::stoi(amount);
419  } catch(const std::invalid_argument&) {}
420  if(amount[amount.size()-1] == '%') {
421  value = div100rounded(number * value);
422  }
423  value += number;
424  if (( minimum > 0 ) && ( value < minimum ))
425  value = minimum;
426  return value;
427 }
428 
429 std::string escape(std::string_view str, const char *special_chars)
430 {
431  std::string::size_type pos = str.find_first_of(special_chars);
432  if (pos == std::string::npos) {
433  return std::string(str);
434  }
435  std::string res = std::string(str);
436  do {
437  res.insert(pos, 1, '\\');
438  pos = res.find_first_of(special_chars, pos + 2);
439  } while (pos != std::string::npos);
440  return res;
441 }
442 
443 std::string unescape(std::string_view str)
444 {
445  std::string::size_type pos = str.find('\\');
446  if (pos == std::string::npos) {
447  return std::string(str);
448  }
449  std::string res = std::string(str);
450  do {
451  res.erase(pos, 1);
452  pos = res.find('\\', pos + 1);
453  } while (pos != std::string::npos);
454  return res;
455 }
456 
457 std::string urlencode(std::string_view str)
458 {
459  static const std::string nonresv_str =
460  "-."
461  "0123456789"
462  "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
463  "_"
464  "abcdefghijklmnopqrstuvwxyz"
465  "~";
466  static const std::set<char> nonresv(nonresv_str.begin(), nonresv_str.end());
467 
468  std::ostringstream res;
469  res << std::hex;
470  res.fill('0');
471 
472  for(char c : str) {
473  if(nonresv.count(c) != 0) {
474  res << c;
475  continue;
476  }
477 
478  res << '%';
479  res.width(2);
480  res << static_cast<int>(c);
481  }
482 
483  return res.str();
484 }
485 
486 bool string_bool(const std::string& str, bool def) {
487  if (str.empty()) return def;
488 
489  // yes/no is the standard, test it first
490  if (str == "yes") return true;
491  if (str == "no"|| str == "false" || str == "off" || str == "0" || str == "0.0")
492  return false;
493 
494  // all other non-empty string are considered as true
495  return true;
496 }
497 
498 std::string bool_string(const bool value)
499 {
500  std::ostringstream ss;
501  ss << std::boolalpha << value;
502 
503  return ss.str();
504 }
505 
506 std::string signed_value(int val)
507 {
508  std::ostringstream oss;
509  oss << (val >= 0 ? "+" : font::unicode_minus) << std::abs(val);
510  return oss.str();
511 }
512 
513 std::string half_signed_value(int val)
514 {
515  std::ostringstream oss;
516  if (val < 0)
517  oss << font::unicode_minus;
518  oss << std::abs(val);
519  return oss.str();
520 }
521 
522 static void si_string_impl_stream_write(std::stringstream &ss, double input) {
523  std::streamsize oldprec = ss.precision();
524 #ifdef _MSC_VER
525  // For MSVC, default mode misbehaves, so we use fixed instead.
526  ss.precision(1);
527  ss << std::fixed
528  << input;
529 #else
530  // In default mode, precision sets the number of significant figures.
531 
532  // 999.5 and above will render as 1000+, however, only numbers above 1000 will use 4 digits
533  // Rounding everything from 100 up (at which point we stop using decimals anyway) avoids this.
534  if (input >= 100) {
535  input = std::round(input);
536  }
537 
538  // When in binary mode, numbers of up to 1023.9999 can be passed
539  // We should render those with 4 digits, instead of as 1e+3.
540  // Input should be an integer number now, but doubles can do strange things, so check the halfway point instead.
541  if (input >= 999.5) {
542  ss.precision(4);
543  } else {
544  ss.precision(3);
545  }
546  ss << input;
547 #endif
548  ss.precision(oldprec);
549 }
550 
551 std::string si_string(double input, bool base2, const std::string& unit) {
552  const double multiplier = base2 ? 1024 : 1000;
553 
554  typedef std::array<std::string, 9> strings9;
555 
556  if(input < 0){
557  return font::unicode_minus + si_string(std::abs(input), base2, unit);
558  }
559 
560  strings9 prefixes;
561  strings9::const_iterator prefix;
562  if (input == 0.0) {
563  strings9 tmp { { "","","","","","","","","" } };
564  prefixes = tmp;
565  prefix = prefixes.begin();
566  } else if (input < 1.0) {
567  strings9 tmp { {
568  "",
569  _("prefix_milli^m"),
570  _("prefix_micro^µ"),
571  _("prefix_nano^n"),
572  _("prefix_pico^p"),
573  _("prefix_femto^f"),
574  _("prefix_atto^a"),
575  _("prefix_zepto^z"),
576  _("prefix_yocto^y")
577  } };
578  prefixes = tmp;
579  prefix = prefixes.begin();
580  while (input < 1.0 && *prefix != prefixes.back()) {
581  input *= multiplier;
582  ++prefix;
583  }
584  } else {
585  strings9 tmp { {
586  "",
587  (base2 ?
588  // TRANSLATORS: Translate the K in KiB only
589  _("prefix_kibi^K") :
590  _("prefix_kilo^k")
591  ),
592  _("prefix_mega^M"),
593  _("prefix_giga^G"),
594  _("prefix_tera^T"),
595  _("prefix_peta^P"),
596  _("prefix_exa^E"),
597  _("prefix_zetta^Z"),
598  _("prefix_yotta^Y")
599  } };
600  prefixes = tmp;
601  prefix = prefixes.begin();
602  while (input > multiplier && *prefix != prefixes.back()) {
603  input /= multiplier;
604  ++prefix;
605  }
606  }
607 
608  std::stringstream ss;
609  si_string_impl_stream_write(ss, input);
610  ss << ' '
611  << *prefix
612  // TRANSLATORS: Translate the i in (for example) KiB only
613  << (base2 && (!(*prefix).empty()) ? _("infix_binary^i") : "")
614  << unit;
615  return ss.str();
616 }
617 
618 static bool is_username_char(char c) {
619  return ((c == '_') || (c == '-'));
620 }
621 
622 static bool is_wildcard_char(char c) {
623  return ((c == '?') || (c == '*'));
624 }
625 
626 bool isvalid_username(const std::string& username) {
627  const std::size_t alnum = std::count_if(username.begin(), username.end(), isalnum);
628  const std::size_t valid_char =
629  std::count_if(username.begin(), username.end(), is_username_char);
630  if ((alnum + valid_char != username.size())
631  || valid_char == username.size() || username.empty() )
632  {
633  return false;
634  }
635  return true;
636 }
637 
638 bool isvalid_wildcard(const std::string& username) {
639  const std::size_t alnum = std::count_if(username.begin(), username.end(), isalnum);
640  const std::size_t valid_char =
641  std::count_if(username.begin(), username.end(), is_username_char);
642  const std::size_t wild_char =
643  std::count_if(username.begin(), username.end(), is_wildcard_char);
644  if ((alnum + valid_char + wild_char != username.size())
645  || valid_char == username.size() || username.empty() )
646  {
647  return false;
648  }
649  return true;
650 }
651 
652 
653 bool word_completion(std::string& text, std::vector<std::string>& wordlist) {
654  std::vector<std::string> matches;
655  const std::size_t last_space = text.rfind(" ");
656  // If last character is a space return.
657  if (last_space == text.size() -1) {
658  wordlist = matches;
659  return false;
660  }
661 
662  bool text_start;
663  std::string semiword;
664  if (last_space == std::string::npos) {
665  text_start = true;
666  semiword = text;
667  } else {
668  text_start = false;
669  semiword.assign(text, last_space + 1, text.size());
670  }
671 
672  std::string best_match = semiword;
673  for (std::vector<std::string>::const_iterator word = wordlist.begin();
674  word != wordlist.end(); ++word)
675  {
676  if (word->size() < semiword.size()
677  || !std::equal(semiword.begin(), semiword.end(), word->begin(),
678  [](char a, char b) { return tolower(a) == tolower(b); })) // TODO: is this the right approach?
679  {
680  continue;
681  }
682  if (matches.empty()) {
683  best_match = *word;
684  } else {
685  int j = 0;
686  while (toupper(best_match[j]) == toupper((*word)[j])) j++;
687  if (best_match.begin() + j < best_match.end()) {
688  best_match.erase(best_match.begin() + j, best_match.end());
689  }
690  }
691  matches.push_back(*word);
692  }
693  if(!matches.empty()) {
694  text.replace(last_space + 1, best_match.size(), best_match);
695  }
696  wordlist = matches;
697  return text_start;
698 }
699 
700 static bool is_word_boundary(char c) {
701  return (c == ' ' || c == ',' || c == ':' || c == '\'' || c == '"' || c == '-');
702 }
703 
704 bool word_match(const std::string& message, const std::string& word) {
705  std::size_t first = message.find(word);
706  if (first == std::string::npos) return false;
707  if (first == 0 || is_word_boundary(message[first - 1])) {
708  std::size_t next = first + word.size();
709  if (next == message.size() || is_word_boundary(message[next])) {
710  return true;
711  }
712  }
713  return false;
714 }
715 
716 [[nodiscard]] bool wildcard_string_match(std::string_view str, std::string_view pat) noexcept
717 {
718  auto s_first = str.cbegin();
719  auto p_first = pat.cbegin();
720  const auto s_last = str.cend();
721  const auto p_last = pat.cend();
722 
723  // First, match the initial characters up to, and including, the first '*'/'+' wildcard.
724  // Matching the first wildcard early allows the `std::equal` to be skipped in some common cases.
725 
726  auto is_star_or_plus = [](char x) noexcept { return x == '*' || x == '+'; };
727  auto match_char = [](char s, char p) noexcept { return s == p || p == '?'; };
728 
729  const auto first_wild = std::find_if(p_first, p_last, is_star_or_plus);
730 
731  if(first_wild == p_last) {
732  return std::equal(s_first, s_last, p_first, p_last, match_char);
733  }
734 
735  const auto wild_cat = static_cast<int>(*first_wild == '+');
736 
737  if(first_wild != p_first) {
738  const auto n_chars = std::distance(p_first, first_wild);
739 
740  if(std::distance(s_first, s_last) < (n_chars + wild_cat)) {
741  return false;
742  }
743 
744  std::tie(s_first, p_first) = std::mismatch(s_first, s_first + n_chars, p_first, match_char);
745 
746  if(p_first != first_wild) {
747  return false;
748  }
749  }
750 
751  if(s_first == s_last) {
752  return std::all_of(p_first, p_last, [](char c) { return c == '*'; });
753  }
754 
755  s_first += wild_cat;
756  p_first += 1;
757 
758  // Then
759 
760  while(true) {
761  const auto next_wild = std::find_if(p_first, p_last, is_star_or_plus);
762 
763  if(next_wild == p_last) {
764  return boost::ends_with(std::pair{s_first, s_last}, std::pair{p_first, p_last}, match_char);
765  }
766 
767  if(next_wild != p_first) {
768  auto [sub_f, sub_l] = std::default_searcher{p_first, next_wild, match_char}(s_first, s_last);
769 
770  if(sub_f == s_last) {
771  return false;
772  }
773 
774  s_first = sub_l;
775  p_first = next_wild;
776  }
777 
778  auto is_wildcard = [](char x) noexcept { return x == '*' || x == '+' || x == '?'; };
779  const auto next_non_wild = std::find_if_not(p_first, p_last, is_wildcard);
780  const auto required_chars = std::distance(p_first, next_non_wild) - std::count(p_first, next_non_wild, '*');
781 
782  if(std::distance(s_first, s_last) < required_chars) {
783  return false;
784  }
785 
786  s_first += required_chars;
787  p_first = next_non_wild;
788 
789  if(p_first == p_last) {
790  return true;
791  }
792  }
793 }
794 
795 void to_sql_wildcards(std::string& str, bool underscores)
796 {
797  std::replace(str.begin(), str.end(), '*', '%');
798  if(underscores)
799  {
800  std::size_t n = 0;
801  while((n = str.find("_", n)) != std::string::npos)
802  {
803  str.replace(n, 1, "\\_");
804  n += 2;
805  }
806  }
807 }
808 
809 std::string indent(const std::string& string, std::size_t indent_size)
810 {
811  if(indent_size == 0) {
812  return string;
813  }
814 
815  std::string indent(indent_size, ' ');
816 
817  if(string.empty()) {
818  return indent;
819  }
820 
821  const std::vector<std::string>& lines = split(string, '\x0A', 0);
822  std::string res;
823 
824  for(std::size_t lno = 0; lno < lines.size();) {
825  const std::string& line = lines[lno];
826 
827  // Lines containing only a carriage return count as empty.
828  if(!line.empty() && line != "\x0D") {
829  res += indent;
830  }
831 
832  res += line;
833 
834  if(++lno < lines.size()) {
835  res += '\x0A';
836  }
837  }
838 
839  return res;
840 }
841 
842 std::vector<std::string> quoted_split(const std::string& val, char c, int flags, char quote)
843 {
844  std::vector<std::string> res;
845 
846  std::string::const_iterator i1 = val.begin();
847  std::string::const_iterator i2 = val.begin();
848 
849  while (i2 != val.end()) {
850  if (*i2 == quote) {
851  // Ignore quoted character
852  ++i2;
853  if (i2 != val.end()) ++i2;
854  } else if (*i2 == c) {
855  std::string new_val(i1, i2);
856  if (flags & STRIP_SPACES)
857  boost::trim(new_val);
858  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
859  res.push_back(std::move(new_val));
860  ++i2;
861  if (flags & STRIP_SPACES) {
862  while(i2 != val.end() && *i2 == ' ')
863  ++i2;
864  }
865 
866  i1 = i2;
867  } else {
868  ++i2;
869  }
870  }
871 
872  std::string new_val(i1, i2);
873  if (flags & STRIP_SPACES)
874  boost::trim(new_val);
875  if (!(flags & REMOVE_EMPTY) || !new_val.empty())
876  res.push_back(new_val);
877 
878  return res;
879 }
880 
881 namespace
882 {
883 /**
884  * Internal common code for parse_range and parse_range_real.
885  *
886  * If str contains two elements and a separator such as "a-b", returns a and b.
887  * Otherwise, returns the original string and utils::nullopt.
888  */
889 std::pair<std::string_view, utils::optional<std::string_view>> parse_range_internal_separator(std::string_view str)
890 {
891  // If turning this into a list with additional options, ensure that "-" (if present) is last. Otherwise a
892  // range such as "-2..-1" might be incorrectly split as "-2..", "1".
893  static const auto separator = std::string{"-"};
894 
895  // Starting from the second character means that it won't interpret the minus
896  // sign on a negative number as the separator.
897  // No need to check the string length first, as str.find() already does that.
898  auto pos = str.find(separator, 1);
899  auto length = separator.size();
900 
901  if(pos != std::string::npos && pos + length < str.size()) {
902  return {str.substr(0, pos), str.substr(pos + length)};
903  }
904 
905  return {str, utils::nullopt};
906 }
907 } // namespace
908 
909 std::pair<int, int> parse_range(std::string_view str)
910 {
911  auto [a, b] = parse_range_internal_separator(str);
912  std::pair<int, int> res{0, 0};
913  try {
914  if(a == "-infinity" && b) {
915  // The "&& b" is so that we treat parse_range("-infinity") the same as parse_range("infinity"),
916  // both of those will report an invalid range.
917  res.first = std::numeric_limits<int>::min();
918  } else {
919  res.first = utils::stoi(a);
920  }
921 
922  if(!b) {
923  res.second = res.first;
924  } else if(*b == "infinity") {
925  res.second = std::numeric_limits<int>::max();
926  } else {
927  res.second = utils::stoi(*b);
928  if(res.second < res.first) {
929  res.second = res.first;
930  }
931  }
932  } catch(const std::invalid_argument&) {
933  ERR_GENERAL << "Invalid range: " << str;
934  }
935 
936  return res;
937 }
938 
939 std::pair<double, double> parse_range_real(std::string_view str)
940 {
941  auto [a, b] = parse_range_internal_separator(str);
942  std::pair<double, double> res{0, 0};
943  try {
944  if(a == "-infinity" && b) {
945  // There's already a static-assert for is_iec559 in random.cpp, so this isn't limiting the architectures
946  // that Wesnoth can run on.
947  static_assert(std::numeric_limits<double>::is_iec559,
948  "Don't know how negative infinity is treated on this architecture");
949  res.first = -std::numeric_limits<double>::infinity();
950  } else {
951  res.first = utils::stod(a);
952  }
953 
954  if(!b) {
955  res.second = res.first;
956  } else if(*b == "infinity") {
957  res.second = std::numeric_limits<double>::infinity();
958  } else {
959  res.second = utils::stod(*b);
960  if(res.second < res.first) {
961  res.second = res.first;
962  }
963  }
964  } catch(const std::invalid_argument&) {
965  ERR_GENERAL << "Invalid range: " << str;
966  }
967 
968  return res;
969 }
970 
971 std::vector<std::pair<int, int>> parse_ranges_unsigned(const std::string& str)
972 {
973  auto to_return = parse_ranges_int(str);
974  if(std::any_of(to_return.begin(), to_return.end(), [](const std::pair<int, int>& r) { return r.first < 0; })) {
975  ERR_GENERAL << "Invalid range (expected values to be zero or positive): " << str;
976  return {};
977  }
978 
979  return to_return;
980 }
981 
982 std::vector<std::pair<double, double>> parse_ranges_real(const std::string& str)
983 {
984  std::vector<std::pair<double, double>> to_return;
985  for(const std::string& r : utils::split(str)) {
986  to_return.push_back(parse_range_real(r));
987  }
988 
989  return to_return;
990 }
991 
992 std::vector<std::pair<int, int>> parse_ranges_int(const std::string& str)
993 {
994  std::vector<std::pair<int, int>> to_return;
995  for(const std::string& r : utils::split(str)) {
996  to_return.push_back(parse_range(r));
997  }
998 
999  return to_return;
1000 }
1001 
1002 void ellipsis_truncate(std::string& str, const std::size_t size)
1003 {
1004  const std::size_t prev_size = str.length();
1005 
1006  utf8::truncate(str, size);
1007 
1008  if(str.length() != prev_size) {
1009  str += font::ellipsis;
1010  }
1011 }
1012 
1013 } // end namespace utils
This class represents a single unit of a specific type.
Definition: unit.hpp:39
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.
auto * find_if(Container &container, const Predicate &predicate)
Convenience wrapper for using find_if on a container without needing to comare to end()
Definition: general.hpp:151
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