From 61784079403dd68b95dc304991562bb6505c066b Mon Sep 17 00:00:00 2001 From: Raul Santos Date: Wed, 5 Oct 2022 18:15:39 +0200 Subject: [PATCH] C#: Cleanup and sync StringExtensions with core - Moved `GetBaseName` to keep methods alphabetically sorted. - Removed `Length`, users should just use the Length property. - Removed `Insert`, string already has a method with the same signature that takes precedence. - Removed `Erase`. - Removed `ToLower` and `ToUpper`, string already has methods with the same signature that take precedence. - Removed `FindLast` in favor of `RFind`. - Replaced `RFind` and `RFindN` implemenation with a ca ll to `string.LastIndexOf` to avoid marshaling. - Added `LPad` and `RPad`. - Added `StripEscapes`. - Replaced `LStrip` and `RStrip` implementation with a call to `string.TrimStart` and `string.TrimEnd`. - Added `TrimPrefix` and `TrimSuffix`. - Renamed `OrdAt` to `UnicodeAt`. - Added `CountN` and move the `caseSensitive` parameter of `Count` to the end. - Added `Indent` and `Dedent`. --- .../Core/NativeInterop/NativeFuncs.cs | 15 - .../GodotSharp/Core/StringExtensions.cs | 445 +++++++++++------- modules/mono/glue/runtime_interop.cpp | 30 -- 3 files changed, 278 insertions(+), 212 deletions(-) diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs index b30b6a07525..c7deb6423b4 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/NativeInterop/NativeFuncs.cs @@ -414,21 +414,6 @@ namespace Godot.NativeInterop // StringExtensions - public static partial void godotsharp_string_md5_buffer(in godot_string p_self, - out godot_packed_byte_array r_md5_buffer); - - public static partial void godotsharp_string_md5_text(in godot_string p_self, out godot_string r_md5_text); - - public static partial int godotsharp_string_rfind(in godot_string p_self, in godot_string p_what, int p_from); - - public static partial int godotsharp_string_rfindn(in godot_string p_self, in godot_string p_what, int p_from); - - public static partial void godotsharp_string_sha256_buffer(in godot_string p_self, - out godot_packed_byte_array r_sha256_buffer); - - public static partial void godotsharp_string_sha256_text(in godot_string p_self, - out godot_string r_sha256_text); - public static partial void godotsharp_string_simplify_path(in godot_string p_self, out godot_string r_simplified_path); diff --git a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs index f511233fccf..4988910fbdd 100644 --- a/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs +++ b/modules/mono/glue/GodotSharp/GodotSharp/Core/StringExtensions.cs @@ -66,24 +66,6 @@ namespace Godot return string.Empty; } - /// - /// If the string is a path to a file, return the path to the file without the extension. - /// - /// - /// - /// - /// The path to a file. - /// The path to the file without the extension. - public static string GetBaseName(this string instance) - { - int index = instance.LastIndexOf('.'); - - if (index > 0) - return instance.Substring(0, index); - - return instance; - } - /// /// Returns if the strings begins /// with the given string . @@ -144,15 +126,15 @@ namespace Godot } /// - /// Returns the amount of substrings in the string. + /// Returns the number of occurrences of substring in the string. /// /// The string where the substring will be searched. /// The substring that will be counted. - /// If the search is case sensitive. /// Index to start searching from. /// Index to stop searching at. - /// Amount of substrings in the string. - public static int Count(this string instance, string what, bool caseSensitive = true, int from = 0, int to = 0) + /// If the search is case sensitive. + /// Number of occurrences of the substring in the string. + public static int Count(this string instance, string what, int from = 0, int to = 0, bool caseSensitive = true) { if (what.Length == 0) { @@ -210,6 +192,82 @@ namespace Godot return c; } + /// + /// Returns the number of occurrences of substring (ignoring case) + /// between and positions. If + /// and equals 0 the whole string will be used. If only + /// equals 0 the remained substring will be used. + /// + /// The string where the substring will be searched. + /// The substring that will be counted. + /// Index to start searching from. + /// Index to stop searching at. + /// Number of occurrences of the substring in the string. + public static int CountN(this string instance, string what, int from = 0, int to = 0) + { + return instance.Count(what, from, to, caseSensitive: false); + } + + /// + /// Returns a copy of the string with indentation (leading tabs and spaces) removed. + /// See also to add indentation. + /// + /// The string to remove the indentation from. + /// The string with the indentation removed. + public static string Dedent(this string instance) + { + var sb = new StringBuilder(); + string indent = ""; + bool hasIndent = false; + bool hasText = false; + int lineStart = 0; + int indentStop = -1; + + for (int i = 0; i < instance.Length; i++) + { + char c = instance[i]; + if (c == '\n') + { + if (hasText) + { + sb.Append(instance.Substring(indentStop, i - indentStop)); + } + sb.Append('\n'); + hasText = false; + lineStart = i + 1; + indentStop = -1; + } + else if (!hasText) + { + if (c > 32) + { + hasText = true; + if (!hasIndent) + { + hasIndent = true; + indent = instance.Substring(lineStart, i - lineStart); + indentStop = i; + } + } + if (hasIndent && indentStop < 0) + { + int j = i - lineStart; + if (j >= indent.Length || c != indent[j]) + { + indentStop = i; + } + } + } + } + + if (hasText) + { + sb.Append(instance.Substring(indentStop, instance.Length - indentStop)); + } + + return sb.ToString(); + } + /// /// Returns a copy of the string with special characters escaped using the C language standard. /// @@ -454,17 +512,6 @@ namespace Godot return instance.EndsWith(text); } - /// - /// Erase characters from the string starting from . - /// - /// The string to modify. - /// Starting position from which to erase. - /// Amount of characters to erase. - public static void Erase(this StringBuilder instance, int pos, int chars) - { - instance.Remove(pos, chars); - } - /// /// Returns the extension without the leading period character (.) /// if the string is a valid file name or path. If the string does not contain @@ -489,7 +536,7 @@ namespace Godot /// The extension of the file or an empty string. public static string GetExtension(this string instance) { - int pos = instance.FindLast("."); + int pos = instance.RFind("."); if (pos < 0) return instance; @@ -498,12 +545,16 @@ namespace Godot } /// - /// Find the first occurrence of a substring. Optionally, the search starting position can be passed. + /// Returns the index of the first occurrence of the specified string in this instance, + /// or -1. Optionally, the starting search index can be specified, continuing + /// to the end of the string. + /// Note: If you just want to know whether a string contains a substring, use the + /// method. /// /// - /// - /// /// + /// + /// /// The string that will be searched. /// The substring to find. /// The search starting position. @@ -519,9 +570,9 @@ namespace Godot /// Find the first occurrence of a char. Optionally, the search starting position can be passed. /// /// - /// - /// /// + /// + /// /// The string that will be searched. /// The substring to find. /// The search starting position. @@ -529,50 +580,21 @@ namespace Godot /// The first instance of the char, or -1 if not found. public static int Find(this string instance, char what, int from = 0, bool caseSensitive = true) { - // TODO: Could be more efficient if we get a char version of `IndexOf`. - // See https://github.com/dotnet/runtime/issues/44116 - return instance.IndexOf(what.ToString(), from, - caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); - } + if (caseSensitive) + return instance.IndexOf(what, from); - /// Find the last occurrence of a substring. - /// - /// - /// - /// - /// The string that will be searched. - /// The substring to find. - /// If , the search is case sensitive. - /// The starting position of the substring, or -1 if not found. - public static int FindLast(this string instance, string what, bool caseSensitive = true) - { - return instance.FindLast(what, instance.Length - 1, caseSensitive); - } - - /// Find the last occurrence of a substring specifying the search starting position. - /// - /// - /// - /// - /// The string that will be searched. - /// The substring to find. - /// The search starting position. - /// If , the search is case sensitive. - /// The starting position of the substring, or -1 if not found. - public static int FindLast(this string instance, string what, int from, bool caseSensitive = true) - { - return instance.LastIndexOf(what, from, - caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + return CultureInfo.InvariantCulture.CompareInfo.IndexOf(instance, what, from, CompareOptions.OrdinalIgnoreCase); } /// - /// Find the first occurrence of a substring but search as case-insensitive. - /// Optionally, the search starting position can be passed. + /// Returns the index of the first case-insensitive occurrence of the specified string in this instance, + /// or -1. Optionally, the starting search index can be specified, continuing + /// to the end of the string. /// /// /// - /// - /// + /// + /// /// The string that will be searched. /// The substring to find. /// The search starting position. @@ -616,7 +638,7 @@ namespace Godot } } - int sep = Mathf.Max(rs.FindLast("/"), rs.FindLast("\\")); + int sep = Mathf.Max(rs.RFind("/"), rs.RFind("\\")); if (sep == -1) return directory; @@ -624,6 +646,24 @@ namespace Godot return directory + rs.Substr(0, sep); } + /// + /// If the string is a path to a file, return the path to the file without the extension. + /// + /// + /// + /// + /// The path to a file. + /// The path to the file without the extension. + public static string GetBaseName(this string instance) + { + int index = instance.RFind("."); + + if (index > 0) + return instance.Substring(0, index); + + return instance; + } + /// /// If the string is a path to a file, return the file and ignore the base directory. /// @@ -634,7 +674,7 @@ namespace Godot /// The file name. public static string GetFile(this string instance) { - int sep = Mathf.Max(instance.FindLast("/"), instance.FindLast("\\")); + int sep = Mathf.Max(instance.RFind("/"), instance.RFind("\\")); if (sep == -1) return instance; @@ -766,18 +806,44 @@ namespace Godot } /// - /// Inserts a substring at a given position. + /// Returns a copy of the string with lines indented with . + /// For example, the string can be indented with two tabs using "\t\t", + /// or four spaces using " ". The prefix can be any string so it can + /// also be used to comment out strings with e.g. "// . + /// See also to remove indentation. + /// Note: Empty lines are kept empty. /// - /// The string to modify. - /// Position at which to insert the substring. - /// Substring to insert. - /// - /// The string with inserted at the given - /// position . - /// - public static string Insert(this string instance, int pos, string what) + /// The string to add indentation to. + /// The string to use as indentation. + /// The string with indentation added. + public static string Indent(this string instance, string prefix) { - return instance.Insert(pos, what); + var sb = new StringBuilder(); + int lineStart = 0; + + for (int i = 0; i < instance.Length; i++) + { + char c = instance[i]; + if (c == '\n') + { + if (i == lineStart) + { + sb.Append(c); // Leave empty lines empty. + } + else + { + sb.Append(prefix); + sb.Append(instance.Substring(lineStart, i - lineStart + 1)); + } + lineStart = i + 1; + } + } + if (lineStart != instance.Length) + { + sb.Append(prefix); + sb.Append(instance.Substring(lineStart)); + } + return sb.ToString(); } /// @@ -1003,17 +1069,24 @@ namespace Godot } /// - /// Returns the length of the string in characters. + /// Formats a string to be at least long by + /// adding s to the left of the string. /// - /// The string to check. - /// The length of the string. - public static int Length(this string instance) + /// String that will be padded. + /// Minimum length that the resulting string must have. + /// Character to add to the left of the string. + /// String padded with the specified character. + public static string LPad(this string instance, int minLength, char character = ' ') { - return instance.Length; + return instance.PadLeft(minLength, character); } /// /// Returns a copy of the string with characters removed from the left. + /// The argument is a string specifying the set of characters + /// to be removed. + /// Note: The is not a prefix. See + /// method that will remove a single prefix string rather than a set of characters. /// /// /// The string to remove characters from. @@ -1021,23 +1094,7 @@ namespace Godot /// A copy of the string with characters removed from the left. public static string LStrip(this string instance, string chars) { - int len = instance.Length; - int beg; - - for (beg = 0; beg < len; beg++) - { - if (chars.Find(instance[beg]) == -1) - { - break; - } - } - - if (beg == 0) - { - return instance; - } - - return instance.Substr(beg, len - beg); + return instance.TrimStart(chars.ToCharArray()); } /// @@ -1150,17 +1207,6 @@ namespace Godot return instance.CompareTo(to, caseSensitive: false); } - /// - /// Returns the character code at position . - /// - /// The string to check. - /// The position int the string for the character to check. - /// The character code. - public static int OrdAt(this string instance, int at) - { - return instance[at]; - } - /// /// Format a number to have an exact number of /// after the decimal point. @@ -1282,34 +1328,47 @@ namespace Godot } /// - /// Perform a search for a substring, but start from the end of the string instead of the beginning. + /// Returns the index of the last occurrence of the specified string in this instance, + /// or -1. Optionally, the starting search index can be specified, continuing to + /// the beginning of the string. /// + /// + /// + /// /// /// The string that will be searched. /// The substring to search in the string. /// The position at which to start searching. + /// If , the search is case sensitive. /// The position at which the substring was found, or -1 if not found. - public static int RFind(this string instance, string what, int from = -1) + public static int RFind(this string instance, string what, int from = -1, bool caseSensitive = true) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - using godot_string whatStr = Marshaling.ConvertStringToNative(instance); - return NativeFuncs.godotsharp_string_rfind(instanceStr, whatStr, from); + if (from == -1) + from = instance.Length - 1; + + return instance.LastIndexOf(what, from, + caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); } /// - /// Perform a search for a substring, but start from the end of the string instead of the beginning. - /// Also search case-insensitive. + /// Returns the index of the last case-insensitive occurrence of the specified string in this instance, + /// or -1. Optionally, the starting search index can be specified, continuing to + /// the beginning of the string. /// - /// + /// + /// + /// + /// /// The string that will be searched. /// The substring to search in the string. /// The position at which to start searching. /// The position at which the substring was found, or -1 if not found. public static int RFindN(this string instance, string what, int from = -1) { - using godot_string instanceStr = Marshaling.ConvertStringToNative(instance); - using godot_string whatStr = Marshaling.ConvertStringToNative(instance); - return NativeFuncs.godotsharp_string_rfindn(instanceStr, whatStr, from); + if (from == -1) + from = instance.Length - 1; + + return instance.LastIndexOf(what, from, StringComparison.OrdinalIgnoreCase); } /// @@ -1330,8 +1389,25 @@ namespace Godot return instance.Substring(pos, instance.Length - pos); } + /// + /// Formats a string to be at least long by + /// adding s to the right of the string. + /// + /// String that will be padded. + /// Minimum length that the resulting string must have. + /// Character to add to the right of the string. + /// String padded with the specified character. + public static string RPad(this string instance, int minLength, char character = ' ') + { + return instance.PadRight(minLength, character); + } + /// /// Returns a copy of the string with characters removed from the right. + /// The argument is a string specifying the set of characters + /// to be removed. + /// Note: The is not a suffix. See + /// method that will remove a single suffix string rather than a set of characters. /// /// /// The string to remove characters from. @@ -1339,16 +1415,8 @@ namespace Godot /// A copy of the string with characters removed from the right. public static string RStrip(this string instance, string chars) { - int len = instance.Length; - int end; - - for (end = len - 1; end >= 0; end--) - { - if (chars.Find(instance[end]) == -1) - { - break; - } - } + return instance.TrimEnd(chars.ToCharArray()); + } if (end == len - 1) { @@ -1455,7 +1523,7 @@ namespace Godot /// The array of strings split from the string. public static string[] Split(this string instance, string divisor, bool allowEmpty = true) { - return instance.Split(new[] { divisor }, + return instance.Split(divisor, allowEmpty ? StringSplitOptions.None : StringSplitOptions.RemoveEmptyEntries); } @@ -1503,8 +1571,10 @@ namespace Godot }; /// - /// Returns a copy of the string stripped of any non-printable character at the beginning and the end. - /// The optional arguments are used to toggle stripping on the left and right edges respectively. + /// Returns a copy of the string stripped of any non-printable character + /// (including tabulations, spaces and line breaks) at the beginning and the end. + /// The optional arguments are used to toggle stripping on the left and right + /// edges respectively. /// /// The string to strip. /// If the left side should be stripped. @@ -1522,6 +1592,30 @@ namespace Godot return instance.TrimEnd(_nonPrintable); } + + /// + /// Returns a copy of the string stripped of any escape character. + /// These include all non-printable control characters of the first page + /// of the ASCII table (< 32), such as tabulation (\t) and + /// newline (\n and \r) characters, but not spaces. + /// + /// The string to strip. + /// The string stripped of any escape characters. + public static string StripEscapes(this string instance) + { + var sb = new StringBuilder(); + for (int i = 0; i < instance.Length; i++) + { + // Escape characters on first page of the ASCII table, before 32 (Space). + if (instance[i] < 32) + continue; + + sb.Append(instance[i]); + } + + return sb.ToString(); + } + /// /// Returns part of the string from the position , with length . /// @@ -1572,28 +1666,6 @@ namespace Godot return int.Parse(instance); } - /// - /// Returns the string converted to lowercase. - /// - /// - /// The string to convert. - /// The string converted to lowercase. - public static string ToLower(this string instance) - { - return instance.ToLower(); - } - - /// - /// Returns the string converted to uppercase. - /// - /// - /// The string to convert. - /// The string converted to uppercase. - public static string ToUpper(this string instance) - { - return instance.ToUpper(); - } - /// /// Converts the String (which is an array of characters) to PackedByteArray (which is an array of bytes). /// The conversion is a bit slower than , but supports all UTF-8 characters. @@ -1607,6 +1679,45 @@ namespace Godot return Encoding.UTF8.GetBytes(instance); } + /// + /// Removes a given string from the start if it starts with it or leaves the string unchanged. + /// + /// The string to remove the prefix from. + /// The string to remove from the start. + /// A copy of the string with the prefix string removed from the start. + public static string TrimPrefix(this string instance, string prefix) + { + if (instance.StartsWith(prefix)) + return instance.Substring(prefix.Length); + + return instance; + } + + /// + /// Removes a given string from the end if it ends with it or leaves the string unchanged. + /// + /// The string to remove the suffix from. + /// The string to remove from the end. + /// A copy of the string with the suffix string removed from the end. + public static string TrimSuffix(this string instance, string suffix) + { + if (instance.EndsWith(suffix)) + return instance.Substring(0, instance.Length - suffix.Length); + + return instance; + } + + /// + /// Returns the character code at position . + /// + /// The string to check. + /// The position int the string for the character to check. + /// The character code. + public static int UnicodeAt(this string instance, int at) + { + return instance[at]; + } + /// /// Decodes a string in URL encoded format. This is meant to /// decode parameters in a URL when receiving an HTTP request. diff --git a/modules/mono/glue/runtime_interop.cpp b/modules/mono/glue/runtime_interop.cpp index e20a88076af..338e5a01477 100644 --- a/modules/mono/glue/runtime_interop.cpp +++ b/modules/mono/glue/runtime_interop.cpp @@ -1067,30 +1067,6 @@ void godotsharp_dictionary_to_string(const Dictionary *p_self, String *r_str) { *r_str = Variant(*p_self).operator String(); } -void godotsharp_string_md5_buffer(const String *p_self, PackedByteArray *r_md5_buffer) { - memnew_placement(r_md5_buffer, PackedByteArray(p_self->md5_buffer())); -} - -void godotsharp_string_md5_text(const String *p_self, String *r_md5_text) { - memnew_placement(r_md5_text, String(p_self->md5_text())); -} - -int32_t godotsharp_string_rfind(const String *p_self, const String *p_what, int32_t p_from) { - return p_self->rfind(*p_what, p_from); -} - -int32_t godotsharp_string_rfindn(const String *p_self, const String *p_what, int32_t p_from) { - return p_self->rfindn(*p_what, p_from); -} - -void godotsharp_string_sha256_buffer(const String *p_self, PackedByteArray *r_sha256_buffer) { - memnew_placement(r_sha256_buffer, PackedByteArray(p_self->sha256_buffer())); -} - -void godotsharp_string_sha256_text(const String *p_self, String *r_sha256_text) { - memnew_placement(r_sha256_text, String(p_self->sha256_text())); -} - void godotsharp_string_simplify_path(const String *p_self, String *r_simplified_path) { memnew_placement(r_simplified_path, String(p_self->simplify_path())); } @@ -1473,12 +1449,6 @@ static const void *unmanaged_callbacks[]{ (void *)godotsharp_dictionary_duplicate, (void *)godotsharp_dictionary_remove_key, (void *)godotsharp_dictionary_to_string, - (void *)godotsharp_string_md5_buffer, - (void *)godotsharp_string_md5_text, - (void *)godotsharp_string_rfind, - (void *)godotsharp_string_rfindn, - (void *)godotsharp_string_sha256_buffer, - (void *)godotsharp_string_sha256_text, (void *)godotsharp_string_simplify_path, (void *)godotsharp_string_to_camel_case, (void *)godotsharp_string_to_pascal_case,