Skip to main content

godot_core/builtin/strings/
string_macros.rs

1/*
2 * Copyright (c) godot-rust; Bromeon and contributors.
3 * This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6 */
7
8#[macro_export]
9macro_rules! impl_shared_string_api {
10    (
11        builtin: $Builtin:ty,
12        builtin_mod: $builtin_mod:ident,
13    ) => {
14        // --------------------------------------------------------------------------------------------------------------------------------------
15        // Extending the builtin itself
16
17        /// Manually-declared, shared methods between `GString` and `StringName`.
18        impl $Builtin {
19            /// Returns the Unicode code point ("character") at position `index`.
20            ///
21            /// # Panics (safeguards-balanced)
22            /// If `index` is out of bounds. In disengaged level, `0` is returned instead.
23            // Unicode conversion panic is not documented because we rely on Godot strings having valid Unicode.
24            // TODO implement Index/IndexMut (for GString; StringName may have varying reprs).
25            pub fn unicode_at(&self, index: usize) -> char {
26                sys::balanced_assert!(index < self.len(), "unicode_at: index {} out of bounds (len {})", index, self.len());
27
28                let char_i64 = self.as_inner().unicode_at(index as i64);
29
30                u32::try_from(char_i64).ok()
31                    .and_then(|char_u32| char::from_u32(char_u32))
32                    .unwrap_or_else(|| {
33                        panic!("cannot map Unicode code point (value {char_i64}) to char (at position {index})")
34                    })
35            }
36
37            /// Find first occurrence of `what` and return index, or `None` if not found.
38            ///
39            /// Check [`find_ex()`](Self::find_ex) for all custom options.
40            pub fn find(&self, what: impl AsArg<GString>) -> Option<usize> {
41                self.find_ex(what).done()
42            }
43
44            /// Returns a builder for finding substrings, with various configuration options.
45            ///
46            /// The builder struct offers methods to configure 3 dimensions, which map to different Godot functions in the back:
47            ///
48            /// | Method        | Default behavior                          | Behavior after method call         |
49            /// |---------------|-------------------------------------------|------------------------------------|
50            /// | `r()`         | forward search (`find*`)                  | backward search (`rfind*`)         |
51            /// | `n()`         | case-sensitive search (`*find`)           | case-insensitive search (`*findn`) |
52            /// | `from(index)` | search from beginning (or end if reverse) | search from specified index        |
53            ///
54            /// Returns `Some(index)` of the first occurrence, or `None` if not found.
55            ///
56            /// # Example
57            /// To find the substring `"O"` in `"Hello World"`, not considering case and starting from position 5, you can write:
58            /// ```no_run
59            /// # use godot::prelude::*;
60            /// # fn do_sth_with_index(_i: usize) {}
61            /// let s = GString::from("Hello World");
62            /// if let Some(found) = s.find_ex("O").n().from(5).done() {
63            ///    do_sth_with_index(found)
64            /// }
65            /// ```
66            /// This is equivalent to the following GDScript code:
67            /// ```gdscript
68            /// var s: GString = "Hello World"
69            /// var found = s.findn("O", 5)
70            /// if found != -1:
71            ///     do_sth_with_index(found)
72            /// ```
73            #[doc(alias = "findn", alias = "rfind", alias = "rfindn")]
74            pub fn find_ex<'s, 'w>(
75                &'s self,
76                what: impl AsArg<GString> + 'w,
77            ) -> ExFind<'s, 'w> {
78                ExFind::new(self, what.into_arg())
79            }
80
81            /// Count how many times `what` appears within `range`. Use `..` for full string search.
82            pub fn count(&self, what: impl AsArg<GString>, range: impl std::ops::RangeBounds<usize>) -> usize {
83                let (from, to) = $crate::meta::signed_range::to_godot_range_fromto(range);
84                self.as_inner().count(what.into_arg(), from, to) as usize
85            }
86
87            /// Count how many times `what` appears within `range`, case-insensitively. Use `..` for full string search.
88            pub fn countn(&self, what: impl AsArg<GString>, range: impl std::ops::RangeBounds<usize>) -> usize {
89                let (from, to) = $crate::meta::signed_range::to_godot_range_fromto(range);
90                self.as_inner().countn(what.into_arg(), from, to) as usize
91            }
92
93            /// Splits the string according to `delimiter`.
94            ///
95            /// See [`split_ex()`][Self::split_ex] if you need further configuration.
96            pub fn split(&self, delimiter: impl AsArg<GString>) -> $crate::builtin::PackedStringArray {
97                self.split_ex(delimiter).done()
98            }
99
100            /// Returns a builder that splits this string into substrings using `delimiter`.
101            ///
102            /// If `delimiter` is an empty string, each substring will be a single character.
103            ///
104            /// The builder struct offers methods to configure multiple dimensions. Note that `rsplit` in Godot is not useful without the
105            /// `maxsplit` argument, so the two are combined in Rust as `maxsplit_r`.
106            ///
107            /// | Method             | Default behavior       | Behavior after method call                        |
108            /// |--------------------|------------------------|---------------------------------------------------|
109            /// | `disallow_empty()` | allows empty parts     | empty parts are removed from result               |
110            /// | `maxsplit(n)`      | entire string is split | splits `n` times -> `n+1` parts                   |
111            /// | `maxsplit_r(n)`    | entire string is split | splits `n` times -> `n+1` parts (start from back) |
112            #[doc(alias = "rsplit")]
113            pub fn split_ex<'s, 'w>(
114                &'s self,
115                delimiter: impl AsArg<GString> + 'w,
116            ) -> ExSplit<'s, 'w> {
117                ExSplit::new(self, delimiter.into_arg())
118            }
119
120            /// Returns a substring of this, as another `GString`.
121            // TODO is there no efficient way to implement this for StringName by interning?
122            pub fn substr(&self, range: impl std::ops::RangeBounds<usize>) -> GString {
123                let (from, len) = $crate::meta::signed_range::to_godot_range_fromlen(range, -1);
124
125                self.as_inner().substr(from, len)
126            }
127
128            /// Splits the string using a string delimiter and returns the substring at index `slice`.
129            ///
130            /// Returns the original string if delimiter does not occur in the string. Returns `None` if `slice` is out of bounds.
131            ///
132            /// This is faster than [`split()`][Self::split], if you only need one substring.
133            pub fn get_slice(
134                &self,
135                delimiter: impl AsArg<GString>,
136                slice: usize,
137            ) -> Option<GString> {
138                let sliced = self.as_inner().get_slice(delimiter, slice as i64);
139
140                // Note: self="" always returns None.
141                super::populated_or_none(sliced)
142            }
143
144            /// Splits the string using a Unicode char `delimiter` and returns the substring at index `slice`.
145            ///
146            /// Returns the original string if delimiter does not occur in the string. Returns `None` if `slice` is out of bounds.
147            ///
148            /// This is faster than [`split()`][Self::split], if you only need one substring.
149            pub fn get_slicec(&self, delimiter: char, slice: usize) -> Option<GString> {
150                let sliced = self.as_inner().get_slicec(delimiter as i64, slice as i64);
151
152                // Note: self="" always returns None.
153                super::populated_or_none(sliced)
154            }
155
156            /// Returns the total number of slices, when the string is split with the given delimiter.
157            ///
158            /// See also [`split()`][Self::split] and [`get_slice()`][Self::get_slice].
159            pub fn get_slice_count(&self, delimiter: impl AsArg<GString>) -> usize {
160                self.as_inner().get_slice_count(delimiter) as usize
161            }
162
163            /// Returns a copy of the string without the specified index range.
164            pub fn erase(&self, range: impl std::ops::RangeBounds<usize>) -> GString {
165                let (from, len) = $crate::meta::signed_range::to_godot_range_fromlen(range, i32::MAX as i64);
166                self.as_inner().erase(from, len)
167            }
168
169            /// Returns a copy of the string with an additional string inserted at the given position.
170            ///
171            /// If the position is out of bounds, the string will be inserted at the end.
172            ///
173            /// Consider using [`format()`](Self::format) for more flexibility.
174            pub fn insert(&self, position: usize, what: impl AsArg<GString>) -> GString {
175                self.as_inner().insert(position as i64, what)
176            }
177
178            /// Format a string using substitutions from an array or dictionary.
179            ///
180            /// See Godot's [`String.format()`](https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-format).
181            pub fn format(&self, array_or_dict: &Variant) -> GString {
182                self.as_inner().format(array_or_dict, "{_}")
183            }
184
185            /// Format a string using substitutions from an array or dictionary + custom placeholder.
186            ///
187            /// See Godot's [`String.format()`](https://docs.godotengine.org/en/stable/classes/class_string.html#class-string-method-format).
188            pub fn format_with_placeholder(
189                &self,
190                array_or_dict: &Variant,
191                placeholder: impl AsArg<GString>,
192            ) -> GString {
193                self.as_inner().format(array_or_dict, placeholder)
194            }
195
196            // left() + right() are not redefined, as their i64 can be negative.
197
198            /// Formats the string to be at least `min_length` long, by adding characters to the left of the string, if necessary.
199            ///
200            /// Godot itself allows padding with multiple characters, but that behavior is not very useful, because `min_length` isn't
201            /// respected in that case. The parameter in Godot is even called `character`. In Rust, we directly expose `char` instead.
202            ///
203            /// See also [`rpad()`](Self::rpad).
204            pub fn lpad(&self, min_length: usize, character: char) -> GString {
205                let one_char_string = GString::from([character].as_slice());
206                self.as_inner().lpad(min_length as i64, &one_char_string)
207            }
208
209            /// Formats the string to be at least `min_length` long, by adding characters to the right of the string, if necessary.
210            ///
211            /// Godot itself allows padding with multiple characters, but that behavior is not very useful, because `min_length` isn't
212            /// respected in that case. The parameter in Godot is even called `character`. In Rust, we directly expose `char` instead.
213            ///
214            /// See also [`lpad()`](Self::lpad).
215            pub fn rpad(&self, min_length: usize, character: char) -> GString {
216                let one_char_string = GString::from([character].as_slice());
217                self.as_inner().rpad(min_length as i64, &one_char_string)
218            }
219
220            /// Formats the string representing a number to have an exact number of `digits` _after_ the decimal point.
221            pub fn pad_decimals(&self, digits: usize) -> GString {
222                self.as_inner().pad_decimals(digits as i64)
223            }
224
225            /// Formats the string representing a number to have an exact number of `digits` _before_ the decimal point.
226            pub fn pad_zeros(&self, digits: usize) -> GString {
227                self.as_inner().pad_zeros(digits as i64)
228            }
229
230            /// Case-sensitive, lexicographic comparison to another string.
231            ///
232            /// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which
233            /// roughly matches the alphabetical order.
234            ///
235            /// See also [`nocasecmp_to()`](Self::nocasecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
236            pub fn casecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
237                sys::i64_to_ordering(self.as_inner().casecmp_to(to))
238            }
239
240            /// Case-**insensitive**, lexicographic comparison to another string.
241            ///
242            /// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which
243            /// roughly matches the alphabetical order.
244            ///
245            /// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
246            pub fn nocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
247                sys::i64_to_ordering(self.as_inner().nocasecmp_to(to))
248            }
249
250            /// Case-sensitive, **natural-order** comparison to another string.
251            ///
252            /// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which
253            /// roughly matches the alphabetical order.
254            ///
255            /// When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often
256            /// expected, instead of the single digit's value. A sorted sequence of numbered strings will be `["1", "2", "3", ...]`, not
257            /// `["1", "10", "2", "3", ...]`.
258            ///
259            /// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less`
260            /// if shorter.
261            ///
262            /// See also [`casecmp_to()`](Self::casecmp_to), [`naturalnocasecmp_to()`](Self::naturalnocasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
263            pub fn naturalcasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
264                sys::i64_to_ordering(self.as_inner().naturalcasecmp_to(to))
265            }
266
267            /// Case-insensitive, **natural-order** comparison to another string.
268            ///
269            /// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which
270            /// roughly matches the alphabetical order.
271            ///
272            /// When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often
273            /// expected, instead of the single digit's value. A sorted sequence of numbered strings will be `["1", "2", "3", ...]`, not
274            /// `["1", "10", "2", "3", ...]`.
275            ///
276            /// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less`
277            /// if shorter.
278            ///
279            /// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
280            pub fn naturalnocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
281                sys::i64_to_ordering(self.as_inner().naturalnocasecmp_to(to))
282            }
283
284            /// Case-sensitive, filename-oriented comparison to another string.
285            ///
286            /// Like [`naturalcasecmp_to()`][Self::naturalcasecmp_to], but prioritizes strings that begin with periods (`.`) and underscores
287            /// (`_`) before any other character. Useful when sorting folders or file names.
288            ///
289            /// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filenocasecmp_to()`](Self::filenocasecmp_to).
290            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
291            pub fn filecasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
292                sys::i64_to_ordering(self.as_inner().filecasecmp_to(to))
293            }
294
295            /// Case-insensitive, filename-oriented comparison to another string.
296            ///
297            /// Like [`naturalnocasecmp_to()`][Self::naturalnocasecmp_to], but prioritizes strings that begin with periods (`.`) and underscores
298            /// (`_`) before any other character. Useful when sorting folders or file names.
299            ///
300            /// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
301            #[cfg(since_api = "4.3")] #[cfg_attr(published_docs, doc(cfg(since_api = "4.3")))]
302            pub fn filenocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
303                sys::i64_to_ordering(self.as_inner().filenocasecmp_to(to))
304            }
305
306            /// Simple expression match (also called "glob" or "globbing"), where `*` matches zero or more arbitrary characters and `?`
307            /// matches any single character except a period (`.`).
308            ///
309            /// An empty string or empty expression always evaluates to `false`.
310            ///
311            /// Renamed from `match` because of collision with Rust keyword + possible confusion with `String::matches()` that can match regex.
312            #[doc(alias = "match")]
313            pub fn match_glob(&self, pattern: impl AsArg<GString>) -> bool {
314                self.as_inner().match_(pattern)
315            }
316
317            /// Simple **case-insensitive** expression match (also called "glob" or "globbing"), where `*` matches zero or more arbitrary
318            /// characters and `?` matches any single character except a period (`.`).
319            ///
320            /// An empty string or empty expression always evaluates to `false`.
321            ///
322            /// Renamed from `matchn` because of collision with Rust keyword + possible confusion with `String::matches()` that can match regex.
323            #[doc(alias = "matchn")]
324            pub fn matchn_glob(&self, pattern: impl AsArg<GString>) -> bool {
325                self.as_inner().matchn(pattern)
326            }
327        }
328
329        // --------------------------------------------------------------------------------------------------------------------------------------
330        // find() support
331
332        #[doc = concat!("Manual extender for [`", stringify!($Builtin), "::find_ex`][crate::builtin::", stringify!($builtin_mod), "::ExFind].")]
333        #[must_use]
334        pub struct ExFind<'s, 'w> {
335            owner: &'s $Builtin,
336            what: meta::CowArg<'w, GString>,
337            reverse: bool,
338            case_insensitive: bool,
339            from_index: Option<usize>,
340        }
341
342        impl<'s, 'w> ExFind<'s, 'w> {
343            pub(crate) fn new(owner: &'s $Builtin, what: meta::CowArg<'w, GString>) -> Self {
344                Self {
345                    owner,
346                    what,
347                    reverse: false,
348                    case_insensitive: false,
349                    from_index: None,
350                }
351            }
352
353            /// Reverse search direction (start at back).
354            pub fn r(self) -> Self {
355                Self {
356                    reverse: true,
357                    ..self
358                }
359            }
360
361            /// Case-**insensitive** search.
362            pub fn n(self) -> Self {
363                Self {
364                    case_insensitive: true,
365                    ..self
366                }
367            }
368
369            /// Start index -- begin search here rather than at start/end of string.
370            pub fn from(self, index: usize) -> Self {
371                Self {
372                    from_index: Some(index),
373                    ..self
374                }
375            }
376
377            /// Does the actual work. Must be called to finalize find operation.
378            pub fn done(self) -> Option<usize> {
379                let from_index = self.from_index.map(|i| i as i64);
380                let inner = self.owner.as_inner();
381                let what = self.what;
382
383                let godot_found = if self.reverse {
384                    let from_index = from_index.unwrap_or(-1);
385
386                    if self.case_insensitive {
387                        inner.rfindn(what, from_index)
388                    } else {
389                        inner.rfind(what, from_index)
390                    }
391                } else {
392                    let from_index = from_index.unwrap_or(0);
393
394                    if self.case_insensitive {
395                        inner.findn(what, from_index)
396                    } else {
397                        inner.find(what, from_index)
398                    }
399                };
400
401                sys::found_to_option(godot_found)
402            }
403        }
404
405        // --------------------------------------------------------------------------------------------------------------------------------------
406        // split() support
407
408        #[doc = concat!("Manual extender for [`", stringify!($Builtin), "::split_ex`][crate::builtin::", stringify!($builtin_mod), "::ExSplit].")]
409        #[must_use]
410        pub struct ExSplit<'s, 'w> {
411            owner: &'s $Builtin,
412            delimiter: meta::CowArg<'w, GString>,
413            reverse: bool,
414            allow_empty: bool,
415            maxsplit: usize,
416        }
417
418        impl<'s, 'w> ExSplit<'s, 'w> {
419            pub(crate) fn new(owner: &'s $Builtin, delimiter: meta::CowArg<'w, GString>) -> Self {
420                Self {
421                    owner,
422                    delimiter,
423                    reverse: false,
424                    allow_empty: true,
425                    maxsplit: 0,
426                }
427            }
428
429            /// After calling this method, empty strings between adjacent delimiters are excluded from the array.
430            pub fn disallow_empty(self) -> Self {
431                Self {
432                    allow_empty: false,
433                    ..self
434                }
435            }
436
437            /// Limit number of splits (forward mode).
438            ///
439            /// If `maxsplit` is greater than 0, the number of splits may not exceed `maxsplit`. By default, the entire string is split.
440            ///
441            /// Note that `number_of_splits` refers to the number of times a split occurs, which is the resulting element count **minus one**.
442            pub fn maxsplit(self, number_of_splits: usize) -> Self {
443                Self {
444                    maxsplit: number_of_splits,
445                    ..self
446                }
447            }
448
449            /// Limit number of splits (reverse mode).
450            ///
451            /// If `maxsplit` is greater than 0, the number of splits may not exceed `maxsplit`. By default, the entire string is split.
452            ///
453            /// Note that `number_of_splits` refers to the number of times a split occurs, which is the resulting element count **minus one**.
454            pub fn maxsplit_r(self, number_of_splits: usize) -> Self {
455                Self {
456                    maxsplit: number_of_splits,
457                    reverse: true,
458                    ..self
459                }
460            }
461
462            /// Does the actual work. Must be called to finalize split operation.
463            pub fn done(self) -> $crate::builtin::PackedStringArray {
464                let inner = self.owner.as_inner();
465                let delimiter = self.delimiter;
466
467                if self.reverse {
468                    inner.rsplit(delimiter, self.allow_empty, self.maxsplit as i64)
469                } else {
470                    inner.split(delimiter, self.allow_empty, self.maxsplit as i64)
471                }
472            }
473        }
474    };
475}