diff --git a/Changelog.md b/Changelog.md index ce31f3b0..2ba64291 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,8 +1,9 @@ ## Next +* **Breaking:** Rename `sort` to `sortInPlace`, add `sort` (#405). * Improve CI for external contributions (#413). +* Add `forEachRange` to `List` (#411). * Add `isSorted` to `List` (#408). -* **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). diff --git a/src/List.mo b/src/List.mo index 62755632..6766962a 100644 --- a/src/List.mo +++ b/src/List.mo @@ -1597,6 +1597,56 @@ 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"; + /// 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 + /// ``` + /// + /// 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 ba74c5c7..e49e504f 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; @@ -1172,6 +1173,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)) @@ -1255,6 +1276,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 b7e25ca8..3971496e 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",