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