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}