Skip to content

Commit a03ecc5

Browse files
authored
Merge pull request #1300 from Yarwin/support-negative-indices-in-array-slices
Add `SignedRange` for negative indices in range ops
2 parents 9d040aa + 0c9fdc4 commit a03ecc5

File tree

13 files changed

+296
-143
lines changed

13 files changed

+296
-143
lines changed

godot-core/src/builtin/collections/array.rs

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use sys::{ffi_methods, interface_fn, GodotFfi};
1515
use crate::builtin::*;
1616
use crate::meta;
1717
use crate::meta::error::{ConvertError, FromGodotError, FromVariantError};
18+
use crate::meta::signed_range::SignedRange;
1819
use crate::meta::{
1920
element_godot_type_name, element_variant_type, ArrayElement, AsArg, ClassName, ElementType,
2021
ExtVariantType, FromGodot, GodotConvert, GodotFfiVariant, GodotType, PropertyHintInfo, RefArg,
@@ -108,6 +109,35 @@ use crate::registry::property::{BuiltinExport, Export, Var};
108109
/// // ...and so on.
109110
/// ```
110111
///
112+
/// # Working with signed ranges and steps
113+
///
114+
/// For negative indices, use [`wrapped()`](crate::meta::wrapped).
115+
///
116+
/// ```no_run
117+
/// # use godot::builtin::array;
118+
/// # use godot::meta::wrapped;
119+
/// let arr = array![0, 1, 2, 3, 4, 5];
120+
///
121+
/// // The values of `begin` (inclusive) and `end` (exclusive) will be clamped to the array size.
122+
/// let clamped_array = arr.subarray_deep(999..99999, None);
123+
/// assert_eq!(clamped_array, array![]);
124+
///
125+
/// // If either `begin` or `end` is negative, its value is relative to the end of the array.
126+
/// let sub = arr.subarray_shallow(wrapped(-1..-5), None);
127+
/// assert_eq!(sub, array![5, 3]);
128+
///
129+
/// // If `end` is not specified, the range spans through whole array.
130+
/// let sub = arr.subarray_deep(1.., None);
131+
/// assert_eq!(sub, array![1, 2, 3, 4, 5]);
132+
/// let other_clamped_array = arr.subarray_shallow(5.., Some(2));
133+
/// assert_eq!(other_clamped_array, array![5]);
134+
///
135+
/// // If specified, `step` is the relative index between source elements. It can be negative,
136+
/// // in which case `begin` must be higher than `end`.
137+
/// let sub = arr.subarray_shallow(wrapped(-1..-5), Some(-2));
138+
/// assert_eq!(sub, array![5, 3]);
139+
/// ```
140+
///
111141
/// # Thread safety
112142
///
113143
/// Usage is safe if the `Array` is used on a single thread only. Concurrent reads on
@@ -526,57 +556,41 @@ impl<T: ArrayElement> Array<T> {
526556
result.with_cache(self)
527557
}
528558

529-
/// Returns a sub-range `begin..end`, as a new array.
530-
///
531-
/// The values of `begin` (inclusive) and `end` (exclusive) will be clamped to the array size.
532-
///
533-
/// If specified, `step` is the relative index between source elements. It can be negative,
534-
/// in which case `begin` must be higher than `end`. For example,
535-
/// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
559+
/// Returns a sub-range `begin..end` as a new `Array`.
536560
///
537561
/// Array elements are copied to the slice, but any reference types (such as `Array`,
538562
/// `Dictionary` and `Object`) will still refer to the same value. To create a deep copy, use
539563
/// [`subarray_deep()`][Self::subarray_deep] instead.
540564
///
541565
/// _Godot equivalent: `slice`_
542566
#[doc(alias = "slice")]
543-
// TODO(v0.3): change to i32 like NodePath::slice/subpath() and support+test negative indices.
544-
pub fn subarray_shallow(&self, begin: usize, end: usize, step: Option<isize>) -> Self {
545-
self.subarray_impl(begin, end, step, false)
567+
pub fn subarray_shallow(&self, range: impl SignedRange, step: Option<i32>) -> Self {
568+
self.subarray_impl(range, step, false)
546569
}
547570

548-
/// Returns a sub-range `begin..end`, as a new `Array`.
549-
///
550-
/// The values of `begin` (inclusive) and `end` (exclusive) will be clamped to the array size.
551-
///
552-
/// If specified, `step` is the relative index between source elements. It can be negative,
553-
/// in which case `begin` must be higher than `end`. For example,
554-
/// `Array::from(&[0, 1, 2, 3, 4, 5]).slice(5, 1, -2)` returns `[5, 3]`.
571+
/// Returns a sub-range `begin..end` as a new `Array`.
555572
///
556573
/// All nested arrays and dictionaries are duplicated and will not be shared with the original
557574
/// array. Note that any `Object`-derived elements will still be shallow copied. To create a
558575
/// shallow copy, use [`subarray_shallow()`][Self::subarray_shallow] instead.
559576
///
560577
/// _Godot equivalent: `slice`_
561578
#[doc(alias = "slice")]
562-
// TODO(v0.3): change to i32 like NodePath::slice/subpath() and support+test negative indices.
563-
pub fn subarray_deep(&self, begin: usize, end: usize, step: Option<isize>) -> Self {
564-
self.subarray_impl(begin, end, step, true)
579+
pub fn subarray_deep(&self, range: impl SignedRange, step: Option<i32>) -> Self {
580+
self.subarray_impl(range, step, true)
565581
}
566582

567-
fn subarray_impl(&self, begin: usize, end: usize, step: Option<isize>, deep: bool) -> Self {
583+
// Note: Godot will clamp values by itself.
584+
fn subarray_impl(&self, range: impl SignedRange, step: Option<i32>, deep: bool) -> Self {
568585
assert_ne!(step, Some(0), "subarray: step cannot be zero");
569586

570-
let len = self.len();
571-
let begin = begin.min(len);
572-
let end = end.min(len);
573587
let step = step.unwrap_or(1);
588+
let (begin, end) = range.signed();
589+
let end = end.unwrap_or(i32::MAX as i64);
574590

575591
// SAFETY: The type of the array is `T` and we convert the returned array to an `Array<T>` immediately.
576-
let subarray: VariantArray = unsafe {
577-
self.as_inner()
578-
.slice(to_i64(begin), to_i64(end), step.try_into().unwrap(), deep)
579-
};
592+
let subarray: VariantArray =
593+
unsafe { self.as_inner().slice(begin, end, step as i64, deep) };
580594

581595
// SAFETY: slice() returns a typed array with the same type as Self.
582596
let result = unsafe { subarray.assume_type() };

godot-core/src/builtin/collections/packed_array.rs

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use crate::builtin::collections::extend_buffer::ExtendBufferTrait;
1919
use crate::builtin::*;
2020
use crate::classes::file_access::CompressionMode;
2121
use crate::meta;
22+
use crate::meta::signed_range::SignedRange;
2223
use crate::meta::{AsArg, FromGodot, GodotConvert, PackedArrayElement, ToGodot};
2324
use crate::obj::EngineEnum;
2425
use crate::registry::property::{Export, Var};
@@ -226,18 +227,31 @@ impl<T: PackedArrayElement> PackedArray<T> {
226227

227228
/// Returns a sub-range `begin..end`, as a new packed array.
228229
///
229-
/// This method is called `slice()` in Godot.
230230
/// The values of `begin` (inclusive) and `end` (exclusive) will be clamped to the array size.
231-
///
232231
/// To obtain Rust slices, see [`as_slice`][Self::as_slice] and [`as_mut_slice`][Self::as_mut_slice].
232+
///
233+
/// # Usage
234+
/// For negative indices, use [`wrapped()`](crate::meta::wrapped).
235+
///
236+
/// ```no_run
237+
/// # use godot::builtin::PackedArray;
238+
/// # use godot::meta::wrapped;
239+
/// let array = PackedArray::from([10, 20, 30, 40, 50]);
240+
///
241+
/// // If either `begin` or `end` is negative, its value is relative to the end of the array.
242+
/// let sub = array.subarray(wrapped(-4..-2));
243+
/// assert_eq!(sub, PackedArray::from([20, 30]));
244+
///
245+
/// // If `end` is not specified, the resulting subarray will span to the end of the array.
246+
/// let sub = array.subarray(2..);
247+
/// assert_eq!(sub, PackedArray::from([30, 40, 50]));
248+
/// ```
249+
///
250+
/// _Godot equivalent: `slice`_
233251
#[doc(alias = "slice")]
234-
// TODO(v0.3): change to i32 like NodePath::slice/subpath() and support+test negative indices.
235-
pub fn subarray(&self, begin: usize, end: usize) -> Self {
236-
let len = self.len();
237-
let begin = begin.min(len);
238-
let end = end.min(len);
239-
240-
T::op_slice(self.as_inner(), to_i64(begin), to_i64(end))
252+
// Note: Godot will clamp values by itself.
253+
pub fn subarray(&self, range: impl SignedRange) -> Self {
254+
T::op_slice(self.as_inner(), range)
241255
}
242256

243257
/// Returns a shared Rust slice of the array.

godot-core/src/builtin/collections/packed_array_element.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use sys::{interface_fn, GodotFfi, SysPtr};
99

1010
use crate::builtin::collections::extend_buffer::{ExtendBuffer, ExtendBufferTrait};
1111
use crate::builtin::PackedArray;
12+
use crate::meta::signed_range::SignedRange;
1213
use crate::meta::{CowArg, FromGodot, GodotType, ToGodot};
1314
use crate::registry::property::builtin_type_string;
1415
use crate::{builtin, sys};
@@ -126,7 +127,7 @@ pub trait PackedArrayElement: GodotType + Clone + ToGodot + FromGodot {
126127
fn op_append_array(inner: Self::Inner<'_>, other: &PackedArray<Self>);
127128

128129
#[doc(hidden)]
129-
fn op_slice(inner: Self::Inner<'_>, begin: i64, end: i64) -> PackedArray<Self>;
130+
fn op_slice(inner: Self::Inner<'_>, range: impl SignedRange) -> PackedArray<Self>;
130131

131132
#[doc(hidden)]
132133
fn op_find(inner: Self::Inner<'_>, value: CowArg<'_, Self>, from: i64) -> i64;
@@ -285,8 +286,9 @@ macro_rules! impl_packed_array_element {
285286
inner.append_array(other);
286287
}
287288

288-
fn op_slice(inner: Self::Inner<'_>, begin: i64, end: i64) -> PackedArray<Self> {
289-
inner.slice(begin, end)
289+
fn op_slice(inner: Self::Inner<'_>, range: impl $crate::meta::signed_range::SignedRange) -> PackedArray<Self> {
290+
let (begin, end) = range.signed();
291+
inner.slice(begin, end.unwrap_or(i32::MAX as i64))
290292
}
291293

292294
fn op_find(inner: Self::Inner<'_>, value: CowArg<'_, Self>, from: i64) -> i64 {

godot-core/src/builtin/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ pub mod inner {
113113
pub use crate::gen::builtin_classes::*;
114114
}
115115

116+
// ----------------------------------------------------------------------------------------------------------------------------------------------
117+
// Conversion functions
118+
116119
pub(crate) fn to_i64(i: usize) -> i64 {
117120
i.try_into().unwrap()
118121
}

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

Lines changed: 0 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ mod node_path;
1313
mod string_macros;
1414
mod string_name;
1515

16-
use std::ops;
17-
1816
pub use gstring::*;
1917
pub use node_path::NodePath;
2018
pub use string_name::*;
@@ -70,71 +68,6 @@ pub enum Encoding {
7068

7169
// ----------------------------------------------------------------------------------------------------------------------------------------------
7270

73-
/// Returns a tuple of `(from, len)` from a Rust range.
74-
///
75-
/// Unbounded upper bounds are represented by `len = -1`.
76-
fn to_godot_fromlen_neg1<R>(range: R) -> (i64, i64)
77-
where
78-
R: ops::RangeBounds<usize>,
79-
{
80-
let from = match range.start_bound() {
81-
ops::Bound::Included(&n) => n as i64,
82-
ops::Bound::Excluded(&n) => (n as i64) + 1,
83-
ops::Bound::Unbounded => 0,
84-
};
85-
86-
let len = match range.end_bound() {
87-
ops::Bound::Included(&n) => {
88-
let to = (n + 1) as i64;
89-
debug_assert!(
90-
from <= to,
91-
"range: start ({from}) > inclusive end ({n}) + 1"
92-
);
93-
to - from
94-
}
95-
ops::Bound::Excluded(&n) => {
96-
let to = n as i64;
97-
debug_assert!(from <= to, "range: start ({from}) > exclusive end ({to})");
98-
to - from
99-
}
100-
ops::Bound::Unbounded => -1,
101-
};
102-
103-
(from, len)
104-
}
105-
106-
/// Returns a tuple of `(from, len)` from a Rust range.
107-
///
108-
/// Unbounded upper bounds are represented by `i32::MAX` (yes, not `i64::MAX` -- since Godot treats some indexes as 32-bit despite being
109-
/// declared `i64` in GDExtension API).
110-
fn to_godot_fromlen_i32max<R>(range: R) -> (i64, i64)
111-
where
112-
R: ops::RangeBounds<usize>,
113-
{
114-
let (from, len) = to_godot_fromlen_neg1(range);
115-
if len == -1 {
116-
// Use i32 here because Godot may wrap around larger values (see Rustdoc).
117-
(from, i32::MAX as i64)
118-
} else {
119-
(from, len)
120-
}
121-
}
122-
123-
/// Returns a tuple of `(from, to)` from a Rust range.
124-
///
125-
/// Unbounded upper bounds are represented by `to = 0`.
126-
fn to_godot_fromto<R>(range: R) -> (i64, i64)
127-
where
128-
R: ops::RangeBounds<usize>,
129-
{
130-
let (from, len) = to_godot_fromlen_neg1(range);
131-
if len == -1 {
132-
(from, 0)
133-
} else {
134-
(from, from + len)
135-
}
136-
}
137-
13871
fn populated_or_none(s: GString) -> Option<GString> {
13972
if s.is_empty() {
14073
None

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

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use godot_ffi::{ffi_methods, ExtVariantType, GdextBuild, GodotFfi};
1212

1313
use super::{GString, StringName};
1414
use crate::builtin::inner;
15+
use crate::meta::signed_range::SignedRange;
1516

1617
/// A pre-parsed scene tree path.
1718
///
@@ -127,29 +128,46 @@ impl NodePath {
127128
/// Returns the range `begin..exclusive_end` as a new `NodePath`.
128129
///
129130
/// The absolute value of `begin` and `exclusive_end` will be clamped to [`get_total_count()`][Self::get_total_count].
130-
/// So, to express "until the end", you can simply pass a large value for `exclusive_end`, such as `i32::MAX`.
131131
///
132-
/// If either `begin` or `exclusive_end` are negative, they will be relative to the end of the `NodePath`. \
133-
/// For example, `path.subpath(0, -2)` is a shorthand for `path.subpath(0, path.get_total_count() - 2)`.
132+
/// # Usage
133+
/// For negative indices, use [`wrapped()`](crate::meta::wrapped).
134+
///
135+
/// ```no_run
136+
/// # use godot::builtin::NodePath;
137+
/// # use godot::meta::wrapped;
138+
/// let path = NodePath::from("path/to/Node:with:props");
139+
///
140+
/// // If upper bound is not defined, `exclusive_end` will span to the end of the `NodePath`.
141+
/// let sub = path.subpath(2..);
142+
/// assert_eq!(sub, ":props".into());
143+
///
144+
/// // If either `begin` or `exclusive_end` are negative, they will be relative to the end of the `NodePath`.
145+
/// let total_count = path.get_total_count();
146+
/// let wrapped_sub = path.subpath(wrapped(0..-2));
147+
/// let normal_sub = path.subpath(0..total_count - 2);
148+
/// // Both are equal to "path/to/Node".
149+
/// assert_eq!(wrapped_sub, normal_sub);
150+
/// ```
134151
///
135152
/// _Godot equivalent: `slice`_
136153
///
137154
/// # Compatibility
138-
/// The `slice()` behavior for Godot <= 4.3 is unintuitive, see [#100954](https://github.com/godotengine/godot/pull/100954). godot-rust
155+
/// The `slice()` behavior for Godot <= 4.3 is unintuitive, see [#100954](https://github.com/godotengine/godot/pull/100954). Godot-rust
139156
/// automatically changes this to the fixed version for Godot 4.4+, even when used in older versions. So, the behavior is always the same.
140157
// i32 used because it can be negative and many Godot APIs use this, see https://github.com/godot-rust/gdext/pull/982/files#r1893732978.
141158
#[cfg(since_api = "4.3")]
142159
#[doc(alias = "slice")]
143-
pub fn subpath(&self, begin: i32, exclusive_end: i32) -> NodePath {
160+
pub fn subpath(&self, range: impl SignedRange) -> NodePath {
161+
let (from, exclusive_end) = range.signed();
144162
// Polyfill for bug https://github.com/godotengine/godot/pull/100954, fixed in 4.4.
145163
let begin = if GdextBuild::since_api("4.4") {
146-
begin
164+
from
147165
} else {
148-
let name_count = self.get_name_count() as i32;
149-
let subname_count = self.get_subname_count() as i32;
166+
let name_count = self.get_name_count() as i64;
167+
let subname_count = self.get_subname_count() as i64;
150168
let total_count = name_count + subname_count;
151169

152-
let mut begin = begin.clamp(-total_count, total_count);
170+
let mut begin = from.clamp(-total_count, total_count);
153171
if begin < 0 {
154172
begin += total_count;
155173
}
@@ -159,7 +177,8 @@ impl NodePath {
159177
begin
160178
};
161179

162-
self.as_inner().slice(begin as i64, exclusive_end as i64)
180+
self.as_inner()
181+
.slice(begin, exclusive_end.unwrap_or(i32::MAX as i64))
163182
}
164183

165184
crate::meta::declare_arg_method! {

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,13 @@ macro_rules! impl_shared_string_api {
8181

8282
/// Count how many times `what` appears within `range`. Use `..` for full string search.
8383
pub fn count(&self, what: impl AsArg<GString>, range: impl std::ops::RangeBounds<usize>) -> usize {
84-
let (from, to) = super::to_godot_fromto(range);
84+
let (from, to) = $crate::meta::signed_range::to_godot_range_fromto(range);
8585
self.as_inner().count(what, from, to) as usize
8686
}
8787

8888
/// Count how many times `what` appears within `range`, case-insensitively. Use `..` for full string search.
8989
pub fn countn(&self, what: impl AsArg<GString>, range: impl std::ops::RangeBounds<usize>) -> usize {
90-
let (from, to) = super::to_godot_fromto(range);
90+
let (from, to) = $crate::meta::signed_range::to_godot_range_fromto(range);
9191
self.as_inner().countn(what, from, to) as usize
9292
}
9393

@@ -121,7 +121,7 @@ macro_rules! impl_shared_string_api {
121121
/// Returns a substring of this, as another `GString`.
122122
// TODO is there no efficient way to implement this for StringName by interning?
123123
pub fn substr(&self, range: impl std::ops::RangeBounds<usize>) -> GString {
124-
let (from, len) = super::to_godot_fromlen_neg1(range);
124+
let (from, len) = $crate::meta::signed_range::to_godot_range_fromlen(range, -1);
125125

126126
self.as_inner().substr(from, len)
127127
}
@@ -163,7 +163,7 @@ macro_rules! impl_shared_string_api {
163163

164164
/// Returns a copy of the string without the specified index range.
165165
pub fn erase(&self, range: impl std::ops::RangeBounds<usize>) -> GString {
166-
let (from, len) = super::to_godot_fromlen_i32max(range);
166+
let (from, len) = $crate::meta::signed_range::to_godot_range_fromlen(range, i32::MAX as i64);
167167
self.as_inner().erase(from, len)
168168
}
169169

0 commit comments

Comments
 (0)