diff --git a/Changelog.md b/Changelog.md index 2b40e5df..f77815ea 100644 --- a/Changelog.md +++ b/Changelog.md @@ -8,6 +8,7 @@ * Add `PriorityQueue` (#392). * Add support for Weak references (#388). * Clarify difference between `List` and `pure/List` in doc comments (#386). +* Added `tabulate`, `flatten`, `join`, `mapInPlace`, `mapEntries`, `mapResult`, `flatMap`, `nextIndexOf`, `prevIndexOf`, `range`, `sliceToArray`, `sliceToVarArray` to `List` (#350). * Optimize methods in `List` (#337). ## 1.0.0 diff --git a/src/List.mo b/src/List.mo index ebd4f9cf..eb2577f4 100644 --- a/src/List.mo +++ b/src/List.mo @@ -16,9 +16,7 @@ import PureList "pure/List"; import Prim "mo:⛔"; import Nat32 "Nat32"; import Array "Array"; -import Iter "Iter"; import Nat "Nat"; -import Order "Order"; import Option "Option"; import VarArray "VarArray"; import Types "Types"; @@ -80,15 +78,13 @@ module { i += 1 }; if (elementIndex != 0) { - let block = VarArray.repeat(null, dataBlockSize(i)); - if (Option.isSome(initValue)) { - var j = 0; - while (j < elementIndex) { - block[j] := initValue; - j += 1 - } - }; - dataBlocks[i] := block + dataBlocks[blockIndex] := if (Option.isNull(initValue)) VarArray.repeat( + null, + dataBlockSize(blockIndex) + ) else VarArray.tabulate( + dataBlockSize(blockIndex), + func i = if (i < elementIndex) initValue else null + ) }; { @@ -149,14 +145,14 @@ module { result }; - /// Converts a purely functional `PureList` to a mutable `List`. + /// Converts a purely functional `PureList` to a `List`. /// /// Example: /// ```motoko include=import /// import PureList "mo:core/pure/List"; /// /// let pureList = PureList.fromArray([1, 2, 3]); - /// let list = List.fromPure(pureList); // converts to mutable List + /// let list = List.fromPure(pureList); // converts to List /// ``` /// /// Runtime: `O(size)` @@ -229,6 +225,16 @@ module { list.elementIndex := elementIndex }; + private func reserve(list : List, size : Nat) { + let blockIndex = list.blockIndex; + let elementIndex = list.elementIndex; + + addRepeatInternal(list, null, size); + + list.blockIndex := blockIndex; + list.elementIndex := elementIndex + }; + /// Add to list `count` copies of the initial value. /// /// ```motoko include=import @@ -260,6 +266,115 @@ module { list.elementIndex := 0 }; + /// Creates a list of size `size`. Each element at index i + /// is created by applying `generator` to i. + /// + /// ```motoko include=import + /// import Nat "mo:core/Nat"; + /// + /// let list = List.tabulate(4, func i = i * 2); + /// assert List.toArray(list) == [0, 2, 4, 6]; + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(size) + /// + /// *Runtime and space assumes that `generator` runs in O(1) time and space. + public func tabulate(size : Nat, generator : Nat -> T) : List { + let (blockIndex, elementIndex) = locate(size); + + let blocks = newIndexBlockLength(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); + let dataBlocks = VarArray.repeat<[var ?T]>([var], blocks); + + var i = 1; + var pos = 0; + + while (i < blockIndex) { + let len = dataBlockSize(i); + dataBlocks[i] := VarArray.tabulate(len, func i = ?generator(pos + i)); + pos += len; + i += 1 + }; + if (elementIndex != 0 and blockIndex < blocks) { + dataBlocks[i] := VarArray.tabulate( + dataBlockSize(blockIndex), + func i = if (i < elementIndex) ?generator(pos + i) else null + ) + }; + + { + var blocks = dataBlocks; + var blockIndex = blockIndex; + var elementIndex = elementIndex + } + }; + + /// Combines a list of lists into a single list. Retains the original + /// ordering of the elements. + /// + /// This has better performance compared to `List.join()`. + /// + /// ```motoko include=import + /// import Nat "mo:core/Nat"; + /// + /// let lists = List.fromArray>([ + /// List.fromArray([0, 1, 2]), List.fromArray([2, 3]), List.fromArray([]), List.fromArray([4]) + /// ]); + /// let flatList = List.flatten(lists); + /// assert List.equal(flatList, List.fromArray([0, 1, 2, 2, 3, 4]), Nat.equal); + /// ``` + /// + /// Runtime: O(number of elements in list) + /// + /// Space: O(number of elements in list) + public func flatten(lists : List>) : List { + var sz = 0; + forEach>(lists, func(sublist) = sz += size(sublist)); + + let result = repeatInternal(null, sz); + result.blockIndex := 1; + result.elementIndex := 0; + + forEach>( + lists, + func(sublist) { + forEach( + sublist, + func(item) { + add(result, item) + } + ) + } + ); + result + }; + + /// Combines an iterator of lists into a single list. + /// Retains the original ordering of the elements. + /// + /// Consider using `List.flatten()` for better performance. + /// + /// ```motoko include=import + /// import Nat "mo:core/Nat"; + /// + /// let lists = [List.fromArray([0, 1, 2]), List.fromArray([2, 3]), List.fromArray([]), List.fromArray([4])]; + /// let joinedList = List.join(lists.vals()); + /// assert List.equal(joinedList, List.fromArray([0, 1, 2, 2, 3, 4]), Nat.equal); + /// ``` + /// + /// Runtime: O(number of elements in list) + /// + /// Space: O(number of elements in list) + public func join(lists : Types.Iter>) : List { + var result = empty(); + for (list in lists) { + reserve(result, size(list)); + forEach(list, func item = addUnsafe(result, item)) + }; + result + }; + /// Returns a copy of a List, with the same size. /// /// Example: @@ -329,6 +444,167 @@ module { } }; + /// Applies `f` to each element of `list` in place, + /// retaining the original ordering of elements. + /// This modifies the original list. + /// + /// ```motoko include=import + /// import Nat "mo:core/Nat"; + /// + /// let list = List.fromArray([0, 1, 2, 3]); + /// List.mapInPlace(list, func x = x * 3); + /// assert List.equal(list, List.fromArray([0, 3, 6, 9]), Nat.equal); + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(size) + /// + /// *Runtime and space assumes that `f` runs in O(1) time and space. + public func mapInPlace(list : List, f : T -> T) { + let blocks = list.blocks; + let blockCount = blocks.size(); + + var i = 1; + while (i < blockCount) { + let db = blocks[i]; + let sz = db.size(); + if (sz == 0) return; + + var j = 0; + while (j < sz) { + switch (db[j]) { + case (?x) db[j] := ?f(x); + case null return + }; + j += 1 + }; + i += 1 + } + }; + + /// Creates a new list by applying `f` to each element in `list` and its index. + /// Retains original ordering of elements. + /// + /// ```motoko include=import + /// import Nat "mo:core/Nat"; + /// + /// let list = List.fromArray([10, 10, 10, 10]); + /// let newList = List.mapEntries(list, func (x, i) = i * x); + /// assert List.equal(newList, List.fromArray([0, 10, 20, 30]), Nat.equal); + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(size) + /// + /// *Runtime and space assumes that `f` runs in O(1) time and space. + public func mapEntries(list : List, f : (T, Nat) -> R) : List { + let blocks = VarArray.repeat<[var ?R]>([var], list.blocks.size()); + let blocksCount = list.blocks.size(); + + var index = 0; + + var i = 1; + label l while (i < blocksCount) { + let oldBlock = list.blocks[i]; + let blockSize = oldBlock.size(); + let newBlock = VarArray.repeat(null, blockSize); + blocks[i] := newBlock; + var j = 0; + + while (j < blockSize) { + switch (oldBlock[j]) { + case (?item) newBlock[j] := ?f(item, index); + case null break l + }; + j += 1; + index += 1 + }; + i += 1 + }; + + { + var blocks = blocks; + var blockIndex = list.blockIndex; + var elementIndex = list.elementIndex + } + }; + + /// Creates a new list by applying `f` to each element in `list`. + /// If any invocation of `f` produces an `#err`, returns an `#err`. Otherwise + /// returns an `#ok` containing the new list. + /// + /// ```motoko include=import + /// import Result "mo:core/Result"; + /// + /// let list = List.fromArray([4, 3, 2, 1, 0]); + /// // divide 100 by every element in the list + /// let result = List.mapResult(list, func x { + /// if (x > 0) { + /// #ok(100 / x) + /// } else { + /// #err "Cannot divide by zero" + /// } + /// }); + /// assert Result.isErr(result); + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(size) + /// + /// *Runtime and space assumes that `f` runs in O(1) time and space. + public func mapResult(list : List, f : T -> Types.Result) : Types.Result, E> { + var error : ?E = null; + + let blocks = VarArray.repeat<[var ?R]>([var], list.blocks.size()); + let blocksCount = list.blocks.size(); + + var i = 1; + while (i < blocksCount) { + let oldBlock = list.blocks[i]; + let blockSize = oldBlock.size(); + let newBlock = VarArray.repeat(null, blockSize); + blocks[i] := newBlock; + var j = 0; + + while (j < blockSize) { + switch (oldBlock[j]) { + case (?item) newBlock[j] := switch (f(item)) { + case (#ok x) ?x; + case (#err e) switch (error) { + case (null) { + error := ?e; + null + }; + case (?_) null + } + }; + case null return switch (error) { + case (null) return #ok { + var blocks = blocks; + var blockIndex = list.blockIndex; + var elementIndex = list.elementIndex + }; + case (?e) return #err e + } + }; + j += 1 + }; + i += 1 + }; + + switch (error) { + case (null) return #ok { + var blocks = blocks; + var blockIndex = list.blockIndex; + var elementIndex = list.elementIndex + }; + case (?e) return #err e + } + }; + /// Returns a new list containing only the elements from `list` for which the predicate returns true. /// /// Example: @@ -413,8 +689,48 @@ module { filtered }; - func indexByBlockElement(block : Nat, element : Nat) : Nat { - let d = Nat32.fromNat(block); + /// Creates a new list by applying `k` to each element in `list`, + /// and concatenating the resulting iterators in order. + /// + /// ```motoko include=import + /// import Int "mo:core/Int" + /// + /// let list = List.fromArray([1, 2, 3, 4]); + /// let newList = List.flatMap(list, func x = [x, -x].vals()); + /// assert List.equal(newList, List.fromArray([1, -1, 2, -2, 3, -3, 4, -4]), Int.equal); + /// ``` + /// Runtime: O(size) + /// + /// Space: O(size) + /// *Runtime and space assumes that `k` runs in O(1) time and space. + public func flatMap(list : List, k : T -> Types.Iter) : List { + let result = empty(); + + let blocks = list.blocks; + let blockCount = blocks.size(); + + var i = 1; + while (i < blockCount) { + let db = blocks[i]; + let sz = db.size(); + if (sz == 0) return result; + + var j = 0; + while (j < sz) { + switch (db[j]) { + case (?x) for (y in k(x)) add(result, y); + case _ return result + }; + j += 1 + }; + i += 1 + }; + + result + }; + + func indexByBlockElement(blockIndex : Nat, elementIndex : Nat) : Nat { + let d = Nat32.fromNat(blockIndex); // We call all data blocks of the same capacity an "epoch". We number the epochs 0,1,2,... // A data block is in epoch e iff the data block has capacity 2 ** e. @@ -436,7 +752,7 @@ module { // there can be overflows, but the result is without overflows, so use addWrap and subWrap // we don't erase bits by >>, so to use <>> is ok - Nat32.toNat((d -% (1 <>> lz)) <>> lz +% Nat32.fromNat(element)) + Nat32.toNat((d -% (1 <>> lz)) <>> lz +% Nat32.fromNat(elementIndex)) }; /// Returns the current number of elements in the list. @@ -543,6 +859,19 @@ module { list.elementIndex := elementIndex }; + private func addUnsafe(list : List, element : T) { + var elementIndex = list.elementIndex; + let lastDataBlock = list.blocks[list.blockIndex]; + lastDataBlock[elementIndex] := ?element; + + elementIndex += 1; + if (elementIndex == lastDataBlock.size()) { + elementIndex := 0; + list.blockIndex += 1 + }; + list.elementIndex := elementIndex + }; + /// Removes and returns the last item in the list or `null` if /// the list is empty. /// @@ -713,7 +1042,7 @@ module { /// /// Space: O(size) /// *Runtime and space assumes that `compare` runs in O(1) time and space. - public func sortInPlace(list : List, compare : (T, T) -> Order.Order) { + public func sortInPlace(list : List, compare : (T, T) -> Types.Order) { if (size(list) < 2) return; let array = toVarArray(list); @@ -781,7 +1110,7 @@ module { /// Runtime: O(size) /// /// Space: O(1) - public func isSorted(list : List, compare : (T, T) -> Order.Order) : Bool { + public func isSorted(list : List, compare : (T, T) -> Types.Order) : Bool { var prev = switch (first(list)) { case (?x) x; case _ return true @@ -834,17 +1163,42 @@ module { /// /// *Runtime and space assumes that `equal` runs in `O(1)` time and space. public func indexOf(list : List, equal : (T, T) -> Bool, element : T) : ?Nat { - // inlined version of findIndex + if (isEmpty(list)) return null; + nextIndexOf(list, equal, element, 0) + }; + + /// Returns the index of the next occurence of `element` in the `list` starting from the `from` index (inclusive). + /// + /// ```motoko include=import + /// import Char "mo:core/Char"; + /// let list = List.fromArray(['c', 'o', 'f', 'f', 'e', 'e']); + /// assert List.nextIndexOf(list, Char.equal, 'c', 0) == ?0; + /// assert List.nextIndexOf(list, Char.equal, 'f', 0) == ?2; + /// assert List.nextIndexOf(list, Char.equal, 'f', 2) == ?2; + /// assert List.nextIndexOf(list, Char.equal, 'f', 3) == ?3; + /// assert List.nextIndexOf(list, Char.equal, 'f', 4) == null; + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(1) + /// + /// *Runtime and space assumes that `equal` runs in O(1) time and space. + public func nextIndexOf(list : List, equal : (T, T) -> Bool, element : T, fromInclusive : Nat) : ?Nat { + if (fromInclusive >= size(list)) Prim.trap "List index out of bounds in nextIndexOf"; + + let (blockIndex, elementIndex) = locate(fromInclusive); + let blocks = list.blocks; let blockCount = blocks.size(); - var i = 1; + var i = blockIndex; while (i < blockCount) { let db = blocks[i]; let sz = db.size(); if (sz == 0) return null; - var j = 0; + var j = if (i == blockIndex) elementIndex else 0; while (j < sz) { switch (db[j]) { case (?x) if (equal(x, element)) return ?indexByBlockElement(i, j); @@ -873,11 +1227,32 @@ module { /// Runtime: `O(size)` /// /// *Runtime and space assumes that `equal` runs in `O(1)` time and space. - public func lastIndexOf(list : List, equal : (T, T) -> Bool, element : T) : ?Nat { - // inlined version of findLastIndex + public func lastIndexOf(list : List, equal : (T, T) -> Bool, element : T) : ?Nat = prevIndexOf( + list, + equal, + element, + size(list) + ); + + /// Returns the index of the previous occurence of `element` in the `list` starting from the `from` index (exclusive). + /// + /// ```motoko include=import + /// import Char "mo:core/Char"; + /// let list = List.fromArray(['c', 'o', 'f', 'f', 'e', 'e']); + /// assert List.prevIndexOf(list, Char.equal, 'c', List.size(list)) == ?0; + /// assert List.prevIndexOf(list, Char.equal, 'e', List.size(list)) == ?5; + /// assert List.prevIndexOf(list, Char.equal, 'e', 5) == ?4; + /// assert List.prevIndexOf(list, Char.equal, 'e', 4) == null; + /// ``` + /// + /// Runtime: O(size) + /// + /// Space: O(1) + public func prevIndexOf(list : List, equal : (T, T) -> Bool, element : T, fromExclusive : Nat) : ?Nat { + if (fromExclusive > size(list)) Prim.trap "List index out of bounds in prevIndexOf"; + let blocks = list.blocks; - let blockIndex = list.blockIndex; - let elementIndex = list.elementIndex; + let (blockIndex, elementIndex) = locate(fromExclusive); var i = blockIndex; if (elementIndex == 0) i -= 1; @@ -1021,7 +1396,7 @@ module { /// Space: `O(1)` /// /// *Runtime and space assumes that `compare` runs in `O(1)` time and space. - public func binarySearch(list : List, compare : (T, T) -> Order.Order, element : T) : { + public func binarySearch(list : List, compare : (T, T) -> Types.Order, element : T) : { #found : Nat; #insertionIndex : Nat } { @@ -1184,7 +1559,7 @@ module { /// List, then this may lead to unexpected results. /// /// Runtime: `O(1)` - public func values(list : List) : Iter.Iter = object { + public func values(list : List) : Types.Iter = object { let blocks = list.blocks.size(); var blockIndex = 0; var elementIndex = 0; @@ -1231,7 +1606,7 @@ module { /// Runtime: `O(1)` /// /// Warning: Allocates memory on the heap to store ?(Nat, T). - public func enumerate(list : List) : Iter.Iter<(Nat, T)> = object { + public func enumerate(list : List) : Types.Iter<(Nat, T)> = object { let blocks = list.blocks.size(); var blockIndex = 0; var elementIndex = 0; @@ -1282,7 +1657,7 @@ module { /// List, then this may lead to unexpected results. /// /// Runtime: `O(1)` - public func reverseValues(list : List) : Iter.Iter = object { + public func reverseValues(list : List) : Types.Iter = object { var blockIndex = list.blockIndex; var elementIndex = list.elementIndex; var db : [var ?T] = if (blockIndex < list.blocks.size()) { @@ -1324,7 +1699,7 @@ module { /// Runtime: `O(1)` /// /// Warning: Allocates memory on the heap to store ?(T, Nat). - public func reverseEnumerate(list : List) : Iter.Iter<(Nat, T)> = object { + public func reverseEnumerate(list : List) : Types.Iter<(Nat, T)> = object { var i = size(list); var blockIndex = list.blockIndex; var elementIndex = list.elementIndex; @@ -1346,7 +1721,7 @@ module { i -= 1; return ?(i, x) }; - case (_) Prim.trap(INTERNAL_ERROR) + case (_) Prim.trap INTERNAL_ERROR } } }; @@ -1370,7 +1745,7 @@ module { /// List, then this may lead to unexpected results. /// /// Runtime: `O(1)` - public func keys(list : List) : Iter.Iter = Nat.range(0, size(list)); + public func keys(list : List) : Types.Iter = Nat.range(0, size(list)); /// Creates a new List containing all elements from the provided iterator. /// Elements are added in the order they are returned by the iterator. @@ -1388,7 +1763,7 @@ module { /// ``` /// /// Runtime: `O(size)` - public func fromIter(iter : Iter.Iter) : List { + public func fromIter(iter : Types.Iter) : List { let list = empty(); for (element in iter) add(list, element); list @@ -1413,7 +1788,7 @@ module { /// The maximum number of elements in a `List` is 2^32. /// /// Runtime: `O(size)`, where n is the size of iter. - public func addAll(list : List, iter : Iter.Iter) { + public func addAll(list : List, iter : Types.Iter) { for (element in iter) add(list, element) }; @@ -1429,7 +1804,6 @@ module { /// /// Runtime: `O(size)` public func toArray(list : List) : [T] { - let blocks = list.blocks.size(); var blockIndex = 0; var elementIndex = 0; var sz = 0; @@ -1438,10 +1812,8 @@ module { func generator(_ : Nat) : T { if (elementIndex == sz) { blockIndex += 1; - if (blockIndex >= blocks) Prim.trap(INTERNAL_ERROR); db := list.blocks[blockIndex]; sz := db.size(); - if (sz == 0) Prim.trap(INTERNAL_ERROR); elementIndex := 0 }; switch (db[elementIndex]) { @@ -1449,7 +1821,7 @@ module { elementIndex += 1; return x }; - case (_) Prim.trap(INTERNAL_ERROR) + case (_) Prim.trap INTERNAL_ERROR } }; @@ -1475,29 +1847,20 @@ module { let blocks = newIndexBlockLength(Nat32.fromNat(if (elementIndex == 0) { blockIndex - 1 } else blockIndex)); let dataBlocks = VarArray.repeat<[var ?T]>([var], blocks); - func makeBlock(array : [T], p : Nat, len : Nat, fill : Nat) : [var ?T] { - let block = VarArray.repeat(null, len); - var j = 0; - var pos = p; - while (j < fill) { - block[j] := ?array[pos]; - j += 1; - pos += 1 - }; - block - }; - var i = 1; var pos = 0; while (i < blockIndex) { let len = dataBlockSize(i); - dataBlocks[i] := makeBlock(array, pos, len, len); + dataBlocks[i] := VarArray.tabulate(len, func i = ?array[pos + i]); pos += len; i += 1 }; - if (elementIndex != 0) { - dataBlocks[i] := makeBlock(array, pos, dataBlockSize(i), elementIndex) + if (elementIndex != 0 and blockIndex < blocks) { + dataBlocks[i] := VarArray.tabulate( + dataBlockSize(i), + func i = if (i < elementIndex) ?array[pos + i] else null + ) }; { @@ -1726,6 +2089,154 @@ module { } }; + func actualInterval(fromInclusive : Int, toExclusive : Int, size : Nat) : (Nat, Nat) { + let startInt = if (fromInclusive < 0) { + let s = size + fromInclusive; + if (s < 0) { 0 } else { s } + } else { + if (fromInclusive > size) { size } else { fromInclusive } + }; + let endInt = if (toExclusive < 0) { + let e = size + toExclusive; + if (e < 0) { 0 } else { e } + } else { + if (toExclusive > size) { size } else { toExclusive } + }; + (Prim.abs(startInt), Prim.abs(endInt)) + }; + + /// Returns an iterator over a slice of `list` starting at `fromInclusive` up to (but not including) `toExclusive`. + /// + /// Negative indices are relative to the end of the list. For example, `-1` corresponds to the last element in the list. + /// + /// If the indices are out of bounds, they are clamped to the list bounds. + /// If the first index is greater than the second, the function returns an empty iterator. + /// + /// ```motoko include=import + /// let list = List.fromArray([1, 2, 3, 4, 5]); + /// let iter1 = List.range(list, 3, List.size(list)); + /// assert iter1.next() == ?4; + /// assert iter1.next() == ?5; + /// assert iter1.next() == null; + /// + /// let iter2 = List.range(list, 3, -1); + /// assert iter2.next() == ?4; + /// assert iter2.next() == null; + /// + /// let iter3 = List.range(list, 0, 0); + /// assert iter3.next() == null; + /// ``` + /// + /// Runtime: O(1) + /// + /// Space: O(1) + public func range(list : List, fromInclusive : Int, toExclusive : Int) : Types.Iter = object { + let (start, end) = actualInterval(fromInclusive, toExclusive, size(list)); + let blocks = list.blocks.size(); + var blockIndex = 0; + var elementIndex = 0; + if (start != 0) { + let (block, element) = locate(start - 1); + blockIndex := block; + elementIndex := element + 1 + }; + var db : [var ?T] = list.blocks[blockIndex]; + var dbSize = db.size(); + var index = fromInclusive; + + public func next() : ?T { + if (index >= end) return null; + index += 1; + + if (elementIndex == dbSize) { + blockIndex += 1; + if (blockIndex >= blocks) return null; + db := list.blocks[blockIndex]; + dbSize := db.size(); + if (dbSize == 0) return null; + elementIndex := 0 + }; + let ret = db[elementIndex]; + elementIndex += 1; + ret + } + }; + + func sliceToArrayBase(list : List, start : Nat) : { + next(i : Nat) : T + } = object { + var blockIndex = 0; + var elementIndex = 0; + if (start != 0) { + let (block, element) = locate(start - 1); + blockIndex := block; + elementIndex := element + 1 + }; + var db : [var ?T] = list.blocks[blockIndex]; + var dbSize = db.size(); + + public func next(i : Nat) : T { + if (elementIndex == dbSize) { + blockIndex += 1; + db := list.blocks[blockIndex]; + dbSize := db.size(); + elementIndex := 0 + }; + switch (db[elementIndex]) { + case (?x) { + elementIndex += 1; + return x + }; + case null Prim.trap INTERNAL_ERROR + } + } + }; + + /// Returns a new array containing elements from `list` starting at index `fromInclusive` up to (but not including) index `toExclusive`. + /// If the indices are out of bounds, they are clamped to the array bounds. + /// + /// ```motoko include=import + /// let array = List.fromArray([1, 2, 3, 4, 5]); + /// + /// let slice1 = List.sliceToArray(array, 1, 4); + /// assert slice1 == [2, 3, 4]; + /// + /// let slice2 = List.sliceToArray(array, 1, -1); + /// assert slice2 == [2, 3, 4]; + /// ``` + /// + /// Runtime: O(toExclusive - fromInclusive) + /// + /// Space: O(toExclusive - fromInclusive) + public func sliceToArray(list : List, fromInclusive : Int, toExclusive : Int) : [T] { + let (start, end) = actualInterval(fromInclusive, toExclusive, size(list)); + Array.tabulate(end - start, sliceToArrayBase(list, start).next) + }; + + /// Returns a new var array containing elements from `list` starting at index `fromInclusive` up to (but not including) index `toExclusive`. + /// If the indices are out of bounds, they are clamped to the array bounds. + /// + /// ```motoko include=import + /// import VarArray "mo:core/VarArray"; + /// import Nat "mo:core/Nat"; + /// + /// let array = List.fromArray([1, 2, 3, 4, 5]); + /// + /// let slice1 = List.sliceToVarArray(array, 1, 4); + /// assert VarArray.equal(slice1, [var 2, 3, 4], Nat.equal); + /// + /// let slice2 = List.sliceToVarArray(array, 1, -1); + /// assert VarArray.equal(slice2, [var 2, 3, 4], Nat.equal); + /// ``` + /// + /// Runtime: O(toExclusive - fromInclusive) + /// + /// Space: O(toExclusive - fromInclusive) + public func sliceToVarArray(list : List, fromInclusive : Int, toExclusive : Int) : [var T] { + let (start, end) = actualInterval(fromInclusive, toExclusive, size(list)); + VarArray.tabulate(end - start, sliceToArrayBase(list, start).next) + }; + /// Like `forEachEntryRev` but iterates through the list in reverse order, /// from end to beginning. /// @@ -1859,7 +2370,7 @@ module { /// Space: `O(1)` /// /// *Runtime and space assumes that `compare` runs in O(1) time and space. - public func max(list : List, compare : (T, T) -> Order.Order) : ?T { + public func max(list : List, compare : (T, T) -> Types.Order) : ?T { var maxSoFar : T = switch (first(list)) { case (?x) x; case null return null @@ -1911,7 +2422,7 @@ module { /// Space: `O(1)` /// /// *Runtime and space assumes that `compare` runs in O(1) time and space. - public func min(list : List, compare : (T, T) -> Order.Order) : ?T { + public func min(list : List, compare : (T, T) -> Types.Order) : ?T { var minSoFar : T = switch (first(list)) { case (?x) x; case null return null @@ -2013,7 +2524,7 @@ module { /// Space: `O(1)` /// /// *Runtime and space assumes that `compare` runs in O(1) time and space. - public func compare(list1 : List, list2 : List, compare : (T, T) -> Order.Order) : Order.Order { + public func compare(list1 : List, list2 : List, compare : (T, T) -> Types.Order) : Types.Order { let blocks1 = list1.blocks; let blocks2 = list2.blocks; let blockCount = Nat.min(blocks1.size(), blocks2.size()); diff --git a/test/List.test.mo b/test/List.test.mo index 0ad12481..6a9dc931 100644 --- a/test/List.test.mo +++ b/test/List.test.mo @@ -970,6 +970,75 @@ run( ) ); +func joinWith(xs : List.List, sep : Text) : Text { + let size = List.size(xs); + + if (size == 0) return ""; + if (size == 1) return List.at(xs, 0); + + var result = List.at(xs, 0); + var i = 0; + label l loop { + i += 1; + if (i >= size) { break l }; + result #= sep # List.at(xs, i) + }; + result +}; + +func listTestable(testableA : T.Testable) : T.Testable> { + { + display = func(xs : List.List) : Text = "[var " # joinWith(List.map(xs, testableA.display), ", ") # "]"; + equals = func(xs1 : List.List, xs2 : List.List) : Bool = List.equal(xs1, xs2, testableA.equals) + } +}; + +run( + suite( + "mapResult", + [ + test( + "mapResult", + List.mapResult( + List.fromArray([1, 2, 3]), + func x { + if (x >= 0) { #ok(Int.abs x) } else { #err "error message" } + } + ), + M.equals(T.result, Text>(listTestable(T.natTestable), T.textTestable, #ok(List.fromArray([1, 2, 3])))) + ), + Suite.test( + "mapResult fail first", + List.mapResult( + List.fromArray([-1, 2, 3]), + func x { + if (x >= 0) { #ok(Int.abs x) } else { #err "error message" } + } + ), + M.equals(T.result, Text>(listTestable(T.natTestable), T.textTestable, #err "error message")) + ), + Suite.test( + "mapResult fail last", + List.mapResult( + List.fromArray([1, 2, -3]), + func x { + if (x >= 0) { #ok(Int.abs x) } else { #err "error message" } + } + ), + M.equals(T.result, Text>(listTestable(T.natTestable), T.textTestable, #err "error message")) + ), + Suite.test( + "mapResult empty", + List.mapResult( + List.fromArray([]), + func x = #ok x + ), + M.equals(T.result, Text>(listTestable(T.natTestable), T.textTestable, #ok(List.fromArray([])))) + ) + ] + ) +); + // Claude tests (from original Mops package) // Helper function to run tests @@ -996,7 +1065,17 @@ func testNew(n : Nat) : Bool { func testInit(n : Nat) : Bool { let vec = List.repeat(1, n); assertValid(vec); - List.size(vec) == n and (n == 0 or (List.at(vec, 0) == 1 and List.at(vec, n - 1 : Nat) == 1)) + if (List.size(vec) != n) { + Debug.print("Init failed: expected size " # Nat.toText(n) # ", got " # Nat.toText(List.size(vec))); + return false + }; + for (i in Nat.range(0, n)) { + if (List.at(vec, i) != 1) { + Debug.print("Init failed at index " # Nat.toText(i) # ": expected 1, got " # Nat.toText(List.at(vec, i))); + return false + } + }; + true }; func testAdd(n : Nat) : Bool { @@ -1101,10 +1180,10 @@ func testAt(n : Nat) : Bool { }; func testGet(n : Nat) : Bool { - let vec = List.fromArray(Array.tabulate(n, func(i) = i + 1)); + let vec = List.tabulate(n, func(i) = i); - for (i in Nat.range(1, n + 1)) { - switch (List.get(vec, i - 1 : Nat)) { + for (i in Nat.range(0, n)) { + switch (List.get(vec, i)) { case (?value) { if (value != i) { Debug.print("get: Mismatch at index " # Nat.toText(i) # ": expected ?" # Nat.toText(i) # ", got ?" # Nat.toText(value)); @@ -1118,14 +1197,13 @@ func testGet(n : Nat) : Bool { } }; - // Test out-of-bounds access - switch (List.get(vec, n)) { - case (null) { - // This is expected - }; - case (?value) { - Debug.print("get: Expected null for out-of-bounds access, got ?" # Nat.toText(value)); - return false + for (i in Nat.range(n, 3 * n + 3)) { + switch (List.get(vec, i)) { + case (?value) { + Debug.print("get: Unexpected value at index " # Nat.toText(i) # ": got ?" # Nat.toText(value)); + return false + }; + case (null) {} } }; @@ -1133,13 +1211,16 @@ func testGet(n : Nat) : Bool { }; func testPut(n : Nat) : Bool { - let vec = List.fromArray(Array.tabulate(n, func(i) = i)); - if (n == 0) { - true - } else { - List.put(vec, n - 1 : Nat, 100); - List.at(vec, n - 1 : Nat) == 100 - } + let vec = List.fromArray(Array.repeat(0, n)); + for (i in Nat.range(0, n)) { + List.put(vec, i, i + 1); + let value = List.at(vec, i); + if (value != i + 1) { + Debug.print("put: Mismatch at index " # Nat.toText(i) # ": expected " # Nat.toText(i + 1) # ", got " # Nat.toText(value)); + return false + } + }; + true }; func testClear(n : Nat) : Bool { @@ -1176,6 +1257,64 @@ func testMap(n : Nat) : Bool { List.equal(mapped, List.fromArray(Array.tabulate(n, func(i) = i * 2)), Nat.equal) }; +func testMapEntries(n : Nat) : Bool { + let vec = List.fromArray(Array.tabulate(n, func(i) = i)); + let mapped = List.mapEntries(vec, func(i, x) = i * x); + List.equal(mapped, List.fromArray(Array.tabulate(n, func(i) = i * i)), Nat.equal) +}; + +func testMapInPlace(n : Nat) : Bool { + let vec = List.fromArray(Array.tabulate(n, func(i) = i)); + List.mapInPlace(vec, func(x) = x * 2); + List.equal(vec, List.fromArray(Array.tabulate(n, func(i) = i * 2)), Nat.equal) +}; + +func testFlatMap(n : Nat) : Bool { + let vec = List.fromArray(Array.tabulate(n, func(i) = i)); + let flatMapped = List.flatMap(vec, func(x) = [x, x].vals()); + + let expected = List.fromArray(Array.tabulate(2 * n, func(i) = i / 2)); + List.equal(flatMapped, expected, Nat.equal) +}; + +func testRange(n : Nat) : Bool { + if (n > 10) return true; // Skip large ranges for performance + let vec = List.tabulate(n, func(i) = i); + for (left in Nat.range(0, n)) { + for (right in Nat.range(left, n + 1)) { + let range = Iter.toArray(List.range(vec, left, right)); + let expected = Array.tabulate(right - left, func(i) = left + i); + if (range != expected) { + Debug.print( + "Range mismatch for left = " # Nat.toText(left) # ", right = " # Nat.toText(right) # ": expected " # debug_show (expected) # ", got " # debug_show (range) + ); + return false + } + } + }; + true +}; + +func testSliceToArray(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 slice = List.sliceToArray(vec, left, right); + let sliceVar = List.sliceToVarArray(vec, left, right); + let expected = Array.tabulate(right - left, func(i) = left + i); + let expectedVar = VarArray.tabulate(right - left, func(i) = left + i); + if (slice != expected or not VarArray.equal(sliceVar, expectedVar, Nat.equal)) { + Debug.print( + "Slice mismatch for left = " # Nat.toText(left) # ", right = " # Nat.toText(right) # ": expected " # debug_show (expected) # ", got " # debug_show (slice) + ); + return false + } + } + }; + true +}; + func testIndexOf(n : Nat) : Bool { let vec = List.fromArray(Array.tabulate(2 * n, func(i) = i % n)); if (n == 0) { @@ -1279,9 +1418,28 @@ func testIsSorted(n : Nat) : Bool { }; func testToArray(n : Nat) : Bool { - let vec = List.fromArray(Array.tabulate(n, func(i) = i)); + let array = Array.tabulate(n, func(i) = i); + let vec = List.fromArray(array); assertValid(vec); - Array.equal(List.toArray(vec), Array.tabulate(n, func(i) = i), Nat.equal) + Array.equal(List.toArray(vec), array, Nat.equal) +}; + +func testToVarArray(n : Nat) : Bool { + let array = VarArray.tabulate(n, func(i) = i); + let vec = List.tabulate(n, func(i) = i); + VarArray.equal(List.toVarArray(vec), array, Nat.equal) +}; + +func testFromVarArray(n : Nat) : Bool { + let array = VarArray.tabulate(n, func(i) = i); + let vec = List.fromVarArray(array); + List.equal(vec, List.fromArray(Array.fromVarArray(array)), Nat.equal) +}; + +func testFromArray(n : Nat) : Bool { + let array = Array.tabulate(n, func(i) = i); + let vec = List.fromArray(array); + List.equal(vec, List.fromArray(array), Nat.equal) }; func testFromIter(n : Nat) : Bool { @@ -1436,6 +1594,180 @@ func testBinarySearch(n : Nat) : Bool { List.binarySearch(vec, Nat.compare, n * 2) == #insertionIndex(n) }; +func testFlatten(n : Nat) : Bool { + let vec = List.fromArray>( + Array.tabulate>( + n, + func(i) = List.fromArray(Array.tabulate(i + 1, func(j) = j)) + ) + ); + let flattened = List.flatten(vec); + let expectedSize = (n * (n + 1)) / 2; + + if (List.size(flattened) != expectedSize) { + Debug.print("Flatten size mismatch: expected " # Nat.toText(expectedSize) # ", got " # Nat.toText(List.size(flattened))); + return false + }; + + for (i in Nat.range(0, n)) { + for (j in Nat.range(0, i + 1)) { + if (List.at(flattened, (i * (i + 1)) / 2 + j) != j) { + Debug.print("Flatten value mismatch at index " # Nat.toText((i * (i + 1)) / 2 + j) # ": expected " # Nat.toText(j)); + return false + } + } + }; + + true +}; + +func testJoin(n : Nat) : Bool { + let iter = Array.tabulate>( + n, + func(i) = List.fromArray(Array.tabulate(i + 1, func(j) = j)) + ).vals(); + let flattened = List.join(iter); + let expectedSize = (n * (n + 1)) / 2; + + if (List.size(flattened) != expectedSize) { + Debug.print("Flatten size mismatch: expected " # Nat.toText(expectedSize) # ", got " # Nat.toText(List.size(flattened))); + return false + }; + + for (i in Nat.range(0, n)) { + for (j in Nat.range(0, i + 1)) { + if (List.at(flattened, (i * (i + 1)) / 2 + j) != j) { + Debug.print("Flatten value mismatch at index " # Nat.toText((i * (i + 1)) / 2 + j) # ": expected " # Nat.toText(j)); + return false + } + } + }; + + true +}; + +func testTabulate(n : Nat) : Bool { + let tabu = List.tabulate(n, func(i) = i); + + if (List.size(tabu) != n) { + Debug.print("Tabulate size mismatch: expected " # Nat.toText(n) # ", got " # Nat.toText(List.size(tabu))); + return false + }; + + for (i in Nat.range(0, n)) { + if (List.at(tabu, i) != i) { + Debug.print("Tabulate value mismatch at index " # Nat.toText(i) # ": expected " # Nat.toText(i) # ", got " # Nat.toText(List.at(tabu, i))); + return false + } + }; + + true +}; + +func testNextIndexOf(n : Nat) : Bool { + func nextIndexOf(vec : List.List, element : Nat, from : Nat) : ?Nat { + for (i in Nat.range(from, List.size(vec))) { + if (List.at(vec, i) == element) { + return ?i + } + }; + return null + }; + + if (n > 10) return true; // Skip large vectors for performance + + let vec = List.tabulate(n, func(i) = i); + for (from in Nat.range(0, n)) { + for (element in Nat.range(0, n + 1)) { + let actual = List.nextIndexOf(vec, Nat.equal, element, from); + let expected = nextIndexOf(vec, element, from); + if (expected != actual) { + Debug.print( + "nextIndexOf failed for element " # Nat.toText(element) # " from index " # Nat.toText(from) # ": expected " # debug_show (expected) # ", got " # debug_show (actual) + ); + return false + } + } + }; + true +}; + +func testPrevIndexOf(n : Nat) : Bool { + func prevIndexOf(vec : List.List, element : Nat, from : Nat) : ?Nat { + var i = from; + while (i > 0) { + i -= 1; + if (List.at(vec, i) == element) { + return ?i + } + }; + return null + }; + + if (n > 10) return true; // Skip large vectors for performance + + let vec = List.tabulate(n, func(i) = i); + for (from in Nat.range(0, n + 1)) { + for (element in Nat.range(0, n + 1)) { + let actual = List.prevIndexOf(vec, Nat.equal, element, from); + let expected = prevIndexOf(vec, element, from); + if (expected != actual) { + Debug.print( + "prevIndexOf failed for element " # Nat.toText(element) # " from index " # Nat.toText(from) # ": expected " # debug_show (expected) # ", got " # debug_show (actual) + ); + return false + } + } + }; + true +}; + +func testMin(n : Nat) : Bool { + if (n == 0) { + let vec = List.empty(); + if (List.min(vec, Nat.compare) != null) { + Debug.print("Min on empty list should return null"); + return false + }; + return true + }; + + let vec = List.fromArray(Array.tabulate(n, func(i) = i + 1)); + for (i in Nat.range(0, n)) { + List.put(vec, i, 0); + let min = List.min(vec, Nat.compare); + if (min != ?0) { + Debug.print("Min failed: expected ?0, got " # debug_show (min)); + return false + }; + List.put(vec, i, i + 1) + }; + true +}; + +func testMax(n : Nat) : Bool { + if (n == 0) { + let vec = List.empty(); + if (List.max(vec, Nat.compare) != null) { + Debug.print("Max on empty list should return null"); + return false + }; + return true + }; + + let vec = List.fromArray(Array.tabulate(n, func(i) = i + 1)); + for (i in Nat.range(0, n)) { + List.put(vec, i, n + 1); + let max = List.max(vec, Nat.compare); + if (max != ?(n + 1)) { + Debug.print("Max failed: expected ?" # Nat.toText(n + 1) # ", got " # debug_show (max)); + return false + }; + List.put(vec, i, i + 1) + }; + true +}; + // Run all tests func runAllTests() { runTest("testNew", testNew); @@ -1449,6 +1781,11 @@ func runAllTests() { runTest("testClear", testClear); runTest("testClone", testClone); runTest("testMap", testMap); + runTest("testMapEntries", testMapEntries); + runTest("testMapInPlace", testMapInPlace); + runTest("testFlatMap", testFlatMap); + runTest("testRange", testRange); + runTest("testSliceToArray", testSliceToArray); runTest("testIndexOf", testIndexOf); runTest("testLastIndexOf", testLastIndexOf); runTest("testContains", testContains); @@ -1456,6 +1793,9 @@ func runAllTests() { runTest("testSort", testSort); runTest("testIsSorted", testIsSorted); runTest("testToArray", testToArray); + runTest("testToVarArray", testToVarArray); + runTest("testFromVarArray", testFromVarArray); + runTest("testFromArray", testFromArray); runTest("testFromIter", testFromIter); runTest("testFoldLeft", testFoldLeft); runTest("testFoldRight", testFoldRight); @@ -1464,7 +1804,14 @@ func runAllTests() { runTest("testPure", testPure); runTest("testReverseForEach", testReverseForEach); runTest("testForEach", testForEach); - runTest("testBinarySearch", testBinarySearch) + runTest("testBinarySearch", testBinarySearch); + runTest("testFlatten", testFlatten); + runTest("testJoin", testJoin); + runTest("testTabulate", testTabulate); + runTest("testNextIndexOf", testNextIndexOf); + runTest("testPrevIndexOf", testPrevIndexOf); + runTest("testMin", testMin); + runTest("testMax", testMax) }; // Run all tests diff --git a/validation/api/api.lock.json b/validation/api/api.lock.json index b7e25ca8..e5b0413c 100644 --- a/validation/api/api.lock.json +++ b/validation/api/api.lock.json @@ -511,18 +511,18 @@ "name": "List", "exports": [ "public func add(list : List, element : T)", - "public func addAll(list : List, iter : Iter.Iter)", + "public func addAll(list : List, iter : Types.Iter)", "public func addRepeat(list : List, initValue : T, count : Nat)", "public func all(list : List, predicate : T -> Bool) : Bool", "public func any(list : List, predicate : T -> Bool) : Bool", "public func at(list : List, index : Nat) : T", - "public func binarySearch(list : List, compare : (T, T) -> Order.Order, element : T) : { #found : Nat; #insertionIndex : Nat }", + "public func binarySearch(list : List, compare : (T, T) -> Types.Order, element : T) : { #found : Nat; #insertionIndex : Nat }", "public func clear(list : List)", "public func clone(list : List) : List", - "public func compare(list1 : List, list2 : List, compare : (T, T) -> Order.Order) : Order.Order", + "public func compare(list1 : List, list2 : List, compare : (T, T) -> Types.Order) : Types.Order", "public func contains(list : List, equal : (T, T) -> Bool, element : T) : Bool", "public func empty() : List", - "public func enumerate(list : List) : Iter.Iter<(Nat, T)>", + "public func enumerate(list : List) : Types.Iter<(Nat, T)>", "public func equal(list1 : List, list2 : List, equal : (T, T) -> Bool) : Bool", "public func filter(list : List, predicate : T -> Bool) : List", "public func filterMap(list : List, f : T -> ?R) : List", @@ -530,43 +530,55 @@ "public func findIndex(list : List, predicate : T -> Bool) : ?Nat", "public func findLastIndex(list : List, predicate : T -> Bool) : ?Nat", "public func first(list : List) : ?T", + "public func flatMap(list : List, k : T -> Types.Iter) : List", + "public func flatten(lists : List>) : List", "public func foldLeft(list : List, base : A, combine : (A, T) -> A) : A", "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 fromArray(array : [T]) : List", - "public func fromIter(iter : Iter.Iter) : List", + "public func fromIter(iter : Types.Iter) : List", "public func fromPure(pure : PureList.List) : List", "public func fromVarArray(array : [var T]) : List", "public func get(list : List, index : Nat) : ?T", "public func indexOf(list : List, equal : (T, T) -> Bool, element : T) : ?Nat", "public func isEmpty(list : List) : Bool", - "public func isSorted(list : List, compare : (T, T) -> Order.Order) : Bool", - "public func keys(list : List) : Iter.Iter", + "public func isSorted(list : List, compare : (T, T) -> Types.Order) : Bool", + "public func join(lists : Types.Iter>) : List", + "public func keys(list : List) : Types.Iter", "public func last(list : List) : ?T", "public func lastIndexOf(list : List, equal : (T, T) -> Bool, element : T) : ?Nat", "public type List", "public func map(list : List, f : T -> R) : List", - "public func max(list : List, compare : (T, T) -> Order.Order) : ?T", - "public func min(list : List, compare : (T, T) -> Order.Order) : ?T", + "public func mapEntries(list : List, f : (T, Nat) -> R) : List", + "public func mapInPlace(list : List, f : T -> T)", + "public func mapResult(list : List, f : T -> Types.Result) : Types.Result, E>", + "public func max(list : List, compare : (T, T) -> Types.Order) : ?T", + "public func min(list : List, compare : (T, T) -> Types.Order) : ?T", + "public func nextIndexOf(list : List, equal : (T, T) -> Bool, element : T, fromInclusive : Nat) : ?Nat", + "public func prevIndexOf(list : List, equal : (T, T) -> Bool, element : T, fromExclusive : Nat) : ?Nat", "public func put(list : List, index : Nat, value : T)", + "public func range(list : List, fromInclusive : Int, toExclusive : Int) : Types.Iter", "public func removeLast(list : List) : ?T", "public func repeat(initValue : T, size : Nat) : List", "public func reverse(list : List) : List", - "public func reverseEnumerate(list : List) : Iter.Iter<(Nat, T)>", + "public func reverseEnumerate(list : List) : Types.Iter<(Nat, T)>", "public func reverseForEach(list : List, f : T -> ())", "public func reverseForEachEntry(list : List, f : (Nat, T) -> ())", "public func reverseInPlace(list : List)", - "public func reverseValues(list : List) : Iter.Iter", + "public func reverseValues(list : List) : Types.Iter", "public func singleton(element : T) : List", "public func size(list : List) : Nat", + "public func sliceToArray(list : List, fromInclusive : Int, toExclusive : Int) : [T]", + "public func sliceToVarArray(list : List, fromInclusive : Int, toExclusive : Int) : [var T]", "public func sort(list : List, compare : (T, T) -> Types.Order) : List", - "public func sortInPlace(list : List, compare : (T, T) -> Order.Order)", + "public func sortInPlace(list : List, compare : (T, T) -> Types.Order)", + "public func tabulate(size : Nat, generator : Nat -> T) : List", "public func toArray(list : List) : [T]", "public func toPure(list : List) : PureList.List", "public func toText(list : List, f : T -> Text) : Text", "public func toVarArray(list : List) : [var T]", - "public func values(list : List) : Iter.Iter" + "public func values(list : List) : Types.Iter" ] }, {