From c54c267d6cf777ea36aff5bc66e3256590d71abb Mon Sep 17 00:00:00 2001 From: Andrii Stepanov Date: Sun, 19 Oct 2025 17:38:32 +0300 Subject: [PATCH 1/2] Add forEachRange to List. --- Changelog.md | 3 ++- src/List.mo | 49 ++++++++++++++++++++++++++++++++++++ test/List.test.mo | 22 ++++++++++++++++ validation/api/api.lock.json | 1 + 4 files changed, 74 insertions(+), 1 deletion(-) diff --git a/Changelog.md b/Changelog.md index 228e6abe..78f48936 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,11 +1,12 @@ ## Next +* Add `forEachRange` to `List` (#411). +* **Breaking:** Rename `sort` to `sortInPlace`, add `sort` (#405). * Add `isCleanReject` to `Error`, align reject code order with IC interface specification and improve comments (#401). * internal: updates `matchers` dev-dependency (#394). * Add `PriorityQueue` (#392). * Add support for Weak references (#388). * Clarify difference between `List` and `pure/List` in doc comments (#386). -* **Breaking:** Rename `sort` to `sortInPlace`, add `sort` (#405). ## 1.0.0 diff --git a/src/List.mo b/src/List.mo index 0ec82f7b..84ab0489 100644 --- a/src/List.mo +++ b/src/List.mo @@ -1552,6 +1552,55 @@ module { } }; + /// Executes the closure over a slice of `list` starting at `fromInclusive` up to (but not including) `toExclusive`. + /// + /// ```motoko include=import + /// import Debug "mo:core/Debug"; + /// + /// let list = List.fromArray([1, 2, 3, 4, 5]); + /// List.forEachRange(list, func x = Debug.print(Nat.toText(x)), 1, 2); // prints 2 and 3 + /// ``` + /// + /// Runtime: `O(toExclusive - fromExclusive)` + /// + /// Space: `O(1)` + public func forEachRange(list : List, f : T -> (), fromInclusive : Nat, toExclusive : Nat) { + if (not (fromInclusive <= toExclusive and toExclusive <= size(list))) Prim.trap("Invalid range"); + + func traverseBlock(block : [var ?T], f : T -> (), from : Nat, to : Nat) { + var i = from; + while (i < to) { + switch (block[i]) { + case (?value) f(value); + case null Prim.trap(INTERNAL_ERROR) + }; + i += 1 + } + }; + + let (fromBlock, fromElement) = locate(fromInclusive); + let (toBlock, toElement) = locate(toExclusive); + + let blocks = list.blocks; + let sz = blocks.size(); + + if (fromBlock == toBlock) { + if (fromBlock < sz) traverseBlock(blocks[fromBlock], f, fromElement, toElement); + return + }; + + traverseBlock(blocks[fromBlock], f, fromElement, blocks[fromBlock].size()); + + var i = fromBlock + 1; + let to = Nat.min(toBlock, sz); + while (i < to) { + traverseBlock(blocks[i], f, 0, blocks[i].size()); + i += 1 + }; + + if (toBlock < sz) traverseBlock(blocks[toBlock], f, 0, toElement) + }; + /// Returns true if the list contains the specified element according to the provided /// equality function. Uses the provided `equal` function to compare elements. /// diff --git a/test/List.test.mo b/test/List.test.mo index f5bc7303..4ee07d08 100644 --- a/test/List.test.mo +++ b/test/List.test.mo @@ -14,6 +14,7 @@ import Runtime "../src/Runtime"; import Int "../src/Int"; import Debug "../src/Debug"; import { Tuple2 } "../src/Tuples"; +import VarArray "../src/VarArray"; let { run; test; suite } = Suite; @@ -1156,6 +1157,26 @@ func testFromIter(n : Nat) : Bool { List.equal(vec, List.fromArray(Array.tabulate(n, func(i) = i + 1)), Nat.equal) }; +func testForEachRange(n : Nat) : Bool { + if (n > 10) return true; // Skip large ranges for performance + let vec = List.fromArray(Array.tabulate(n, func(i) = i)); + + for (left in Nat.range(0, n)) { + for (right in Nat.range(left, n + 1)) { + let expected = VarArray.tabulate(right - left, func(i) = left + i); + let result = VarArray.repeat(0, right - left); + List.forEachRange(vec, func(i) = result[i - left] := i, left, right); + if (Array.fromVarArray(result) != Array.fromVarArray(expected)) { + Debug.print( + "ForEachRange mismatch for left = " # Nat.toText(left) # ", right = " # Nat.toText(right) # ": expected " # debug_show (expected) # ", got " # debug_show (result) + ); + return false + } + } + }; + true +}; + func testFoldLeft(n : Nat) : Bool { let vec = List.fromArray(Array.tabulate(n, func(i) = i + 1)); List.foldLeft(vec, "", func(acc, x) = acc # Nat.toText(x)) == Array.foldLeft(Array.tabulate(n, func(i) = i + 1), "", func(acc, x) = acc # Nat.toText(x)) @@ -1238,6 +1259,7 @@ func runAllTests() { runTest("testFromIter", testFromIter); runTest("testFoldLeft", testFoldLeft); runTest("testFoldRight", testFoldRight); + runTest("testForEachRange", testForEachRange); runTest("testFilter", testFilter); runTest("testFilterMap", testFilterMap) }; diff --git a/validation/api/api.lock.json b/validation/api/api.lock.json index f3a94cbf..e6efb28f 100644 --- a/validation/api/api.lock.json +++ b/validation/api/api.lock.json @@ -534,6 +534,7 @@ "public func foldRight(list : List, base : A, combine : (T, A) -> A) : A", "public func forEach(list : List, f : T -> ())", "public func forEachEntry(list : List, f : (Nat, T) -> ())", + "public func forEachRange(list : List, f : T -> (), fromInclusive : Nat, toExclusive : Nat)", "public func fromArray(array : [T]) : List", "public func fromIter(iter : Iter.Iter) : List", "public func fromPure(pure : PureList.List) : List", From c815474a14fbf761f354d558315b0646f502d30b Mon Sep 17 00:00:00 2001 From: Andrii Stepanov Date: Wed, 5 Nov 2025 15:13:34 +0200 Subject: [PATCH 2/2] Fix. --- src/List.mo | 1 + 1 file changed, 1 insertion(+) diff --git a/src/List.mo b/src/List.mo index 84ab0489..64a427ce 100644 --- a/src/List.mo +++ b/src/List.mo @@ -1556,6 +1556,7 @@ module { /// /// ```motoko include=import /// import Debug "mo:core/Debug"; + /// import Nat "mo:core/Nat"; /// /// let list = List.fromArray([1, 2, 3, 4, 5]); /// List.forEachRange(list, func x = Debug.print(Nat.toText(x)), 1, 2); // prints 2 and 3