Skip to content

Commit af83229

Browse files
committed
Implement Array::bsearch_by()
1 parent 35771ff commit af83229

File tree

3 files changed

+91
-17
lines changed

3 files changed

+91
-17
lines changed

godot-core/src/builtin/callable.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,24 @@ impl Callable {
155155
})
156156
}
157157

158+
#[cfg(since_api = "4.2")]
159+
pub(crate) fn with_scoped_fn<S, F, Fc, R>(name: S, rust_function: F, callable_usage: Fc) -> R
160+
where
161+
S: meta::AsArg<GString>,
162+
F: FnMut(&[&Variant]) -> Result<Variant, ()>,
163+
Fc: FnOnce(&Callable) -> R,
164+
{
165+
meta::arg_into_owned!(name);
166+
167+
let callable = Self::from_fn_wrapper(FnWrapper {
168+
rust_function,
169+
name,
170+
thread_id: Some(std::thread::current().id()),
171+
});
172+
173+
callable_usage(&callable)
174+
}
175+
158176
/// Create callable from **thread-safe** Rust function or closure.
159177
///
160178
/// `name` is used for the string representation of the closure, which helps debugging.

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

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
66
*/
77

8-
use std::fmt;
98
use std::marker::PhantomData;
9+
use std::{cmp, fmt};
1010

1111
use crate::builtin::*;
1212
use crate::meta;
@@ -612,37 +612,80 @@ impl<T: ArrayElement> Array<T> {
612612
}
613613
}
614614

615-
/// Finds the index of an existing value in a sorted array using binary search.
616-
/// Equivalent of `bsearch` in GDScript.
615+
/// Finds the index of a value in a sorted array using binary search.
617616
///
618-
/// If the value is not present in the array, returns the insertion index that
619-
/// would maintain sorting order.
617+
/// If the value is not present in the array, returns the insertion index that would maintain sorting order.
620618
///
621-
/// Calling `bsearch` on an unsorted array results in unspecified behavior.
619+
/// Calling `bsearch` on an unsorted array results in unspecified behavior. Consider using `sort()` to ensure the sorting
620+
/// order is compatible with your callable's ordering.
622621
pub fn bsearch(&self, value: impl AsArg<T>) -> usize {
623622
meta::arg_into_ref!(value: T);
624623

625624
to_usize(self.as_inner().bsearch(&value.to_variant(), true))
626625
}
627626

628-
/// Finds the index of an existing value in a sorted array using binary search.
629-
/// Equivalent of `bsearch_custom` in GDScript.
627+
/// Finds the index of a value in a sorted array using binary search, with type-safe custom predicate.
630628
///
631-
/// Takes a `Callable` and uses the return value of it to perform binary search.
629+
/// The comparator function should return an ordering that indicates whether its argument is `Less`, `Equal` or `Greater` the desired value.
630+
/// For example, for an ascending-ordered array, a simple predicate searching for a constant value would be `|elem| elem.cmp(&4)`.
631+
/// See also [`slice::binary_search_by()`].
632632
///
633-
/// If the value is not present in the array, returns the insertion index that
634-
/// would maintain sorting order.
633+
/// If the value is found, returns `Ok(index)` with its index. Otherwise, returns `Err(index)`, where `index` is the insertion index
634+
/// that would maintain sorting order.
635635
///
636-
/// Calling `bsearch_custom` on an unsorted array results in unspecified behavior.
636+
/// Calling `bsearch_by` on an unsorted array results in unspecified behavior. Consider using [`sort_by()`] to ensure
637+
/// the sorting order is compatible with your callable's ordering.
638+
#[cfg(since_api = "4.2")]
639+
pub fn bsearch_by<F>(&self, mut func: F) -> Result<usize, usize>
640+
where
641+
F: FnMut(&T) -> cmp::Ordering + 'static,
642+
{
643+
// Early exit; later code relies on index 0 being present.
644+
if self.is_empty() {
645+
return Err(0);
646+
}
647+
648+
// We need one dummy element of type T, because Godot's bsearch_custom() checks types (so Variant::nil() can't be passed).
649+
// Optimization: roundtrip Variant -> T -> Variant could be avoided, but anyone needing speed would use Rust binary search...
650+
let ignored_value = self.at(0);
651+
let ignored_value = <T as ParamType>::owned_to_arg(ignored_value);
652+
653+
let godot_comparator = |args: &[&Variant]| {
654+
let value = T::from_variant(&args[0]);
655+
let is_less = matches!(func(&value), cmp::Ordering::Less);
656+
657+
Ok(is_less.to_variant())
658+
};
659+
660+
let debug_name = std::any::type_name::<F>();
661+
let index = Callable::with_scoped_fn(debug_name, godot_comparator, |pred| {
662+
self.bsearch_custom(ignored_value, pred)
663+
});
664+
665+
if let Some(value_at_index) = self.get(index) {
666+
if func(&value_at_index) == cmp::Ordering::Equal {
667+
return Ok(index);
668+
}
669+
}
670+
671+
Err(index)
672+
}
673+
674+
/// Finds the index of a value in a sorted array using binary search, with `Callable` custom predicate.
675+
///
676+
/// The callable `pred` takes two elements `(a, b)` and should return if `a < b` (strictly less).
677+
/// For a type-safe version, check out [`bsearch_by()`][Self::bsearch_by].
678+
///
679+
/// If the value is not present in the array, returns the insertion index that would maintain sorting order.
637680
///
638-
/// Consider using `sort_custom()` to ensure the sorting order is compatible with
639-
/// your callable's ordering
640-
pub fn bsearch_custom(&self, value: impl AsArg<T>, func: &Callable) -> usize {
681+
/// Calling `bsearch_custom` on an unsorted array results in unspecified behavior. Consider using `sort_custom()` to ensure
682+
/// the sorting order is compatible with your callable's ordering.
683+
pub fn bsearch_custom(&self, value: impl AsArg<T>, pred: &Callable) -> usize {
641684
meta::arg_into_ref!(value: T);
642685

643686
to_usize(
644687
self.as_inner()
645-
.bsearch_custom(&value.to_variant(), func, true),
688+
.bsearch_custom(&value.to_variant(), pred, true),
646689
)
647690
}
648691

itest/rust/src/builtin_tests/containers/array_test.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,9 +483,22 @@ fn array_sort_custom() {
483483
assert_eq!(a, array![4, 3, 2, 1]);
484484
}
485485

486+
#[itest(focus)]
487+
#[cfg(since_api = "4.2")]
488+
fn array_bsearch_by() {
489+
let a: Array<i32> = array![1, 2, 4, 5];
490+
491+
assert_eq!(a.bsearch_by(|e| e.cmp(&2)), Ok(1));
492+
assert_eq!(a.bsearch_by(|e| e.cmp(&4)), Ok(2));
493+
494+
assert_eq!(a.bsearch_by(|e| e.cmp(&0)), Err(0));
495+
assert_eq!(a.bsearch_by(|e| e.cmp(&3)), Err(2));
496+
assert_eq!(a.bsearch_by(|e| e.cmp(&9)), Err(4));
497+
}
498+
486499
#[itest]
487500
#[cfg(since_api = "4.2")]
488-
fn array_binary_search_custom() {
501+
fn array_bsearch_custom() {
489502
let a = array![5, 4, 2, 1];
490503
let func = backwards_sort_callable();
491504
assert_eq!(a.bsearch_custom(1, &func), 3);

0 commit comments

Comments
 (0)