diff --git a/Changelog.md b/Changelog.md index 3e50d5b03..9a903b741 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,3 +1,7 @@ +# Next + +* Add `findInRange()` and `findInRangeBinarySearch()` to `Int` and `Nat` (#381). + ## 1.0.0 * Add `sliceToVarArray()` to `Array` and `VarArray` (#377). diff --git a/src/Int.mo b/src/Int.mo index 65bb2c813..a059c65ec 100644 --- a/src/Int.mo +++ b/src/Int.mo @@ -577,4 +577,62 @@ module { } }; + /// Returns the first value in the range of `Int` values [fromInclusive, toExclusive) for which `predicate` returns true. + /// If no element satisfies the predicate (or the range is empty), returns null. + /// + /// ```motoko include=import + /// + /// assert Int.findInRange(3, 9, func(x) { x > 7 }) == ?8; + /// assert Int.findInRange(3, 8, func(x) { x > 7 }) == null; + /// assert Int.findInRange(3, 8, func(x) { x < 7 }) == ?3; + /// ``` + /// + /// Runtime: O(toExclusive - fromInclusive) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `predicate` runs in O(1) time and space. + public func findInRange(fromInclusive : Int, toExclusive : Int, predicate : Int -> Bool) : ?Int { + for (element in range(fromInclusive, toExclusive)) { + if (predicate element) { + return ?element + } + }; + null + }; + + /// Same as `findInRange()`, but requires that `predicate` is non-decreasing on [fromInclusive, toExclusive). + /// Returns the first value in the range of `Int` values [fromInclusive, toExclusive) for which `predicate` returns true. + /// If no element satisfies the predicate (or the range is empty), returns null. + /// If `predicate` is not non-decreasing on the interval, the result is undefined. + /// + /// ```motoko include=import + /// + /// assert Int.findInRangeBinarySearch(3, 9, func(x) { x > 7 }) == ?8; + /// assert Int.findInRangeBinarySearch(3, 8, func(x) { x > 7 }) == null; + /// Int.findInRangeBinarySearch(3, 8, func(x) { x < 7 }); // Undefined result: `predicate` is not non-decreasing. + /// ``` + /// + /// Runtime: O(log(toExclusive - fromInclusive)) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `predicate` runs in O(1) time and space. + public func findInRangeBinarySearch(fromInclusive : Int, toExclusive : Int, predicate : Int -> Bool) : ?Int { + var l = fromInclusive; + var r = toExclusive; + while (l < r) { + let mid = l + (r - l) / 2; + if (predicate mid) { + r := mid + } else { + l := mid + 1 + } + }; + if (r < toExclusive) { + ?r + } else { + null + } + } } diff --git a/src/Nat.mo b/src/Nat.mo index 05ce7d6b5..46f1eb810 100644 --- a/src/Nat.mo +++ b/src/Nat.mo @@ -569,4 +569,62 @@ module { } }; + /// Returns the first value in the range of `Nat` values [fromInclusive, toExclusive) for which `predicate` returns true. + /// If no element satisfies the predicate (or the range is empty), returns null. + /// + /// ```motoko include=import + /// + /// assert Nat.findInRange(3, 9, func(x) { x > 7 }) == ?8; + /// assert Nat.findInRange(3, 8, func(x) { x > 7 }) == null; + /// assert Nat.findInRange(3, 8, func(x) { x < 7 }) == ?3; + /// ``` + /// + /// Runtime: O(toExclusive - fromInclusive) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `predicate` runs in O(1) time and space. + public func findInRange(fromInclusive : Nat, toExclusive : Nat, predicate : Nat -> Bool) : ?Nat { + for (element in range(fromInclusive, toExclusive)) { + if (predicate element) { + return ?element + } + }; + null + }; + + /// Same as `findInRange()`, but requires that `predicate` is non-decreasing on [fromInclusive, toExclusive). + /// Returns the first value in the range of `Nat` values [fromInclusive, toExclusive) for which `predicate` returns true. + /// If no element satisfies the predicate (or the range is empty), returns null. + /// If `predicate` is not non-decreasing on the interval, the result is undefined. + /// + /// ```motoko include=import + /// + /// assert Nat.findInRangeBinarySearch(3, 9, func(x) { x > 7 }) == ?8; + /// assert Nat.findInRangeBinarySearch(3, 8, func(x) { x > 7 }) == null; + /// Nat.findInRangeBinarySearch(3, 8, func(x) { x < 7 }); // Undefined result: `predicate` is not non-decreasing. + /// ``` + /// + /// Runtime: O(log(toExclusive - fromInclusive)) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `predicate` runs in O(1) time and space. + public func findInRangeBinarySearch(fromInclusive : Nat, toExclusive : Nat, predicate : Nat -> Bool) : ?Nat { + var l = fromInclusive; + var r = toExclusive; + while (l < r) { + let mid = (l + r) / 2; + if (predicate mid) { + r := mid + } else { + l := mid + 1 + } + }; + if (r < toExclusive) { + ?r + } else { + null + } + } } diff --git a/test/Int.test.mo b/test/Int.test.mo index 7bc872bb9..a78f5a6c5 100644 --- a/test/Int.test.mo +++ b/test/Int.test.mo @@ -951,6 +951,8 @@ run( ) ); +/* --------------------------------------- */ + do { Debug.print("range()"); @@ -1012,6 +1014,37 @@ do { assert Array.fromIter(Int.rangeByInclusive(1, 2, -1)) == [] }; +do { + Debug.print("findInRange()"); + assert Int.findInRange(-3, -3, func(x) { x > 7 }) == null; + assert Int.findInRange(-3, 9, func(x) { x > 7 }) == ?8; + assert Int.findInRange(-3, 8, func(x) { x > 7 }) == null; + assert Int.findInRange(3, 8, func(x) { x < 7 }) == ?3; + assert Int.findInRange(-5, -3, func(x) { x > -5 }) == ?-4; + let v : [Int] = [7, 3, 10, 3, 17, 10, 3]; + assert Int.findInRange(0, v.size(), func(x) { v[Int.toNat(x)] == 7 }) == ?0; + assert Int.findInRange(0, v.size(), func(x) { v[Int.toNat(x)] == 3 }) == ?1; + assert Int.findInRange(0, v.size(), func(x) { v[Int.toNat(x)] == 10 }) == ?2; + assert Int.findInRange(0, v.size(), func(x) { v[Int.toNat(x)] == 17 }) == ?4 +}; + +do { + Debug.print("findInRangeBinarySearch()"); + assert Int.findInRangeBinarySearch(-3, -3, func(x) { x > 7 }) == null; + assert Int.findInRangeBinarySearch(-3, 9, func(x) { x > 7 }) == ?8; + assert Int.findInRangeBinarySearch(-3, 8, func(x) { x > 7 }) == null; + assert Int.findInRangeBinarySearch(3, 8, func(x) { x < 7 }) == ?3; + assert Int.findInRangeBinarySearch(-5, -3, func(x) { x > -5 }) == ?-4; + // Stress-test binary search against linear search. + for (i in Int.range(-10, 10)) { + for (j in Int.range(-10, 10)) { + for (val in Int.range(-11, 11)) { + assert Int.findInRangeBinarySearch(i, j, func(x) { x >= val }) == Int.findInRange(i, j, func(x) { x >= val }) + } + } + } +}; + /* --------------------------------------- */ run( diff --git a/test/Nat.test.mo b/test/Nat.test.mo index 520cf728b..86c8c9503 100644 --- a/test/Nat.test.mo +++ b/test/Nat.test.mo @@ -96,4 +96,37 @@ test( assert Array.fromIter(Nat.rangeByInclusive(3, 0, 0)) == []; assert Array.fromIter(Nat.rangeByInclusive(3, 3, 0)) == [3] } +); + +test( + "findInRange", + func() { + assert Nat.findInRange(0, 0, func(x) { x > 7 }) == null; + assert Nat.findInRange(3, 9, func(x) { x > 7 }) == ?8; + assert Nat.findInRange(3, 8, func(x) { x > 7 }) == null; + assert Nat.findInRange(3, 8, func(x) { x < 7 }) == ?3; + let v = [7, 3, 10, 3, 17, 10, 3]; + assert Nat.findInRange(0, v.size(), func(x) { v[x] == 7 }) == ?0; + assert Nat.findInRange(0, v.size(), func(x) { v[x] == 3 }) == ?1; + assert Nat.findInRange(0, v.size(), func(x) { v[x] == 10 }) == ?2; + assert Nat.findInRange(0, v.size(), func(x) { v[x] == 17 }) == ?4 + } +); + +test( + "findInRangeBinarySearch", + func() { + assert Nat.findInRangeBinarySearch(0, 0, func(x) { x > 7 }) == null; + assert Nat.findInRangeBinarySearch(3, 9, func(x) { x > 7 }) == ?8; + assert Nat.findInRangeBinarySearch(3, 8, func(x) { x > 7 }) == null; + assert Nat.findInRangeBinarySearch(3, 8, func(x) { x < 7 }) == ?3; + // Stress-test binary search against linear search. + for (i in Nat.range(0, 10)) { + for (j in Nat.range(0, 10)) { + for (val in Nat.range(0, 11)) { + assert Nat.findInRangeBinarySearch(i, j, func(x) { x >= val }) == Nat.findInRange(i, j, func(x) { x >= val }) + } + } + } + } ) diff --git a/validation/api/api.lock.json b/validation/api/api.lock.json index 3a29570f3..3405f154d 100644 --- a/validation/api/api.lock.json +++ b/validation/api/api.lock.json @@ -198,6 +198,8 @@ "public func compare(x : Int, y : Int) : Order.Order", "public func div(x : Int, y : Int) : Int", "public func equal(x : Int, y : Int) : Bool", + "public func findInRange(fromInclusive : Int, toExclusive : Int, predicate : Int -> Bool) : ?Int", + "public func findInRangeBinarySearch(fromInclusive : Int, toExclusive : Int, predicate : Int -> Bool) : ?Int", "public func fromNat(nat : Nat) : Int", "public func fromText(text : Text) : ?Int", "public func greater(x : Int, y : Int) : Bool", @@ -618,6 +620,8 @@ "public func compare(x : Nat, y : Nat) : Order.Order", "public func div(x : Nat, y : Nat) : Nat", "public func equal(x : Nat, y : Nat) : Bool", + "public func findInRange(fromInclusive : Nat, toExclusive : Nat, predicate : Nat -> Bool) : ?Nat", + "public func findInRangeBinarySearch(fromInclusive : Nat, toExclusive : Nat, predicate : Nat -> Bool) : ?Nat", "public func fromInt(int : Int) : Nat", "public func fromText(text : Text) : ?Nat", "public func greater(x : Nat, y : Nat) : Bool",