Browse Source

Merge pull request #84379 from Rubonnek/add-string-overloads

Add `const char *` overloads to String class
Rémi Verschelde 1 year ago
parent
commit
ed0dbc36aa
5 changed files with 672 additions and 160 deletions
  1. 426 8
      core/string/ustring.cpp
  2. 15 0
      core/string/ustring.h
  3. 13 13
      core/variant/variant_call.cpp
  4. 151 139
      tests/core/string/test_string.h
  5. 67 0
      tests/test_macros.h

+ 426 - 8
core/string/ustring.cpp

@@ -1184,6 +1184,26 @@ int String::get_slice_count(const String &p_splitter) const {
 	return slices;
 }
 
+int String::get_slice_count(const char *p_splitter) const {
+	if (is_empty()) {
+		return 0;
+	}
+	if (p_splitter == nullptr || *p_splitter == '\0') {
+		return 0;
+	}
+
+	int pos = 0;
+	int slices = 1;
+	int splitter_length = strlen(p_splitter);
+
+	while ((pos = find(p_splitter, pos)) >= 0) {
+		slices++;
+		pos += splitter_length;
+	}
+
+	return slices;
+}
+
 String String::get_slice(const String &p_splitter, int p_slice) const {
 	if (is_empty() || p_splitter.is_empty()) {
 		return "";
@@ -1224,6 +1244,47 @@ String String::get_slice(const String &p_splitter, int p_slice) const {
 	return ""; //no find!
 }
 
+String String::get_slice(const char *p_splitter, int p_slice) const {
+	if (is_empty() || p_splitter == nullptr || *p_splitter == '\0') {
+		return "";
+	}
+
+	int pos = 0;
+	int prev_pos = 0;
+	//int slices=1;
+	if (p_slice < 0) {
+		return "";
+	}
+	if (find(p_splitter) == -1) {
+		return *this;
+	}
+
+	int i = 0;
+	int splitter_length = strlen(p_splitter);
+	while (true) {
+		pos = find(p_splitter, pos);
+		if (pos == -1) {
+			pos = length(); //reached end
+		}
+
+		int from = prev_pos;
+		//int to=pos;
+
+		if (p_slice == i) {
+			return substr(from, pos - from);
+		}
+
+		if (pos == length()) { //reached end and no find
+			break;
+		}
+		pos += splitter_length;
+		prev_pos = pos;
+		i++;
+	}
+
+	return ""; //no find!
+}
+
 String String::get_slicec(char32_t p_splitter, int p_slice) const {
 	if (is_empty()) {
 		return String();
@@ -1338,6 +1399,54 @@ Vector<String> String::split(const String &p_splitter, bool p_allow_empty, int p
 	return ret;
 }
 
+Vector<String> String::split(const char *p_splitter, bool p_allow_empty, int p_maxsplit) const {
+	Vector<String> ret;
+
+	if (is_empty()) {
+		if (p_allow_empty) {
+			ret.push_back("");
+		}
+		return ret;
+	}
+
+	int from = 0;
+	int len = length();
+
+	while (true) {
+		int end;
+		if (p_splitter == nullptr || *p_splitter == '\0') {
+			end = from + 1;
+		} else {
+			end = find(p_splitter, from);
+			if (end < 0) {
+				end = len;
+			}
+		}
+		if (p_allow_empty || (end > from)) {
+			if (p_maxsplit <= 0) {
+				ret.push_back(substr(from, end - from));
+			} else {
+				// Put rest of the string and leave cycle.
+				if (p_maxsplit == ret.size()) {
+					ret.push_back(substr(from, len));
+					break;
+				}
+
+				// Otherwise, push items until positive limit is reached.
+				ret.push_back(substr(from, end - from));
+			}
+		}
+
+		if (end == len) {
+			break;
+		}
+
+		from = end + strlen(p_splitter);
+	}
+
+	return ret;
+}
+
 Vector<String> String::rsplit(const String &p_splitter, bool p_allow_empty, int p_maxsplit) const {
 	Vector<String> ret;
 	const int len = length();
@@ -1380,6 +1489,49 @@ Vector<String> String::rsplit(const String &p_splitter, bool p_allow_empty, int
 	return ret;
 }
 
+Vector<String> String::rsplit(const char *p_splitter, bool p_allow_empty, int p_maxsplit) const {
+	Vector<String> ret;
+	const int len = length();
+	const int splitter_length = strlen(p_splitter);
+	int remaining_len = len;
+
+	while (true) {
+		if (remaining_len < splitter_length || (p_maxsplit > 0 && p_maxsplit == ret.size())) {
+			// no room for another splitter or hit max splits, push what's left and we're done
+			if (p_allow_empty || remaining_len > 0) {
+				ret.push_back(substr(0, remaining_len));
+			}
+			break;
+		}
+
+		int left_edge;
+		if (p_splitter == nullptr || *p_splitter == '\0') {
+			left_edge = remaining_len - 1;
+			if (left_edge == 0) {
+				left_edge--; // Skip to the < 0 condition.
+			}
+		} else {
+			left_edge = rfind(p_splitter, remaining_len - splitter_length);
+		}
+
+		if (left_edge < 0) {
+			// no more splitters, we're done
+			ret.push_back(substr(0, remaining_len));
+			break;
+		}
+
+		int substr_start = left_edge + splitter_length;
+		if (p_allow_empty || substr_start < remaining_len) {
+			ret.push_back(substr(substr_start, remaining_len - substr_start));
+		}
+
+		remaining_len = left_edge;
+	}
+
+	ret.reverse();
+	return ret;
+}
+
 Vector<double> String::split_floats(const String &p_splitter, bool p_allow_empty) const {
 	Vector<double> ret;
 	int from = 0;
@@ -3087,23 +3239,20 @@ int String::find(const String &p_str, int p_from) const {
 }
 
 int String::find(const char *p_str, int p_from) const {
-	if (p_from < 0) {
+	if (p_from < 0 || !p_str) {
 		return -1;
 	}
 
+	const int src_len = strlen(p_str);
+
 	const int len = length();
 
-	if (len == 0) {
+	if (len == 0 || src_len == 0) {
 		return -1; // won't find anything!
 	}
 
 	const char32_t *src = get_data();
 
-	int src_len = 0;
-	while (p_str[src_len] != '\0') {
-		src_len++;
-	}
-
 	if (src_len == 1) {
 		const char32_t needle = p_str[0];
 
@@ -3238,6 +3387,46 @@ int String::findn(const String &p_str, int p_from) const {
 	return -1;
 }
 
+int String::findn(const char *p_str, int p_from) const {
+	if (p_from < 0) {
+		return -1;
+	}
+
+	int src_len = strlen(p_str);
+
+	if (src_len == 0 || length() == 0) {
+		return -1; // won't find anything!
+	}
+
+	const char32_t *srcd = get_data();
+
+	for (int i = p_from; i <= (length() - src_len); i++) {
+		bool found = true;
+		for (int j = 0; j < src_len; j++) {
+			int read_pos = i + j;
+
+			if (read_pos >= length()) {
+				ERR_PRINT("read_pos>=length()");
+				return -1;
+			}
+
+			char32_t src = _find_lower(srcd[read_pos]);
+			char32_t dst = _find_lower(p_str[j]);
+
+			if (src != dst) {
+				found = false;
+				break;
+			}
+		}
+
+		if (found) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
 int String::rfind(const String &p_str, int p_from) const {
 	// establish a limit
 	int limit = length() - p_str.length();
@@ -3285,6 +3474,57 @@ int String::rfind(const String &p_str, int p_from) const {
 	return -1;
 }
 
+int String::rfind(const char *p_str, int p_from) const {
+	const int source_length = length();
+	int substring_length = strlen(p_str);
+
+	if (source_length == 0 || substring_length == 0) {
+		return -1; // won't find anything!
+	}
+
+	// establish a limit
+	int limit = length() - substring_length;
+	if (limit < 0) {
+		return -1;
+	}
+
+	// establish a starting point
+	int starting_point;
+	if (p_from < 0) {
+		starting_point = limit;
+	} else if (p_from > limit) {
+		starting_point = limit;
+	} else {
+		starting_point = p_from;
+	}
+
+	const char32_t *source = get_data();
+
+	for (int i = starting_point; i >= 0; i--) {
+		bool found = true;
+		for (int j = 0; j < substring_length; j++) {
+			int read_pos = i + j;
+
+			if (read_pos >= source_length) {
+				ERR_PRINT("read_pos>=source_length");
+				return -1;
+			}
+
+			const char32_t key_needle = p_str[j];
+			if (source[read_pos] != key_needle) {
+				found = false;
+				break;
+			}
+		}
+
+		if (found) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
 int String::rfindn(const String &p_str, int p_from) const {
 	// establish a limit
 	int limit = length() - p_str.length();
@@ -3335,6 +3575,60 @@ int String::rfindn(const String &p_str, int p_from) const {
 	return -1;
 }
 
+int String::rfindn(const char *p_str, int p_from) const {
+	const int source_length = length();
+	int substring_length = strlen(p_str);
+
+	if (source_length == 0 || substring_length == 0) {
+		return -1; // won't find anything!
+	}
+
+	// establish a limit
+	int limit = length() - substring_length;
+	if (limit < 0) {
+		return -1;
+	}
+
+	// establish a starting point
+	int starting_point;
+	if (p_from < 0) {
+		starting_point = limit;
+	} else if (p_from > limit) {
+		starting_point = limit;
+	} else {
+		starting_point = p_from;
+	}
+
+	const char32_t *source = get_data();
+
+	for (int i = starting_point; i >= 0; i--) {
+		bool found = true;
+		for (int j = 0; j < substring_length; j++) {
+			int read_pos = i + j;
+
+			if (read_pos >= source_length) {
+				ERR_PRINT("read_pos>=source_length");
+				return -1;
+			}
+
+			const char32_t key_needle = p_str[j];
+			int srcc = _find_lower(source[read_pos]);
+			int keyc = _find_lower(key_needle);
+
+			if (srcc != keyc) {
+				found = false;
+				break;
+			}
+		}
+
+		if (found) {
+			return i;
+		}
+	}
+
+	return -1;
+}
+
 bool String::ends_with(const String &p_string) const {
 	int l = p_string.length();
 	if (l > length()) {
@@ -3357,6 +3651,31 @@ bool String::ends_with(const String &p_string) const {
 	return true;
 }
 
+bool String::ends_with(const char *p_string) const {
+	if (!p_string) {
+		return false;
+	}
+
+	int l = strlen(p_string);
+	if (l > length()) {
+		return false;
+	}
+
+	if (l == 0) {
+		return true;
+	}
+
+	const char32_t *s = &operator[](length() - l);
+
+	for (int i = 0; i < l; i++) {
+		if (static_cast<char32_t>(p_string[i]) != s[i]) {
+			return false;
+		}
+	}
+
+	return true;
+}
+
 bool String::begins_with(const String &p_string) const {
 	int l = p_string.length();
 	if (l > length()) {
@@ -3380,11 +3699,11 @@ bool String::begins_with(const String &p_string) const {
 }
 
 bool String::begins_with(const char *p_string) const {
-	int l = length();
 	if (!p_string) {
 		return false;
 	}
 
+	int l = length();
 	if (l == 0) {
 		return *p_string == 0;
 	}
@@ -3456,14 +3775,61 @@ int String::_count(const String &p_string, int p_from, int p_to, bool p_case_ins
 	return c;
 }
 
+int String::_count(const char *p_string, int p_from, int p_to, bool p_case_insensitive) const {
+	int substring_length = strlen(p_string);
+	if (substring_length == 0) {
+		return 0;
+	}
+	const int source_length = length();
+
+	if (source_length < substring_length) {
+		return 0;
+	}
+	String str;
+	int search_limit = p_to;
+	if (p_from >= 0 && p_to >= 0) {
+		if (p_to == 0) {
+			search_limit = source_length;
+		} else if (p_from >= p_to) {
+			return 0;
+		}
+		if (p_from == 0 && search_limit == source_length) {
+			str = String();
+			str.copy_from_unchecked(&get_data()[0], source_length);
+		} else {
+			str = substr(p_from, search_limit - p_from);
+		}
+	} else {
+		return 0;
+	}
+	int c = 0;
+	int idx = -1;
+	do {
+		idx = p_case_insensitive ? str.findn(p_string) : str.find(p_string);
+		if (idx != -1) {
+			str = str.substr(idx + substring_length, str.length() - substring_length);
+			++c;
+		}
+	} while (idx != -1);
+	return c;
+}
+
 int String::count(const String &p_string, int p_from, int p_to) const {
 	return _count(p_string, p_from, p_to, false);
 }
 
+int String::count(const char *p_string, int p_from, int p_to) const {
+	return _count(p_string, p_from, p_to, false);
+}
+
 int String::countn(const String &p_string, int p_from, int p_to) const {
 	return _count(p_string, p_from, p_to, true);
 }
 
+int String::countn(const char *p_string, int p_from, int p_to) const {
+	return _count(p_string, p_from, p_to, true);
+}
+
 bool String::_base_is_subsequence_of(const String &p_string, bool case_insensitive) const {
 	int len = length();
 	if (len == 0) {
@@ -3673,6 +4039,16 @@ String String::replace_first(const String &p_key, const String &p_with) const {
 	return *this;
 }
 
+String String::replace_first(const char *p_key, const char *p_with) const {
+	int pos = find(p_key);
+	if (pos >= 0) {
+		int substring_length = strlen(p_key);
+		return substr(0, pos) + p_with + substr(pos + substring_length, length());
+	}
+
+	return *this;
+}
+
 String String::replacen(const String &p_key, const String &p_with) const {
 	String new_string;
 	int search_from = 0;
@@ -3692,6 +4068,31 @@ String String::replacen(const String &p_key, const String &p_with) const {
 	return new_string;
 }
 
+String String::replacen(const char *p_key, const char *p_with) const {
+	String new_string;
+	int search_from = 0;
+	int result = 0;
+	int substring_length = strlen(p_key);
+
+	if (substring_length == 0) {
+		return *this; // there's nothing to match or substitute
+	}
+
+	while ((result = findn(p_key, search_from)) >= 0) {
+		new_string += substr(search_from, result - search_from);
+		new_string += p_with;
+		search_from = result + substring_length;
+	}
+
+	if (search_from == 0) {
+		return *this;
+	}
+
+	new_string += substr(search_from, length() - search_from);
+
+	return new_string;
+}
+
 String String::repeat(int p_count) const {
 	ERR_FAIL_COND_V_MSG(p_count < 0, "", "Parameter count should be a positive number.");
 
@@ -4420,6 +4821,15 @@ String String::trim_prefix(const String &p_prefix) const {
 	return s;
 }
 
+String String::trim_prefix(const char *p_prefix) const {
+	String s = *this;
+	if (s.begins_with(p_prefix)) {
+		int prefix_length = strlen(p_prefix);
+		return s.substr(prefix_length, s.length() - prefix_length);
+	}
+	return s;
+}
+
 String String::trim_suffix(const String &p_suffix) const {
 	String s = *this;
 	if (s.ends_with(p_suffix)) {
@@ -4428,6 +4838,14 @@ String String::trim_suffix(const String &p_suffix) const {
 	return s;
 }
 
+String String::trim_suffix(const char *p_suffix) const {
+	String s = *this;
+	if (s.ends_with(p_suffix)) {
+		return s.substr(0, s.length() - strlen(p_suffix));
+	}
+	return s;
+}
+
 bool String::is_valid_int() const {
 	int len = length();
 

+ 15 - 0
core/string/ustring.h

@@ -198,6 +198,7 @@ class String {
 
 	bool _base_is_subsequence_of(const String &p_string, bool case_insensitive) const;
 	int _count(const String &p_string, int p_from, int p_to, bool p_case_insensitive) const;
+	int _count(const char *p_string, int p_from, int p_to, bool p_case_insensitive) const;
 	String _camelcase_to_underscore() const;
 
 public:
@@ -288,14 +289,18 @@ public:
 	int find(const char *p_str, int p_from = 0) const; ///< return <0 if failed
 	int find_char(const char32_t &p_char, int p_from = 0) const; ///< return <0 if failed
 	int findn(const String &p_str, int p_from = 0) const; ///< return <0 if failed, case insensitive
+	int findn(const char *p_str, int p_from = 0) const; ///< return <0 if failed
 	int rfind(const String &p_str, int p_from = -1) const; ///< return <0 if failed
+	int rfind(const char *p_str, int p_from = -1) const; ///< return <0 if failed
 	int rfindn(const String &p_str, int p_from = -1) const; ///< return <0 if failed, case insensitive
+	int rfindn(const char *p_str, int p_from = -1) const; ///< return <0 if failed
 	int findmk(const Vector<String> &p_keys, int p_from = 0, int *r_key = nullptr) const; ///< return <0 if failed
 	bool match(const String &p_wildcard) const;
 	bool matchn(const String &p_wildcard) const;
 	bool begins_with(const String &p_string) const;
 	bool begins_with(const char *p_string) const;
 	bool ends_with(const String &p_string) const;
+	bool ends_with(const char *p_string) const;
 	bool is_enclosed_in(const String &p_string) const;
 	bool is_subsequence_of(const String &p_string) const;
 	bool is_subsequence_ofn(const String &p_string) const;
@@ -304,9 +309,11 @@ public:
 	float similarity(const String &p_string) const;
 	String format(const Variant &values, const String &placeholder = "{_}") const;
 	String replace_first(const String &p_key, const String &p_with) const;
+	String replace_first(const char *p_key, const char *p_with) const;
 	String replace(const String &p_key, const String &p_with) const;
 	String replace(const char *p_key, const char *p_with) const;
 	String replacen(const String &p_key, const String &p_with) const;
+	String replacen(const char *p_key, const char *p_with) const;
 	String repeat(int p_count) const;
 	String reverse() const;
 	String insert(int p_at_pos, const String &p_string) const;
@@ -314,7 +321,9 @@ public:
 	String pad_decimals(int p_digits) const;
 	String pad_zeros(int p_digits) const;
 	String trim_prefix(const String &p_prefix) const;
+	String trim_prefix(const char *p_prefix) const;
 	String trim_suffix(const String &p_suffix) const;
+	String trim_suffix(const char *p_suffix) const;
 	String lpad(int min_length, const String &character = " ") const;
 	String rpad(int min_length, const String &character = " ") const;
 	String sprintf(const Array &values, bool *error) const;
@@ -353,11 +362,15 @@ public:
 
 	String get_with_code_lines() const;
 	int get_slice_count(const String &p_splitter) const;
+	int get_slice_count(const char *p_splitter) const;
 	String get_slice(const String &p_splitter, int p_slice) const;
+	String get_slice(const char *p_splitter, int p_slice) const;
 	String get_slicec(char32_t p_splitter, int p_slice) const;
 
 	Vector<String> split(const String &p_splitter = "", bool p_allow_empty = true, int p_maxsplit = 0) const;
+	Vector<String> split(const char *p_splitter = "", bool p_allow_empty = true, int p_maxsplit = 0) const;
 	Vector<String> rsplit(const String &p_splitter = "", bool p_allow_empty = true, int p_maxsplit = 0) const;
+	Vector<String> rsplit(const char *p_splitter = "", bool p_allow_empty = true, int p_maxsplit = 0) const;
 	Vector<String> split_spaces() const;
 	Vector<double> split_floats(const String &p_splitter, bool p_allow_empty = true) const;
 	Vector<float> split_floats_mk(const Vector<String> &p_splitters, bool p_allow_empty = true) const;
@@ -372,7 +385,9 @@ public:
 	String to_lower() const;
 
 	int count(const String &p_string, int p_from = 0, int p_to = 0) const;
+	int count(const char *p_string, int p_from = 0, int p_to = 0) const;
 	int countn(const String &p_string, int p_from = 0, int p_to = 0) const;
+	int countn(const char *p_string, int p_from = 0, int p_to = 0) const;
 
 	String left(int p_len) const;
 	String right(int p_len) const;

+ 13 - 13
core/variant/variant_call.cpp

@@ -1648,19 +1648,19 @@ static void _register_variant_builtin_methods() {
 	bind_string_method(filenocasecmp_to, sarray("to"), varray());
 	bind_string_method(length, sarray(), varray());
 	bind_string_method(substr, sarray("from", "len"), varray(-1));
-	bind_string_method(get_slice, sarray("delimiter", "slice"), varray());
+	bind_string_methodv(get_slice, static_cast<String (String::*)(const String &, int) const>(&String::get_slice), sarray("delimiter", "slice"), varray());
 	bind_string_method(get_slicec, sarray("delimiter", "slice"), varray());
-	bind_string_method(get_slice_count, sarray("delimiter"), varray());
+	bind_string_methodv(get_slice_count, static_cast<int (String::*)(const String &) const>(&String::get_slice_count), sarray("delimiter"), varray());
 	bind_string_methodv(find, static_cast<int (String::*)(const String &, int) const>(&String::find), sarray("what", "from"), varray(0));
-	bind_string_method(count, sarray("what", "from", "to"), varray(0, 0));
-	bind_string_method(countn, sarray("what", "from", "to"), varray(0, 0));
-	bind_string_method(findn, sarray("what", "from"), varray(0));
-	bind_string_method(rfind, sarray("what", "from"), varray(-1));
-	bind_string_method(rfindn, sarray("what", "from"), varray(-1));
+	bind_string_methodv(findn, static_cast<int (String::*)(const String &, int) const>(&String::findn), sarray("what", "from"), varray(0));
+	bind_string_methodv(count, static_cast<int (String::*)(const String &, int, int) const>(&String::count), sarray("what", "from", "to"), varray(0, 0));
+	bind_string_methodv(countn, static_cast<int (String::*)(const String &, int, int) const>(&String::countn), sarray("what", "from", "to"), varray(0, 0));
+	bind_string_methodv(rfind, static_cast<int (String::*)(const String &, int) const>(&String::rfind), sarray("what", "from"), varray(-1));
+	bind_string_methodv(rfindn, static_cast<int (String::*)(const String &, int) const>(&String::rfindn), sarray("what", "from"), varray(-1));
 	bind_string_method(match, sarray("expr"), varray());
 	bind_string_method(matchn, sarray("expr"), varray());
 	bind_string_methodv(begins_with, static_cast<bool (String::*)(const String &) const>(&String::begins_with), sarray("text"), varray());
-	bind_string_method(ends_with, sarray("text"), varray());
+	bind_string_methodv(ends_with, static_cast<bool (String::*)(const String &) const>(&String::ends_with), sarray("text"), varray());
 	bind_string_method(is_subsequence_of, sarray("text"), varray());
 	bind_string_method(is_subsequence_ofn, sarray("text"), varray());
 	bind_string_method(bigrams, sarray(), varray());
@@ -1668,7 +1668,7 @@ static void _register_variant_builtin_methods() {
 
 	bind_string_method(format, sarray("values", "placeholder"), varray("{_}"));
 	bind_string_methodv(replace, static_cast<String (String::*)(const String &, const String &) const>(&String::replace), sarray("what", "forwhat"), varray());
-	bind_string_method(replacen, sarray("what", "forwhat"), varray());
+	bind_string_methodv(replacen, static_cast<String (String::*)(const String &, const String &) const>(&String::replacen), sarray("what", "forwhat"), varray());
 	bind_string_method(repeat, sarray("count"), varray());
 	bind_string_method(reverse, sarray(), varray());
 	bind_string_method(insert, sarray("position", "what"), varray());
@@ -1677,8 +1677,8 @@ static void _register_variant_builtin_methods() {
 	bind_string_method(to_camel_case, sarray(), varray());
 	bind_string_method(to_pascal_case, sarray(), varray());
 	bind_string_method(to_snake_case, sarray(), varray());
-	bind_string_method(split, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
-	bind_string_method(rsplit, sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
+	bind_string_methodv(split, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::split), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
+	bind_string_methodv(rsplit, static_cast<Vector<String> (String::*)(const String &, bool, int) const>(&String::rsplit), sarray("delimiter", "allow_empty", "maxsplit"), varray("", true, 0));
 	bind_string_method(split_floats, sarray("delimiter", "allow_empty"), varray(true));
 	bind_string_method(join, sarray("parts"), varray());
 
@@ -1741,8 +1741,8 @@ static void _register_variant_builtin_methods() {
 	bind_string_method(rpad, sarray("min_length", "character"), varray(" "));
 	bind_string_method(pad_decimals, sarray("digits"), varray());
 	bind_string_method(pad_zeros, sarray("digits"), varray());
-	bind_string_method(trim_prefix, sarray("prefix"), varray());
-	bind_string_method(trim_suffix, sarray("suffix"), varray());
+	bind_string_methodv(trim_prefix, static_cast<String (String::*)(const String &) const>(&String::trim_prefix), sarray("prefix"), varray());
+	bind_string_methodv(trim_suffix, static_cast<String (String::*)(const String &) const>(&String::trim_suffix), sarray("suffix"), varray());
 
 	bind_string_method(to_ascii_buffer, sarray(), varray());
 	bind_string_method(to_utf8_buffer, sarray(), varray());

+ 151 - 139
tests/core/string/test_string.h

@@ -360,18 +360,37 @@ TEST_CASE("[String] Substr") {
 
 TEST_CASE("[String] Find") {
 	String s = "Pretty Woman Woman";
-	CHECK(s.find("tty") == 3);
-	CHECK(s.find("Wo", 9) == 13);
-	CHECK(s.find("Revenge of the Monster Truck") == -1);
-	CHECK(s.rfind("man") == 15);
+	MULTICHECK_STRING_EQ(s, find, "tty", 3);
+	MULTICHECK_STRING_EQ(s, find, "Revenge of the Monster Truck", -1);
+	MULTICHECK_STRING_INT_EQ(s, find, "Wo", 9, 13);
+	MULTICHECK_STRING_EQ(s, find, "", -1);
+	MULTICHECK_STRING_EQ(s, find, "Pretty Woman Woman", 0);
+	MULTICHECK_STRING_EQ(s, find, "WOMAN", -1);
+	MULTICHECK_STRING_INT_EQ(s, find, "", 9, -1);
+
+	MULTICHECK_STRING_EQ(s, rfind, "", -1);
+	MULTICHECK_STRING_EQ(s, rfind, "foo", -1);
+	MULTICHECK_STRING_EQ(s, rfind, "Pretty Woman Woman", 0);
+	MULTICHECK_STRING_EQ(s, rfind, "man", 15);
+	MULTICHECK_STRING_EQ(s, rfind, "WOMAN", -1);
+	MULTICHECK_STRING_INT_EQ(s, rfind, "", 15, -1);
 }
 
 TEST_CASE("[String] Find no case") {
 	String s = "Pretty Whale Whale";
-	CHECK(s.findn("WHA") == 7);
-	CHECK(s.findn("WHA", 9) == 13);
-	CHECK(s.findn("Revenge of the Monster SawFish") == -1);
-	CHECK(s.rfindn("WHA") == 13);
+	MULTICHECK_STRING_EQ(s, findn, "WHA", 7);
+	MULTICHECK_STRING_INT_EQ(s, findn, "WHA", 9, 13);
+	MULTICHECK_STRING_EQ(s, findn, "Revenge of the Monster SawFish", -1);
+	MULTICHECK_STRING_EQ(s, findn, "", -1);
+	MULTICHECK_STRING_EQ(s, findn, "wha", 7);
+	MULTICHECK_STRING_EQ(s, findn, "Wha", 7);
+	MULTICHECK_STRING_INT_EQ(s, findn, "", 3, -1);
+
+	MULTICHECK_STRING_EQ(s, rfindn, "WHA", 13);
+	MULTICHECK_STRING_EQ(s, rfindn, "", -1);
+	MULTICHECK_STRING_EQ(s, rfindn, "wha", 13);
+	MULTICHECK_STRING_EQ(s, rfindn, "Wha", 13);
+	MULTICHECK_STRING_INT_EQ(s, rfindn, "", 13, -1);
 }
 
 TEST_CASE("[String] Find MK") {
@@ -392,11 +411,9 @@ TEST_CASE("[String] Find MK") {
 
 TEST_CASE("[String] Find and replace") {
 	String s = "Happy Birthday, Anna!";
-	s = s.replace("Birthday", "Halloween");
-	CHECK(s == "Happy Halloween, Anna!");
-
-	s = s.replace_first("H", "W");
-	CHECK(s == "Wappy Halloween, Anna!");
+	MULTICHECK_STRING_STRING_EQ(s, replace, "Birthday", "Halloween", "Happy Halloween, Anna!");
+	MULTICHECK_STRING_STRING_EQ(s, replace_first, "y", "Y", "HappY Birthday, Anna!");
+	MULTICHECK_STRING_STRING_EQ(s, replacen, "Y", "Y", "HappY BirthdaY, Anna!");
 }
 
 TEST_CASE("[String] Insertion") {
@@ -557,51 +574,76 @@ TEST_CASE("[String] String to float") {
 
 TEST_CASE("[String] Slicing") {
 	String s = "Mars,Jupiter,Saturn,Uranus";
-
 	const char *slices[4] = { "Mars", "Jupiter", "Saturn", "Uranus" };
-	for (int i = 0; i < s.get_slice_count(","); i++) {
-		CHECK(s.get_slice(",", i) == slices[i]);
-	}
+	MULTICHECK_GET_SLICE(s, ",", slices);
+}
+
+TEST_CASE("[String] Begins with") {
+	// Test cases for true:
+	MULTICHECK_STRING_EQ(String("res://foobar"), begins_with, "res://", true);
+	MULTICHECK_STRING_EQ(String("abc"), begins_with, "abc", true);
+	MULTICHECK_STRING_EQ(String("abc"), begins_with, "", true);
+	MULTICHECK_STRING_EQ(String(""), begins_with, "", true);
+
+	// Test cases for false:
+	MULTICHECK_STRING_EQ(String("res"), begins_with, "res://", false);
+	MULTICHECK_STRING_EQ(String("abcdef"), begins_with, "foo", false);
+	MULTICHECK_STRING_EQ(String("abc"), begins_with, "ax", false);
+	MULTICHECK_STRING_EQ(String(""), begins_with, "abc", false);
+
+	// Test "const char *" version also with nullptr.
+	String s("foo");
+	bool state = s.begins_with(nullptr) == false;
+	CHECK_MESSAGE(state, "nullptr check failed");
+
+	String empty("");
+	state = empty.begins_with(nullptr) == false;
+	CHECK_MESSAGE(state, "nullptr check with empty string failed");
+}
+
+TEST_CASE("[String] Ends with") {
+	// Test cases for true:
+	MULTICHECK_STRING_EQ(String("res://foobar"), ends_with, "foobar", true);
+	MULTICHECK_STRING_EQ(String("abc"), ends_with, "abc", true);
+	MULTICHECK_STRING_EQ(String("abc"), ends_with, "", true);
+	MULTICHECK_STRING_EQ(String(""), ends_with, "", true);
+
+	// Test cases for false:
+	MULTICHECK_STRING_EQ(String("res"), ends_with, "res://", false);
+	MULTICHECK_STRING_EQ(String("abcdef"), ends_with, "foo", false);
+	MULTICHECK_STRING_EQ(String("abc"), ends_with, "ax", false);
+	MULTICHECK_STRING_EQ(String(""), ends_with, "abc", false);
+
+	// Test "const char *" version also with nullptr.
+	String s("foo");
+	bool state = s.ends_with(nullptr) == false;
+	CHECK_MESSAGE(state, "nullptr check failed");
+
+	String empty("");
+	state = empty.ends_with(nullptr) == false;
+	CHECK_MESSAGE(state, "nullptr check with empty string failed");
 }
 
 TEST_CASE("[String] Splitting") {
 	String s = "Mars,Jupiter,Saturn,Uranus";
-	Vector<String> l;
-
 	const char *slices_l[3] = { "Mars", "Jupiter", "Saturn,Uranus" };
-	const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" };
-	const char *slices_3[4] = { "t", "e", "s", "t" };
-
-	l = s.split(",", true, 2);
-	CHECK(l.size() == 3);
-	for (int i = 0; i < l.size(); i++) {
-		CHECK(l[i] == slices_l[i]);
-	}
+	MULTICHECK_SPLIT(s, split, ",", true, 2, slices_l, 3);
 
-	l = s.rsplit(",", true, 2);
-	CHECK(l.size() == 3);
-	for (int i = 0; i < l.size(); i++) {
-		CHECK(l[i] == slices_r[i]);
-	}
+	const char *slices_r[3] = { "Mars,Jupiter", "Saturn", "Uranus" };
+	MULTICHECK_SPLIT(s, rsplit, ",", true, 2, slices_r, 3);
 
 	s = "test";
-	l = s.split();
-	CHECK(l.size() == 4);
-	for (int i = 0; i < l.size(); i++) {
-		CHECK(l[i] == slices_3[i]);
-	}
+	const char *slices_3[4] = { "t", "e", "s", "t" };
+	MULTICHECK_SPLIT(s, split, "", true, 0, slices_3, 4);
 
 	s = "";
-	l = s.split();
-	CHECK(l.size() == 1);
-	CHECK(l[0] == "");
-
-	l = s.split("", false);
-	CHECK(l.size() == 0);
+	const char *slices_4[1] = { "" };
+	MULTICHECK_SPLIT(s, split, "", true, 0, slices_4, 1);
+	MULTICHECK_SPLIT(s, split, "", false, 0, slices_4, 0);
 
 	s = "Mars Jupiter Saturn Uranus";
 	const char *slices_s[4] = { "Mars", "Jupiter", "Saturn", "Uranus" };
-	l = s.split_spaces();
+	Vector<String> l = s.split_spaces();
 	for (int i = 0; i < l.size(); i++) {
 		CHECK(l[i] == slices_s[i]);
 	}
@@ -644,69 +686,6 @@ TEST_CASE("[String] Splitting") {
 	}
 }
 
-struct test_27_data {
-	char const *data;
-	char const *part;
-	bool expected;
-};
-
-TEST_CASE("[String] Begins with") {
-	test_27_data tc[] = {
-		// Test cases for true:
-		{ "res://foobar", "res://", true },
-		{ "abc", "abc", true },
-		{ "abc", "", true },
-		{ "", "", true },
-		// Test cases for false:
-		{ "res", "res://", false },
-		{ "abcdef", "foo", false },
-		{ "abc", "ax", false },
-		{ "", "abc", false }
-	};
-	size_t count = sizeof(tc) / sizeof(tc[0]);
-	bool state = true;
-	for (size_t i = 0; i < count; ++i) {
-		String s = tc[i].data;
-		state = s.begins_with(tc[i].part) == tc[i].expected;
-		CHECK_MESSAGE(state, "first check failed at: ", i);
-
-		String sb = tc[i].part;
-		state = s.begins_with(sb) == tc[i].expected;
-		CHECK_MESSAGE(state, "second check failed at: ", i);
-	}
-
-	// Test "const char *" version also with nullptr.
-	String s("foo");
-	state = s.begins_with(nullptr) == false;
-	CHECK_MESSAGE(state, "nullptr check failed");
-
-	String empty("");
-	state = empty.begins_with(nullptr) == false;
-	CHECK_MESSAGE(state, "nullptr check with empty string failed");
-}
-
-TEST_CASE("[String] Ends with") {
-	test_27_data tc[] = {
-		// test cases for true:
-		{ "res://foobar", "foobar", true },
-		{ "abc", "abc", true },
-		{ "abc", "", true },
-		{ "", "", true },
-		// test cases for false:
-		{ "res", "res://", false },
-		{ "", "abc", false },
-		{ "abcdef", "foo", false },
-		{ "abc", "xc", false }
-	};
-	size_t count = sizeof(tc) / sizeof(tc[0]);
-	for (size_t i = 0; i < count; ++i) {
-		String s = tc[i].data;
-		String sb = tc[i].part;
-		bool state = s.ends_with(sb) == tc[i].expected;
-		CHECK_MESSAGE(state, "check failed at: ", i);
-	}
-}
-
 TEST_CASE("[String] format") {
 	const String value_format = "red=\"$red\" green=\"$green\" blue=\"$blue\" alpha=\"$alpha\"";
 
@@ -1498,39 +1477,62 @@ TEST_CASE("[String] Cyrillic to_lower()") {
 }
 
 TEST_CASE("[String] Count and countn functionality") {
-#define COUNT_TEST(x)             \
-	{                             \
-		bool success = x;         \
-		state = state && success; \
-	}
+	String s = String("");
+	MULTICHECK_STRING_EQ(s, count, "Test", 0);
 
-	bool state = true;
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, count, "", 0);
 
-	COUNT_TEST(String("").count("Test") == 0);
-	COUNT_TEST(String("Test").count("") == 0);
-	COUNT_TEST(String("Test").count("test") == 0);
-	COUNT_TEST(String("Test").count("TEST") == 0);
-	COUNT_TEST(String("TEST").count("TEST") == 1);
-	COUNT_TEST(String("Test").count("Test") == 1);
-	COUNT_TEST(String("aTest").count("Test") == 1);
-	COUNT_TEST(String("Testa").count("Test") == 1);
-	COUNT_TEST(String("TestTestTest").count("Test") == 3);
-	COUNT_TEST(String("TestTestTest").count("TestTest") == 1);
-	COUNT_TEST(String("TestGodotTestGodotTestGodot").count("Test") == 3);
-
-	COUNT_TEST(String("TestTestTestTest").count("Test", 4, 8) == 1);
-	COUNT_TEST(String("TestTestTestTest").count("Test", 4, 12) == 2);
-	COUNT_TEST(String("TestTestTestTest").count("Test", 4, 16) == 3);
-	COUNT_TEST(String("TestTestTestTest").count("Test", 4) == 3);
-
-	COUNT_TEST(String("Test").countn("test") == 1);
-	COUNT_TEST(String("Test").countn("TEST") == 1);
-	COUNT_TEST(String("testTest-Testatest").countn("tEst") == 4);
-	COUNT_TEST(String("testTest-TeStatest").countn("tEsT", 4, 16) == 2);
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, count, "test", 0);
 
-	CHECK(state);
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, count, "TEST", 0);
+
+	s = "TEST";
+	MULTICHECK_STRING_EQ(s, count, "TEST", 1);
+
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, count, "Test", 1);
+
+	s = "aTest";
+	MULTICHECK_STRING_EQ(s, count, "Test", 1);
+
+	s = "Testa";
+	MULTICHECK_STRING_EQ(s, count, "Test", 1);
+
+	s = "TestTestTest";
+	MULTICHECK_STRING_EQ(s, count, "Test", 3);
+
+	s = "TestTestTest";
+	MULTICHECK_STRING_EQ(s, count, "TestTest", 1);
+
+	s = "TestGodotTestGodotTestGodot";
+	MULTICHECK_STRING_EQ(s, count, "Test", 3);
+
+	s = "TestTestTestTest";
+	MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 8, 1);
+
+	s = "TestTestTestTest";
+	MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 12, 2);
+
+	s = "TestTestTestTest";
+	MULTICHECK_STRING_INT_INT_EQ(s, count, "Test", 4, 16, 3);
+
+	s = "TestTestTestTest";
+	MULTICHECK_STRING_INT_EQ(s, count, "Test", 4, 3);
+
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, countn, "test", 1);
+
+	s = "Test";
+	MULTICHECK_STRING_EQ(s, countn, "TEST", 1);
+
+	s = "testTest-Testatest";
+	MULTICHECK_STRING_EQ(s, countn, "tEst", 4);
 
-#undef COUNT_TEST
+	s = "testTest-TeStatest";
+	MULTICHECK_STRING_INT_INT_EQ(s, countn, "tEsT", 4, 16, 2);
 }
 
 TEST_CASE("[String] Bigrams") {
@@ -1703,9 +1705,19 @@ TEST_CASE("[String] Strip edges") {
 
 TEST_CASE("[String] Trim") {
 	String s = "aaaTestbbb";
-	CHECK(s.trim_prefix("aaa") == "Testbbb");
-	CHECK(s.trim_suffix("bbb") == "aaaTest");
-	CHECK(s.trim_suffix("Test") == s);
+	MULTICHECK_STRING_EQ(s, trim_prefix, "aaa", "Testbbb");
+	MULTICHECK_STRING_EQ(s, trim_prefix, "Test", s);
+	MULTICHECK_STRING_EQ(s, trim_prefix, "", s);
+	MULTICHECK_STRING_EQ(s, trim_prefix, "aaaTestbbb", "");
+	MULTICHECK_STRING_EQ(s, trim_prefix, "bbb", s);
+	MULTICHECK_STRING_EQ(s, trim_prefix, "AAA", s);
+
+	MULTICHECK_STRING_EQ(s, trim_suffix, "bbb", "aaaTest");
+	MULTICHECK_STRING_EQ(s, trim_suffix, "Test", s);
+	MULTICHECK_STRING_EQ(s, trim_suffix, "", s);
+	MULTICHECK_STRING_EQ(s, trim_suffix, "aaaTestbbb", "");
+	MULTICHECK_STRING_EQ(s, trim_suffix, "aaa", s);
+	MULTICHECK_STRING_EQ(s, trim_suffix, "BBB", s);
 }
 
 TEST_CASE("[String] Right/Left") {

+ 67 - 0
tests/test_macros.h

@@ -406,4 +406,71 @@ public:
 #define SIGNAL_CHECK_FALSE(m_signal) CHECK(SignalWatcher::get_singleton()->check_false(m_signal));
 #define SIGNAL_DISCARD(m_signal) SignalWatcher::get_singleton()->discard_signal(m_signal);
 
+#define MULTICHECK_STRING_EQ(m_obj, m_func, m_param1, m_eq) \
+	CHECK(m_obj.m_func(m_param1) == m_eq);                  \
+	CHECK(m_obj.m_func(U##m_param1) == m_eq);               \
+	CHECK(m_obj.m_func(L##m_param1) == m_eq);               \
+	CHECK(m_obj.m_func(String(m_param1)) == m_eq);
+
+#define MULTICHECK_STRING_INT_EQ(m_obj, m_func, m_param1, m_param2, m_eq) \
+	CHECK(m_obj.m_func(m_param1, m_param2) == m_eq);                      \
+	CHECK(m_obj.m_func(U##m_param1, m_param2) == m_eq);                   \
+	CHECK(m_obj.m_func(L##m_param1, m_param2) == m_eq);                   \
+	CHECK(m_obj.m_func(String(m_param1), m_param2) == m_eq);
+
+#define MULTICHECK_STRING_INT_INT_EQ(m_obj, m_func, m_param1, m_param2, m_param3, m_eq) \
+	CHECK(m_obj.m_func(m_param1, m_param2, m_param3) == m_eq);                          \
+	CHECK(m_obj.m_func(U##m_param1, m_param2, m_param3) == m_eq);                       \
+	CHECK(m_obj.m_func(L##m_param1, m_param2, m_param3) == m_eq);                       \
+	CHECK(m_obj.m_func(String(m_param1), m_param2, m_param3) == m_eq);
+
+#define MULTICHECK_STRING_STRING_EQ(m_obj, m_func, m_param1, m_param2, m_eq) \
+	CHECK(m_obj.m_func(m_param1, m_param2) == m_eq);                         \
+	CHECK(m_obj.m_func(U##m_param1, U##m_param2) == m_eq);                   \
+	CHECK(m_obj.m_func(L##m_param1, L##m_param2) == m_eq);                   \
+	CHECK(m_obj.m_func(String(m_param1), String(m_param2)) == m_eq);
+
+#define MULTICHECK_GET_SLICE(m_obj, m_param1, m_slices)                 \
+	for (int i = 0; i < m_obj.get_slice_count(m_param1); ++i) {         \
+		CHECK(m_obj.get_slice(m_param1, i) == m_slices[i]);             \
+	}                                                                   \
+	for (int i = 0; i < m_obj.get_slice_count(U##m_param1); ++i) {      \
+		CHECK(m_obj.get_slice(U##m_param1, i) == m_slices[i]);          \
+	}                                                                   \
+	for (int i = 0; i < m_obj.get_slice_count(L##m_param1); ++i) {      \
+		CHECK(m_obj.get_slice(L##m_param1, i) == m_slices[i]);          \
+	}                                                                   \
+	for (int i = 0; i < m_obj.get_slice_count(String(m_param1)); ++i) { \
+		CHECK(m_obj.get_slice(String(m_param1), i) == m_slices[i]);     \
+	}
+
+#define MULTICHECK_SPLIT(m_obj, m_func, m_param1, m_param2, m_param3, m_slices, m_expected_size) \
+	do {                                                                                         \
+		Vector<String> string_list;                                                              \
+                                                                                                 \
+		string_list = m_obj.m_func(m_param1, m_param2, m_param3);                                \
+		CHECK(m_expected_size == string_list.size());                                            \
+		for (int i = 0; i < string_list.size(); ++i) {                                           \
+			CHECK(string_list[i] == m_slices[i]);                                                \
+		}                                                                                        \
+                                                                                                 \
+		string_list = m_obj.m_func(U##m_param1, m_param2, m_param3);                             \
+		CHECK(m_expected_size == string_list.size());                                            \
+		for (int i = 0; i < string_list.size(); ++i) {                                           \
+			CHECK(string_list[i] == m_slices[i]);                                                \
+		}                                                                                        \
+                                                                                                 \
+		string_list = m_obj.m_func(L##m_param1, m_param2, m_param3);                             \
+		CHECK(m_expected_size == string_list.size());                                            \
+		for (int i = 0; i < string_list.size(); ++i) {                                           \
+			CHECK(string_list[i] == m_slices[i]);                                                \
+		}                                                                                        \
+                                                                                                 \
+		string_list = m_obj.m_func(String(m_param1), m_param2, m_param3);                        \
+		CHECK(m_expected_size == string_list.size());                                            \
+		for (int i = 0; i < string_list.size(); ++i) {                                           \
+			CHECK(string_list[i] == m_slices[i]);                                                \
+		}                                                                                        \
+	} while (0)
+
 #endif // TEST_MACROS_H