Skip to content

Commit 2055bdf

Browse files
committed
GString/StringName(): unicode_at() + 4 padding functions + match[n]()
1 parent 419ba4a commit 2055bdf

File tree

3 files changed

+147
-39
lines changed

3 files changed

+147
-39
lines changed

godot-codegen/src/special_cases/special_cases.rs

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,11 +232,7 @@ pub fn is_method_private(class_or_builtin_ty: &TyName, godot_method_name: &str)
232232
#[rustfmt::skip]
233233
pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -> bool {
234234
match (builtin_ty.godot_ty.as_str(), godot_method_name) {
235-
// TODO maybe consider renaming "match_" -> "matches". The "*n" could technically be "*_n", but is probably OK.
236-
237235
// GString
238-
| ("String", "match")
239-
| ("String", "matchn")
240236
| ("String", "begins_with")
241237
| ("String", "ends_with")
242238
| ("String", "is_subsequence_of")
@@ -264,7 +260,6 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
264260
| ("String", "get_extension")
265261
| ("String", "get_basename")
266262
| ("String", "path_join")
267-
| ("String", "unicode_at")
268263
| ("String", "indent")
269264
| ("String", "dedent")
270265
| ("String", "md5_text")
@@ -301,10 +296,6 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
301296
| ("String", "to_float")
302297
| ("String", "hex_to_int")
303298
| ("String", "bin_to_int")
304-
| ("String", "lpad")
305-
| ("String", "rpad")
306-
| ("String", "pad_decimals")
307-
| ("String", "pad_zeros")
308299
| ("String", "trim_prefix")
309300
| ("String", "trim_suffix")
310301
| ("String", "to_ascii_buffer")
@@ -321,8 +312,6 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
321312
| ("String", "humanize_size")
322313

323314
// StringName
324-
| ("StringName", "match")
325-
| ("StringName", "matchn")
326315
| ("StringName", "begins_with")
327316
| ("StringName", "ends_with")
328317
| ("StringName", "is_subsequence_of")
@@ -350,7 +339,6 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
350339
| ("StringName", "get_extension")
351340
| ("StringName", "get_basename")
352341
| ("StringName", "path_join")
353-
| ("StringName", "unicode_at")
354342
| ("StringName", "indent")
355343
| ("StringName", "dedent")
356344
| ("StringName", "md5_text")
@@ -387,10 +375,6 @@ pub fn is_builtin_method_exposed(builtin_ty: &TyName, godot_method_name: &str) -
387375
| ("StringName", "to_float")
388376
| ("StringName", "hex_to_int")
389377
| ("StringName", "bin_to_int")
390-
| ("StringName", "lpad")
391-
| ("StringName", "rpad")
392-
| ("StringName", "pad_decimals")
393-
| ("StringName", "pad_zeros")
394378
| ("StringName", "trim_prefix")
395379
| ("StringName", "trim_suffix")
396380
| ("StringName", "to_ascii_buffer")

godot-core/src/builtin/string/string_macros.rs

Lines changed: 97 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,24 @@ macro_rules! impl_shared_string_api {
1717

1818
/// Manually-declared, shared methods between `GString` and `StringName`.
1919
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+
2038
/// Find first occurrence of `what` and return index, or `None` if not found.
2139
///
2240
/// Check [`find_ex()`](Self::find_ex) for all custom options.
@@ -84,8 +102,8 @@ macro_rules! impl_shared_string_api {
84102
///
85103
/// If `delimiter` is an empty string, each substring will be a single character.
86104
///
87-
/// The builder struct offers methods to configure multiple dimensions. Note that `rsplit` in Godot is not useful without the `maxsplit`
88-
/// argument, so the two are combined in Rust as `maxsplit_r`.
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`.
89107
///
90108
/// | Method | Default behavior | Behavior after method call |
91109
/// |--------------------|------------------------|---------------------------------------------------|
@@ -176,10 +194,44 @@ macro_rules! impl_shared_string_api {
176194
self.as_inner().format(array_or_dict, placeholder)
177195
}
178196

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+
179231
/// Case-sensitive, lexicographic comparison to another string.
180232
///
181-
/// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which roughly
182-
/// matches the alphabetical order.
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.
183235
///
184236
/// See also [`nocasecmp_to()`](Self::nocasecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
185237
pub fn casecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
@@ -188,8 +240,8 @@ macro_rules! impl_shared_string_api {
188240

189241
/// Case-**insensitive**, lexicographic comparison to another string.
190242
///
191-
/// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which roughly
192-
/// matches the alphabetical order.
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.
193245
///
194246
/// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
195247
pub fn nocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
@@ -198,13 +250,15 @@ macro_rules! impl_shared_string_api {
198250

199251
/// Case-sensitive, **natural-order** comparison to another string.
200252
///
201-
/// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which roughly
202-
/// matches the alphabetical order.
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.
203255
///
204-
/// When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected,
205-
/// instead of the single digit's value. A sorted sequence of numbered strings will be `["1", "2", "3", ...]`, not `["1", "10", "2", "3", ...]`.
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", ...]`.
206259
///
207-
/// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less` if shorter.
260+
/// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less`
261+
/// if shorter.
208262
///
209263
/// See also [`casecmp_to()`](Self::casecmp_to), [`naturalnocasecmp_to()`](Self::naturalnocasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
210264
pub fn naturalcasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
@@ -213,13 +267,15 @@ macro_rules! impl_shared_string_api {
213267

214268
/// Case-insensitive, **natural-order** comparison to another string.
215269
///
216-
/// Returns the `Ordering` relation of `self` towards `to`. Ordering is determined by the Unicode code points of each string, which roughly
217-
/// matches the alphabetical order.
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.
218272
///
219-
/// When used for sorting, natural order comparison orders sequences of numbers by the combined value of each digit as is often expected,
220-
/// instead of the single digit's value. A sorted sequence of numbered strings will be `["1", "2", "3", ...]`, not `["1", "10", "2", "3", ...]`.
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", ...]`.
221276
///
222-
/// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less` if shorter.
277+
/// With different string lengths, returns `Ordering::Greater` if this string is longer than the `to` string, or `Ordering::Less`
278+
/// if shorter.
223279
///
224280
/// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
225281
pub fn naturalnocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
@@ -228,8 +284,8 @@ macro_rules! impl_shared_string_api {
228284

229285
/// Case-sensitive, filename-oriented comparison to another string.
230286
///
231-
/// Like [`naturalcasecmp_to()`][Self::naturalcasecmp_to], but prioritizes strings that begin with periods (`.`) and underscores (`_`) before
232-
/// any other character. Useful when sorting folders or file names.
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.
233289
///
234290
/// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filenocasecmp_to()`](Self::filenocasecmp_to).
235291
#[cfg(since_api = "4.3")]
@@ -239,15 +295,36 @@ macro_rules! impl_shared_string_api {
239295

240296
/// Case-insensitive, filename-oriented comparison to another string.
241297
///
242-
/// Like [`naturalnocasecmp_to()`][Self::naturalnocasecmp_to], but prioritizes strings that begin with periods (`.`) and underscores (`_`) before
243-
/// any other character. Useful when sorting folders or file names.
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.
244300
///
245301
/// See also [`casecmp_to()`](Self::casecmp_to), [`naturalcasecmp_to()`](Self::naturalcasecmp_to), [`filecasecmp_to()`](Self::filecasecmp_to).
246302
#[cfg(since_api = "4.3")]
247303
pub fn filenocasecmp_to(&self, to: impl AsArg<GString>) -> std::cmp::Ordering {
248304
sys::i64_to_ordering(self.as_inner().filenocasecmp_to(to))
249305
}
250306

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+
}
251328
}
252329

253330
// --------------------------------------------------------------------------------------------------------------------------------------

itest/rust/src/builtin_tests/string/gstring_test.rs

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use std::collections::HashSet;
99

10-
use crate::framework::itest;
10+
use crate::framework::{expect_panic, itest};
1111
use godot::builtin::{GString, PackedStringArray};
1212

1313
// TODO use tests from godot-rust/gdnative
@@ -72,14 +72,42 @@ fn string_chars() {
7272
assert_eq!(string.chars(), empty_char_slice);
7373
assert_eq!(string, GString::from(empty_char_slice));
7474

75-
let string = String::from("some_string");
75+
let string = String::from("ö🍎A💡");
7676
let string_chars: Vec<char> = string.chars().collect();
7777
let gstring = GString::from(string);
7878

79-
assert_eq!(string_chars, gstring.chars().to_vec());
79+
assert_eq!(gstring.chars(), string_chars.as_slice());
80+
assert_eq!(
81+
gstring.chars(),
82+
&[
83+
char::from_u32(0x00F6).unwrap(),
84+
char::from_u32(0x1F34E).unwrap(),
85+
char::from(65),
86+
char::from_u32(0x1F4A1).unwrap(),
87+
]
88+
);
89+
8090
assert_eq!(gstring, GString::from(string_chars.as_slice()));
8191
}
8292

93+
#[itest]
94+
fn string_unicode_at() {
95+
let s = GString::from("ö🍎A💡");
96+
assert_eq!(s.unicode_at(0), 'ö');
97+
assert_eq!(s.unicode_at(1), '🍎');
98+
assert_eq!(s.unicode_at(2), 'A');
99+
assert_eq!(s.unicode_at(3), '💡');
100+
101+
#[cfg(debug_assertions)]
102+
expect_panic("Debug mode: unicode_at() out-of-bounds panics", || {
103+
s.unicode_at(4);
104+
});
105+
106+
// Release mode: out-of-bounds prints Godot error, but returns 0.
107+
#[cfg(not(debug_assertions))]
108+
assert_eq!(s.unicode_at(4), '\0');
109+
}
110+
83111
#[itest]
84112
fn string_hash() {
85113
let set: HashSet<GString> = [
@@ -223,6 +251,25 @@ fn string_insert() {
223251
assert_eq!(s.insert(123, "!"), "H World!".into());
224252
}
225253

254+
#[itest]
255+
fn string_pad() {
256+
let s = GString::from("123");
257+
assert_eq!(s.lpad(5, '0'), "00123".into());
258+
assert_eq!(s.lpad(2, ' '), "123".into());
259+
assert_eq!(s.lpad(4, ' '), " 123".into());
260+
261+
assert_eq!(s.rpad(5, '+'), "123++".into());
262+
assert_eq!(s.rpad(2, ' '), "123".into());
263+
assert_eq!(s.rpad(4, ' '), "123 ".into());
264+
265+
let s = GString::from("123.456");
266+
assert_eq!(s.pad_decimals(5), "123.45600".into());
267+
assert_eq!(s.pad_decimals(2), "123.45".into()); // note: Godot rounds down
268+
269+
assert_eq!(s.pad_zeros(5), "00123.456".into());
270+
assert_eq!(s.pad_zeros(2), "123.456".into());
271+
}
272+
226273
// ----------------------------------------------------------------------------------------------------------------------------------------------
227274

228275
fn packed(strings: &[&str]) -> PackedStringArray {

0 commit comments

Comments
 (0)