diff --git a/src/PersistentOrderedMap.mo b/src/PersistentOrderedMap.mo new file mode 100644 index 00000000..3522b9c0 --- /dev/null +++ b/src/PersistentOrderedMap.mo @@ -0,0 +1,957 @@ +/// Stable key-value map implemented as a red-black tree with nodes storing key-value pairs. +/// +/// A red-black tree is a balanced binary search tree ordered by the keys. +/// +/// The tree data structure internally colors each of its nodes either red or black, +/// and uses this information to balance the tree during the modifying operations. +/// +/// Performance: +/// * Runtime: `O(log(n))` worst case cost per insertion, removal, and retrieval operation. +/// * Space: `O(n)` for storing the entire tree. +/// `n` denotes the number of key-value entries (i.e. nodes) stored in the tree. +/// +/// Note: +/// * Map operations, such as retrieval, insertion, and removal create `O(log(n))` temporary objects that become garbage. +/// +/// Credits: +/// +/// The core of this implementation is derived from: +/// +/// * Ken Friis Larsen's [RedBlackMap.sml](https://github.com/kfl/mosml/blob/master/src/mosmllib/Redblackmap.sml), which itself is based on: +/// * Stefan Kahrs, "Red-black trees with types", Journal of Functional Programming, 11(4): 425-432 (2001), [version 1 in web appendix](http://www.cs.ukc.ac.uk/people/staff/smk/redblack/rb.html). + + +import Debug "Debug"; +import I "Iter"; +import List "List"; +import Nat "Nat"; +import O "Order"; + +module { + + /// Red-black tree of nodes with key-value entries, ordered by the keys. + /// The keys have the generic type `K` and the values the generic type `V`. + /// Leaves are considered implicitly black. + public type Map = { + #red : (Map, K, V, Map); + #black : (Map, K, V, Map); + #leaf + }; + + public type Direction = { #fwd; #bwd }; + + /// Operations on `Map`, that require a comparator. + /// + /// The object should be created once, then used for all the operations + /// with `Map` to ensure that the same comparator is used for every operation. + /// + /// `MapOps` contains methods that require `compare` internally: + /// operations that may reshape a `Map` or should find something. + public class MapOps(compare : (K, K) -> O.Order) { + + /// Returns a new map, containing all entries given by the iterator `i`. + /// If there are multiple entries with the same key the last one is taken. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let m = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(m)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(n * log(n))`. + /// Space: `O(n)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(n * log(n))` temporary objects that will be collected as garbage. + public func fromIter(i : I.Iter<(K, V)>) : Map + = Internal.fromIter(i, compare); + + /// Insert the value `value` with key `key` into the map `m`. Overwrites any existing entry with key `key`. + /// Returns a modified map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// var map = Map.empty(); + /// + /// map := mapOps.put(map, 0, "Zero"); + /// map := mapOps.put(map, 2, "Two"); + /// map := mapOps.put(map, 1, "One"); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func put(m : Map, key : K, value : V) : Map + = Internal.put(m, compare, key, value); + + /// Insert the value `value` with key `key` into the map `m`. Returns modified map and + /// the previous value associated with key `key` or `null` if no such value exists. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// let (map1, old1) = mapOps.replace(map0, 0, "Nil"); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map1)))); + /// Debug.print(debug_show(old1)); + /// // [(0, "Nil"), (1, "One"), (2, "Two")] + /// // ?"Zero" + /// + /// let (map2, old2) = mapOps.replace(map0, 3, "Three"); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map2)))); + /// Debug.print(debug_show(old2)); + /// // [(0, "Zero"), (1, "One"), (2, "Two"), (3, "Three")] + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func replace(m : Map, key : K, value : V) : (Map, ?V) + = Internal.replace(m, compare, key, value); + + /// Creates a new map by applying `f` to each entry in the map `m`. For each entry + /// `(k, v)` in the old map, if `f` evaluates to `null`, the entry is discarded. + /// Otherwise, the entry is transformed into a new entry `(k, v2)`, where + /// the new value `v2` is the result of applying `f` to `(k, v)`. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func f(key : Nat, val : Text) : ?Text { + /// if(key == 0) {null} + /// else { ?("Twenty " # val)} + /// }; + /// + /// let newMap = mapOps.mapFilter(map, f); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(newMap)))); + /// + /// // [(1, "Twenty One"), (2, "Twenty Two")] + /// ``` + /// + /// Runtime: `O(n)`. + /// Space: `O(n)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func mapFilter(m : Map, f : (K, V1) -> ?V2) : Map + = Internal.mapFilter(m, compare, f); + + /// Get the value associated with key `key` in the given map `m` if present and `null` otherwise. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show mapOps.get(map, 1)); + /// Debug.print(debug_show mapOps.get(map, 42)); + /// + /// // ?"One" + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func get(m : Map, key : K) : ?V + = Internal.get(m, compare, key); + + /// Deletes the entry with the key `key` from the map `m`. Has no effect if `key` is not + /// present in the map. Returns modified map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(mapOps.delete(map, 1))))); + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(mapOps.delete(map, 42))))); + /// + /// // [(0, "Zero"), (2, "Two")] + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func delete(m : Map, key : K) : Map + = Internal.delete(m, compare, key); + + /// Deletes the entry with the key `key`. Returns modified map and the + /// previous value associated with key `key` or `null` if no such value exists. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map0 = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// let (map1, old1) = mapOps.remove(map0, 0); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map1)))); + /// Debug.print(debug_show(old1)); + /// // [(1, "One"), (2, "Two")] + /// // ?"Zero" + /// + /// let (map2, old2) = mapOps.remove(map0, 42); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map2)))); + /// Debug.print(debug_show(old2)); + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// // null + /// ``` + /// + /// Runtime: `O(log(n))`. + /// Space: `O(1)` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map and + /// assuming that the `compare` function implements an `O(1)` comparison. + /// + /// Note: Creates `O(log(n))` temporary objects that will be collected as garbage. + public func remove(m : Map, key : K) : (Map, ?V) + = Internal.remove(m, compare, key); + + /// Create a new empty map. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// + /// let map = mapOps.empty(); + /// + /// Debug.print(debug_show(mapOps.size(map))); + /// + /// // 0 + /// ``` + /// + /// Cost of empty map creation + /// Runtime: `O(1)`. + /// Space: `O(1)` + public func empty() : Map = #leaf; + + /// Get an iterator for the entries of the map `m`, in ascending (`#fwd`) or descending (`#bwd`) order as specified by `direction`. + /// The iterator takes a snapshot view of the map and is not affected by concurrent modifications. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.iter(map, #fwd)))); + /// Debug.print(debug_show(Iter.toArray(mapOps.iter(map, #bwd)))); + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// // [(2, "Two"), (1, "One"), (0, "Zero")] + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func iter(m : Map, direction : Direction) : I.Iter<(K, V)> + = Internal.iter(m, direction); + + /// Returns an Iterator (`Iter`) over the key-value pairs in the map. + /// Iterator provides a single method `next()`, which returns + /// pairs in ascending order by keys, or `null` when out of pairs to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(map)))); + /// + /// + /// // [(0, "Zero"), (1, "One"), (2, "Two")] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func entries(m : Map) : I.Iter<(K, V)> = iter(m, #fwd); + + /// Returns an Iterator (`Iter`) over the keys of the map. + /// Iterator provides a single method `next()`, which returns + /// keys in ascending order, or `null` when out of keys to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.keys(map)))); + /// + /// // [0, 1, 2] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func keys(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : K {kv.0}); + + + /// Returns an Iterator (`Iter`) over the values of the map. + /// Iterator provides a single method `next()`, which returns + /// values in no specific order, or `null` when out of values to iterate over. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.vals(map)))); + /// + /// // ["Zero", "One", "Two"] + /// ``` + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: `O(log(n))` retained memory plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func vals(m : Map) : I.Iter + = I.map(entries(m), func(kv : (K, V)) : V {kv.1}); + + /// Creates a new map by applying `f` to each entry in the map `m`. Each entry + /// `(k, v)` in the old map is transformed into a new entry `(k, v2)`, where + /// the new value `v2` is created by applying `f` to `(k, v)`. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func f(key : Nat, _val : Text) : Nat = key * 2; + /// + /// let resMap = mapOps.map(map, f); + /// + /// Debug.print(debug_show(Iter.toArray(mapOps.entries(resMap)))); + /// + /// // [(0, 0), (1, 2), (2, 4)] + /// ``` + /// + /// Cost of mapping all the elements: + /// Runtime: `O(n)`. + /// Space: `O(n)` retained memory + /// where `n` denotes the number of key-value entries stored in the map. + public func map(m : Map, f : (K, V1) -> V2) : Map + = Internal.map(m, f); + + /// Determine the size of the map as the number of key-value entries. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// Debug.print(debug_show(mapOps.size(map))); + /// + /// // 3 + /// ``` + /// + /// Runtime: `O(n)`. + /// Space: `O(1)`. + /// where `n` denotes the number of key-value entries stored in the tree. + public func size(m : Map) : Nat + = Internal.size(m); + + /// Collapses the elements in the `map` into a single value by starting with `base` + /// and progressively combining keys and values into `base` with `combine`. Iteration runs + /// left to right. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(mapOps.foldLeft(map, (0, ""), folder))); + /// + /// // (3, "ZeroOneTwo") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func foldLeft( + map : Map, + base : Accum, + combine : (K, Value, Accum) -> Accum + ) : Accum + = Internal.foldLeft(map, base, combine); + + /// Collapses the elements in the `map` into a single value by starting with `base` + /// and progressively combining keys and values into `base` with `combine`. Iteration runs + /// right to left. + /// + /// Example: + /// ```motoko + /// import Map "mo:base/PersistentOrderedMap"; + /// import Nat "mo:base/Nat"; + /// import Iter "mo:base/Iter"; + /// import Debug "mo:base/Debug"; + /// + /// let mapOps = Map.MapOps(Nat.compare); + /// let map = mapOps.fromIter(Iter.fromArray([(0, "Zero"), (2, "Two"), (1, "One")])); + /// + /// func folder(key : Nat, val : Text, accum : (Nat, Text)) : ((Nat, Text)) + /// = (key + accum.0, accum.1 # val); + /// + /// Debug.print(debug_show(mapOps.foldRight(map, (0, ""), folder))); + /// + /// // (3, "TwoOneZero") + /// ``` + /// + /// Cost of iteration over all elements: + /// Runtime: `O(n)`. + /// Space: depends on `combine` function plus garbage, see the note below. + /// where `n` denotes the number of key-value entries stored in the map. + /// + /// Note: Full map iteration creates `O(n)` temporary objects that will be collected as garbage. + public func foldRight( + map : Map, + base : Accum, + combine : (K, Value, Accum) -> Accum + ) : Accum + = Internal.foldRight(map, base, combine); + }; + + module Internal { + + public func fromIter(i : I.Iter<(K,V)>, compare : (K, K) -> O.Order) : Map + { + var map = #leaf : Map; + for(val in i) { + map := put(map, compare, val.0, val.1); + }; + map + }; + + type IterRep = List.List<{ #tr : Map; #xy : (K, V) }>; + + public func iter(map : Map, direction : Direction) : I.Iter<(K, V)> { + let turnLeftFirst : MapTraverser + = func (l, x, y, r, ts) { ?(#tr(l), ?(#xy(x, y), ?(#tr(r), ts))) }; + + let turnRightFirst : MapTraverser + = func (l, x, y, r, ts) { ?(#tr(r), ?(#xy(x, y), ?(#tr(l), ts))) }; + + switch direction { + case (#fwd) IterMap(map, turnLeftFirst); + case (#bwd) IterMap(map, turnRightFirst) + } + }; + + type MapTraverser = (Map, K, V, Map, IterRep) -> IterRep; + + class IterMap(map : Map, mapTraverser : MapTraverser) { + var trees : IterRep = ?(#tr(map), null); + public func next() : ?(K, V) { + switch (trees) { + case (null) { null }; + case (?(#tr(#leaf), ts)) { + trees := ts; + next() + }; + case (?(#xy(xy), ts)) { + trees := ts; + ?xy + }; + case (?(#tr(#red(l, x, y, r)), ts)) { + trees := mapTraverser(l, x, y, r, ts); + next() + }; + case (?(#tr(#black(l, x, y, r)), ts)) { + trees := mapTraverser(l, x, y, r, ts); + next() + } + } + } + }; + + public func map(map : Map, f : (K, V1) -> V2) : Map { + func mapRec(m : Map) : Map { + switch m { + case (#leaf) { #leaf }; + case (#red(l, x, y, r)) { + #red(mapRec l, x, f(x,y), mapRec r) + }; + case (#black(l, x, y, r)) { + #black(mapRec l, x, f(x, y), mapRec r) + }; + } + }; + mapRec(map) + }; + + public func foldLeft( + map : Map, + base : Accum, + combine : (Key, Value, Accum) -> Accum + ) : Accum + { + switch (map) { + case (#leaf) { base }; + case (#red(l, k, v, r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + }; + case (#black(l, k, v, r)) { + let left = foldLeft(l, base, combine); + let middle = combine(k, v, left); + foldLeft(r, middle, combine) + } + } + }; + + public func foldRight( + map : Map, + base : Accum, + combine : (Key, Value, Accum) -> Accum + ) : Accum + { + switch (map) { + case (#leaf) { base }; + case (#red(l, k, v, r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + }; + case (#black(l, k, v, r)) { + let right = foldRight(r, base, combine); + let middle = combine(k, v, right); + foldRight(l, middle, combine) + } + } + }; + + public func mapFilter(map : Map, compare : (K, K) -> O.Order, f : (K, V1) -> ?V2) : Map{ + func combine(key : K, value1 : V1, acc : Map) : Map { + switch (f(key, value1)){ + case null { acc }; + case (?value2) { + put(acc, compare, key, value2) + } + } + }; + foldLeft(map, #leaf, combine) + }; + + public func size(t : Map) : Nat { + switch t { + case (#red(l, _, _, r)) { + size(l) + size(r) + 1 + }; + case (#black(l, _, _, r)) { + size(l) + size(r) + 1 + }; + case (#leaf) { 0 } + } + }; + + public func get(t : Map, compare : (K, K) -> O.Order, x : K) : ?V { + switch t { + case (#red(l, x1, y1, r)) { + switch (compare(x, x1)) { + case (#less) { get(l, compare, x) }; + case (#equal) { ?y1 }; + case (#greater) { get(r, compare, x) } + } + }; + case (#black(l, x1, y1, r)) { + switch (compare(x, x1)) { + case (#less) { get(l, compare, x) }; + case (#equal) { ?y1 }; + case (#greater) { get(r, compare, x) } + } + }; + case (#leaf) { null } + } + }; + + func redden(t : Map) : Map { + switch t { + case (#black (l, x, y, r)) { + (#red (l, x, y, r)) + }; + case _ { + Debug.trap "PersistentOrderedMap.red" + } + } + }; + + func lbalance(left : Map, x : K, y : V, right : Map) : Map { + switch (left, right) { + case (#red(#red(l1, x1, y1, r1), x2, y2, r2), r) { + #red( + #black(l1, x1, y1, r1), + x2, y2, + #black(r2, x, y, r)) + }; + case (#red(l1, x1, y1, #red(l2, x2, y2, r2)), r) { + #red( + #black(l1, x1, y1, l2), + x2, y2, + #black(r2, x, y, r)) + }; + case _ { + #black(left, x, y, right) + } + } + }; + + func rbalance(left : Map, x : K, y : V, right : Map) : Map { + switch (left, right) { + case (l, #red(l1, x1, y1, #red(l2, x2, y2, r2))) { + #red( + #black(l, x, y, l1), + x1, y1, + #black(l2, x2, y2, r2)) + }; + case (l, #red(#red(l1, x1, y1, r1), x2, y2, r2)) { + #red( + #black(l, x, y, l1), + x1, y1, + #black(r1, x2, y2, r2)) + }; + case _ { + #black(left, x, y, right) + }; + } + }; + + type ClashResolver = { old : A; new : A } -> A; + + func insertWith ( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V, + onClash : ClashResolver + ) + : Map{ + func ins(tree : Map) : Map { + switch tree { + case (#black(left, x, y, right)) { + switch (compare (key, x)) { + case (#less) { + lbalance(ins left, x, y, right) + }; + case (#greater) { + rbalance(left, x, y, ins right) + }; + case (#equal) { + let newVal = onClash({ new = val; old = y }); + #black(left, key, newVal, right) + } + } + }; + case (#red(left, x, y, right)) { + switch (compare (key, x)) { + case (#less) { + #red(ins left, x, y, right) + }; + case (#greater) { + #red(left, x, y, ins right) + }; + case (#equal) { + let newVal = onClash { new = val; old = y }; + #red(left, key, newVal, right) + } + } + }; + case (#leaf) { + #red(#leaf, key, val, #leaf) + } + }; + }; + switch (ins m) { + case (#red(left, x, y, right)) { + #black(left, x, y, right); + }; + case other { other }; + }; + }; + + public func replace( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V + ) + : (Map, ?V) { + var oldVal : ?V = null; + func onClash( clash : { old : V; new : V } ) : V + { + oldVal := ?clash.old; + clash.new + }; + let res = insertWith(m, compare, key, val, onClash); + (res, oldVal) + }; + + public func put ( + m : Map, + compare : (K, K) -> O.Order, + key : K, + val : V + ) : Map = replace(m, compare, key, val).0; + + + func balLeft(left : Map, x : K, y : V, right : Map) : Map { + switch (left, right) { + case (#red(l1, x1, y1, r1), r) { + #red( + #black(l1, x1, y1, r1), + x, y, + r) + }; + case (_, #black(l2, x2, y2, r2)) { + rbalance(left, x, y, #red(l2, x2, y2, r2)) + }; + case (_, #red(#black(l2, x2, y2, r2), x3, y3, r3)) { + #red( + #black(left, x, y, l2), + x2, y2, + rbalance(r2, x3, y3, redden r3)) + }; + case _ { Debug.trap "balLeft" }; + } + }; + + func balRight(left : Map, x : K, y : V, right : Map) : Map { + switch (left, right) { + case (l, #red(l1, x1, y1, r1)) { + #red( + l, + x, y, + #black(l1, x1, y1, r1)) + }; + case (#black(l1, x1, y1, r1), r) { + lbalance(#red(l1, x1, y1, r1), x, y, r); + }; + case (#red(l1, x1, y1, #black(l2, x2, y2, r2)), r3) { + #red( + lbalance(redden l1, x1, y1, l2), + x2, y2, + #black(r2, x, y, r3)) + }; + case _ { Debug.trap "balRight" }; + } + }; + + func append(left : Map, right: Map) : Map { + switch (left, right) { + case (#leaf, _) { right }; + case (_, #leaf) { left }; + case (#red (l1, x1, y1, r1), + #red (l2, x2, y2, r2)) { + switch (append (r1, l2)) { + case (#red (l3, x3, y3, r3)) { + #red( + #red(l1, x1, y1, l3), + x3, y3, + #red(r3, x2, y2, r2)) + }; + case r1l2 { + #red(l1, x1, y1, #red(r1l2, x2, y2, r2)) + } + } + }; + case (t1, #red(l2, x2, y2, r2)) { + #red(append(t1, l2), x2, y2, r2) + }; + case (#red(l1, x1, y1, r1), t2) { + #red(l1, x1, y1, append(r1, t2)) + }; + case (#black(l1, x1, y1, r1), #black (l2, x2, y2, r2)) { + switch (append (r1, l2)) { + case (#red (l3, x3, y3, r3)) { + #red( + #black(l1, x1, y1, l3), + x3, y3, + #black(r3, x2, y2, r2)) + }; + case r1l2 { + balLeft ( + l1, + x1, y1, + #black(r1l2, x2, y2, r2) + ) + } + } + } + } + }; + + public func delete(m : Map, compare : (K, K) -> O.Order, key : K) : Map + = remove(m, compare, key).0; + + public func remove(tree : Map, compare : (K, K) -> O.Order, x : K) : (Map, ?V) { + var y0 : ?V = null; + func delNode(left : Map, x1 : K, y1 : V, right : Map) : Map { + switch (compare (x, x1)) { + case (#less) { + let newLeft = del left; + switch left { + case (#black(_, _, _, _)) { + balLeft(newLeft, x1, y1, right) + }; + case _ { + #red(newLeft, x1, y1, right) + } + } + }; + case (#greater) { + let newRight = del right; + switch right { + case (#black(_, _, _, _)) { + balRight(left, x1, y1, newRight) + }; + case _ { + #red(left, x1, y1, newRight) + } + } + }; + case (#equal) { + y0 := ?y1; + append(left, right) + }; + } + }; + func del(tree : Map) : Map { + switch tree { + case (#red(left, x, y, right)) { + delNode(left, x, y, right) + }; + case (#black(left, x, y, right)) { + delNode(left, x, y, right) + }; + case (#leaf) { + tree + } + }; + }; + switch (del(tree)) { + case (#red(left, x, y, right)) { + (#black(left, x, y, right), y0); + }; + case other { (other, y0) }; + }; + } + } +} diff --git a/test/PersistentOrderedMap.prop.test.mo b/test/PersistentOrderedMap.prop.test.mo new file mode 100644 index 00000000..d0c12609 --- /dev/null +++ b/test/PersistentOrderedMap.prop.test.mo @@ -0,0 +1,240 @@ +// @testmode wasi + +import Map "../src/PersistentOrderedMap"; +import Nat "../src/Nat"; +import Iter "../src/Iter"; +import Debug "../src/Debug"; +import Array "../src/Array"; +import Option "../src/Option"; + +import Suite "mo:matchers/Suite"; +import T "mo:matchers/Testable"; +import M "mo:matchers/Matchers"; + +import Random2 "mo:base/Random"; + +let { run; test; suite } = Suite; + +class MapMatcher(expected : Map.Map) : M.Matcher> { + public func describeMismatch(actual : Map.Map, _description : M.Description) { + Debug.print(debug_show (Iter.toArray(natMap.entries(actual))) # " should be " # debug_show (Iter.toArray(natMap.entries(expected)))) + }; + + public func matches(actual : Map.Map) : Bool { + Iter.toArray(natMap.entries(actual)) == Iter.toArray(natMap.entries(expected)) + } +}; + +object Random { + var number = 4711; + public func next() : Nat { + number := (15485863 * number + 5) % 15485867; + number + }; + + public func nextNat(range: (Nat, Nat)): Nat { + let n = next(); + let v = n % (range.1 - range.0 + 1) + range.0; + v + }; + + public func nextEntries(range: (Nat, Nat), size: Nat): [(Nat, Text)] { + Array.tabulate<(Nat, Text)>(size, func(_ix) { + let key = nextNat(range); (key, debug_show(key)) } ) + } +}; + +let natMap = Map.MapOps(Nat.compare); + +func mapGen(samples_number: Nat, size: Nat, range: (Nat, Nat)): Iter.Iter> { + object { + var n = 0; + public func next(): ?Map.Map { + n += 1; + if (n > samples_number) { + null + } else { + ?natMap.fromIter(Random.nextEntries(range, size).vals()) + } + } + } +}; + + +func run_all_props(range: (Nat, Nat), size: Nat, map_samples: Nat, query_samples: Nat) { + func prop(name: Text, f: Map.Map -> Bool): Suite.Suite { + var error_msg: Text = ""; + test(name, do { + var error = true; + label stop for(map in mapGen(map_samples, size, range)) { + if (not f(map)) { + error_msg := "Property \"" # name # "\" failed\n"; + error_msg #= "\n m: " # debug_show(Iter.toArray(natMap.entries(map))); + break stop; + } + }; + error_msg + }, M.describedAs(error_msg, M.equals(T.text("")))) + }; + func prop_with_key(name: Text, f: (Map.Map, Nat) -> Bool): Suite.Suite { + var error_msg: Text = ""; + test(name, do { + label stop for(map in mapGen(map_samples, size, range)) { + for (_query_ix in Iter.range(0, query_samples-1)) { + let key = Random.nextNat(range); + if (not f(map, key)) { + error_msg #= "Property \"" # name # "\" failed"; + error_msg #= "\n m: " # debug_show(Iter.toArray(natMap.entries(map))); + error_msg #= "\n k: " # debug_show(key); + break stop; + } + } + }; + error_msg + }, M.describedAs(error_msg, M.equals(T.text("")))) + }; + run( + suite("Property tests", + [ + suite("empty", [ + test("get(empty(), k) == null", label res : Bool { + for (_query_ix in Iter.range(0, query_samples-1)) { + let k = Random.nextNat(range); + if(natMap.get(natMap.empty(), k) != null) + break res(false); + }; + true; + }, M.equals(T.bool(true))) + ]), + + suite("get & put", [ + prop_with_key("get(put(m, k, v), k) == ?v", func (m, k) { + natMap.get(natMap.put(m, k, "v"), k) == ?"v" + }), + prop_with_key("get(put(put(m, k, v1), k, v2), k) == ?v2", func (m, k) { + let (v1, v2) = ("V1", "V2"); + natMap.get(natMap.put(natMap.put(m, k, v1), k, v2), k) == v2 + }), + ]), + + suite("replace", [ + prop_with_key("replace(m, k, v).0 == put(m, k, v)", func (m, k) { + natMap.replace(m, k, "v").0 == natMap.put(m, k, "v") + }), + prop_with_key("replace(put(m, k, v1), k, v2).1 == ?v1", func (m, k) { + natMap.replace(natMap.put(m, k, "v1"), k, "v2").1 == ?"v1" + }), + prop_with_key("get(m, k) == null ==> replace(m, k, v).1 == null", func (m, k) { + if (natMap.get(m, k) == null) { + natMap.replace(m, k, "v").1 == null + } else { true } + }), + ]), + + suite("delete", [ + prop_with_key("get(m, k) == null ==> delete(m, k) == m", func (m, k) { + if (natMap.get(m, k) == null) { + MapMatcher(m).matches(natMap.delete(m, k)) + } else { true } + }), + prop_with_key("delete(put(m, k, v), k) == m", func (m, k) { + if (natMap.get(m, k) == null) { + MapMatcher(m).matches(natMap.delete(natMap.put(m, k, "v"), k)) + } else { true } + }), + prop_with_key("delete(delete(m, k), k)) == delete(m, k)", func (m, k) { + let m1 = natMap.delete(natMap.delete(m, k), k); + let m2 = natMap.delete(m, k); + MapMatcher(m2).matches(m1) + }) + ]), + + suite("remove", [ + prop_with_key("remove(m, k).0 == delete(m, k)", func (m, k) { + let m1 = natMap.remove(m, k).0; + let m2 = natMap.delete(m, k); + MapMatcher(m2).matches(m1) + }), + prop_with_key("remove(put(m, k, v), k).1 == ?v", func (m, k) { + natMap.remove(natMap.put(m, k, "v"), k).1 == ?"v" + }), + prop_with_key("remove(remove(m, k).0, k).1 == null", func (m, k) { + natMap.remove(natMap.remove(m, k).0, k).1 == null + }), + prop_with_key("put(remove(m, k).0, k, remove(m, k).1) == m", func (m, k) { + if (natMap.get(m, k) != null) { + MapMatcher(m).matches(natMap.put(natMap.remove(m, k).0, k, Option.get(natMap.remove(m, k).1, ""))) + } else { true } + }) + ]), + + suite("size", [ + prop_with_key("size(put(m, k, v)) == size(m) + int(get(m, k) == null)", func (m, k) { + natMap.size(natMap.put(m, k, "v")) == natMap.size(m) + (if (natMap.get(m, k) == null) {1} else {0}) + }), + prop_with_key("size(delete(m, k)) + int(get(m, k) != null) == size(m)", func (m, k) { + natMap.size(natMap.delete(m, k)) + (if (natMap.get(m, k) != null) {1} else {0}) == natMap.size(m) + }) + ]), + + suite("iter,keys,vals,entries", [ + prop("fromIter(iter(m, #fwd)) == m", func (m) { + MapMatcher(m).matches(natMap.fromIter(natMap.iter(m, #fwd))) + }), + prop("fromIter(iter(m, #bwd)) == m", func (m) { + MapMatcher(m).matches(natMap.fromIter(natMap.iter(m, #bwd))) + }), + prop("iter(m, #fwd) = zip(key(m), vals(m))", func (m) { + let k = natMap.keys(m); + let v = natMap.vals(m); + for (e in natMap.iter(m, #fwd)) { + if (e.0 != k.next() or e.1 != v.next()) + return false; + }; + return true; + }), + prop("entries(m) == iter(m, #fwd)", func (m) { + let it = natMap.iter(m, #fwd); + for (e in natMap.entries(m)) { + if (it.next() != e) + return false; + }; + return true + }) + ]), + + suite("mapFilter", [ + prop_with_key("get(mapFilter(m, (!=k)), k) == null", func (m, k) { + natMap.get(natMap.mapFilter(m, + func (ki, vi) { if (ki != k) {?vi} else {null}}), k) == null + }), + prop_with_key("get(mapFilter(put(m, k, v), (==k)), k) == ?v", func (m, k) { + natMap.get(natMap.mapFilter(natMap.put(m, k, "v"), + func (ki, vi) { if (ki == k) {?vi} else {null}}), k) == ?"v" + }) + ]), + + suite("map", [ + prop("map(m, id) == m", func (m) { + MapMatcher(m).matches(natMap.map(m, func (k, v) {v})) + }) + ]), + + suite("folds", [ + prop("foldLeft as iter(#fwd)", func (m) { + let it = natMap.iter(m, #fwd); + natMap.foldLeft(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + }), + prop("foldRight as iter(#bwd)", func(m) { + let it = natMap.iter(m, #bwd); + natMap.foldRight(m, true, func (k, v, acc) {acc and it.next() == ?(k, v)}) + }) + ]), + ])) +}; + +run_all_props((1, 3), 0, 1, 10); +run_all_props((1, 5), 5, 100, 100); +run_all_props((1, 10), 10, 100, 100); +run_all_props((1, 100), 20, 100, 100); +run_all_props((1, 1000), 100, 100, 100); diff --git a/test/PersistentOrderedMap.test.mo b/test/PersistentOrderedMap.test.mo new file mode 100644 index 00000000..adcf9799 --- /dev/null +++ b/test/PersistentOrderedMap.test.mo @@ -0,0 +1,541 @@ +// @testmode wasi + +import Map "../src/PersistentOrderedMap"; +import Nat "../src/Nat"; +import Iter "../src/Iter"; +import Debug "../src/Debug"; +import Array "../src/Array"; + +import Suite "mo:matchers/Suite"; +import T "mo:matchers/Testable"; +import M "mo:matchers/Matchers"; + +let { run; test; suite } = Suite; + +let entryTestable = T.tuple2Testable(T.natTestable, T.textTestable); + +let natMapOps = Map.MapOps(Nat.compare); + +class MapMatcher(expected : [(Nat, Text)]) : M.Matcher> { + public func describeMismatch(actual : Map.Map, _description : M.Description) { + Debug.print(debug_show (Iter.toArray(natMapOps.entries(actual))) # " should be " # debug_show (expected)) + }; + + public func matches(actual : Map.Map) : Bool { + Iter.toArray(natMapOps.entries(actual)) == expected + } +}; + +func checkMap(rbMap : Map.Map) { + ignore blackDepth(rbMap) +}; + +func blackDepth(node : Map.Map) : Nat { + func checkNode(left : Map.Map, key : Nat, right : Map.Map) : Nat { + checkKey(left, func(x) { x < key }); + checkKey(right, func(x) { x > key }); + let leftBlacks = blackDepth(left); + let rightBlacks = blackDepth(right); + assert (leftBlacks == rightBlacks); + leftBlacks + }; + switch node { + case (#leaf) 0; + case (#red(left, key, _, right)) { + let leftBlacks = checkNode(left, key, right); + assert (not isRed(left)); + assert (not isRed(right)); + leftBlacks + }; + case (#black(left, key, _, right)) { + checkNode(left, key, right) + 1 + } + } +}; + + +func isRed(node : Map.Map) : Bool { + switch node { + case (#red(_, _, _, _)) true; + case _ false + } +}; + +func checkKey(node : Map.Map, isValid : Nat -> Bool) { + switch node { + case (#leaf) {}; + case (#red( _, key, _, _)) { + assert (isValid(key)) + }; + case (#black( _, key, _, _)) { + assert (isValid(key)) + } + } +}; + +func insert(rbTree : Map.Map, key : Nat) : Map.Map { + let updatedTree = natMapOps.put(rbTree, key, debug_show (key)); + checkMap(updatedTree); + updatedTree +}; + +func getAll(rbTree : Map.Map, keys : [Nat]) { + for (key in keys.vals()) { + let value = natMapOps.get(rbTree, key); + assert (value == ?debug_show (key)) + } +}; + +func clear(initialRbMap : Map.Map) : Map.Map { + var rbMap = initialRbMap; + for ((key, value) in natMapOps.entries(initialRbMap)) { + // stable iteration + assert (value == debug_show (key)); + let (newMap, result) = natMapOps.remove(rbMap, key); + rbMap := newMap; + assert (result == ?debug_show (key)); + checkMap(rbMap) + }; + rbMap +}; + +func expectedEntries(keys : [Nat]) : [(Nat, Text)] { + Array.tabulate<(Nat, Text)>(keys.size(), func(index) { (keys[index], debug_show (keys[index])) }) +}; + +func concatenateKeys(key : Nat, value : Text, accum : Text) : Text { + accum # debug_show(key) +}; + +func concatenateValues(key : Nat, value : Text, accum : Text) : Text { + accum # value +}; + +func multiplyKeyAndConcat(key : Nat, value : Text) : Text { + debug_show(key * 2) # value +}; + +func ifKeyLessThan(threshold : Nat, f : (Nat, Text) -> Text) : (Nat, Text) -> ?Text + = func (key, value) { + if(key < threshold) + ?f(key, value) + else null + }; + +/* --------------------------------------- */ + +var buildTestMap = func() : Map.Map { + natMapOps.empty() +}; + +run( + suite( + "empty", + [ + test( + "size", + natMapOps.size(buildTestMap()), + M.equals(T.nat(0)) + ), + test( + "iterate forward", + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "iterate backward", + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "entries", + Iter.toArray(natMapOps.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, [])) + ), + test( + "keys", + Iter.toArray(natMapOps.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [])) + ), + test( + "vals", + Iter.toArray(natMapOps.vals(buildTestMap())), + M.equals(T.array(T.textTestable, [])) + ), + test( + "empty from iter", + natMapOps.fromIter(Iter.fromArray([])), + MapMatcher([]) + ), + test( + "get absent", + natMapOps.get(buildTestMap(), 0), + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "remove absent", + natMapOps.remove(buildTestMap(), 0).1, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "replace absent/no value", + natMapOps.replace(buildTestMap(), 0, "Test").1, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "replace absent/key appeared", + natMapOps.replace(buildTestMap(), 0, "Test").0, + MapMatcher([(0, "Test")]) + ), + test( + "empty right fold keys", + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("")) + ), + test( + "empty left fold keys", + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("")) + ), + test( + "empty right fold values", + natMapOps.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("")) + ), + test( + "empty left fold values", + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("")) + ), + test( + "traverse empty map", + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([]) + ), + test( + "empty map filter", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + ] + ) +); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + insert(natMapOps.empty(), 0); +}; + +var expected = expectedEntries([0]); + +run( + suite( + "single root", + [ + test( + "size", + natMapOps.size(buildTestMap()), + M.equals(T.nat(1)) + ), + test( + "iterate forward", + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "iterate backward", + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "entries", + Iter.toArray(natMapOps.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "keys", + Iter.toArray(natMapOps.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [0])) + ), + test( + "vals", + Iter.toArray(natMapOps.vals(buildTestMap())), + M.equals(T.array(T.textTestable, ["0"])) + ), + test( + "from iter", + natMapOps.fromIter(Iter.fromArray(expected)), + MapMatcher(expected) + ), + test( + "get", + natMapOps.get(buildTestMap(), 0), + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "replace function result", + natMapOps.replace(buildTestMap(), 0, "TEST").1, + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "replace map result", + do { + let rbMap = buildTestMap(); + natMapOps.replace(rbMap, 0, "TEST").0 + }, + MapMatcher([(0, "TEST")]) + ), + test( + "remove function result", + natMapOps.remove(buildTestMap(), 0).1, + M.equals(T.optional(T.textTestable, ?"0")) + ), + test( + "remove map result", + do { + var rbMap = buildTestMap(); + rbMap := natMapOps.remove(rbMap, 0).0; + checkMap(rbMap); + rbMap + }, + MapMatcher([]) + ), + test( + "right fold keys", + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("0")) + ), + test( + "left fold keys", + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("0")) + ), + test( + "right fold values", + natMapOps.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("0")) + ), + test( + "left fold values", + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("0")) + ), + test( + "traverse map", + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([(0, "00")]) + ), + test( + "map filter/filter all", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + test( + "map filter/no filer", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(1, multiplyKeyAndConcat)), + MapMatcher([(0, "00")]) + ), + ] + ) +); + +/* --------------------------------------- */ + +expected := expectedEntries([0, 1, 2]); + +func rebalanceTests(buildTestMap : () -> Map.Map) : [Suite.Suite] = + [ + test( + "size", + natMapOps.size(buildTestMap()), + M.equals(T.nat(3)) + ), + test( + "map match", + buildTestMap(), + MapMatcher(expected) + ), + test( + "iterate forward", + Iter.toArray(natMapOps.iter(buildTestMap(), #fwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "iterate backward", + Iter.toArray(natMapOps.iter(buildTestMap(), #bwd)), + M.equals(T.array<(Nat, Text)>(entryTestable, Array.reverse(expected))) + ), + test( + "entries", + Iter.toArray(natMapOps.entries(buildTestMap())), + M.equals(T.array<(Nat, Text)>(entryTestable, expected)) + ), + test( + "keys", + Iter.toArray(natMapOps.keys(buildTestMap())), + M.equals(T.array(T.natTestable, [0, 1, 2])) + ), + test( + "vals", + Iter.toArray(natMapOps.vals(buildTestMap())), + M.equals(T.array(T.textTestable, ["0", "1", "2"])) + ), + test( + "from iter", + natMapOps.fromIter(Iter.fromArray(expected)), + MapMatcher(expected) + ), + test( + "get all", + do { + let rbMap = buildTestMap(); + getAll(rbMap, [0, 1, 2]); + rbMap + }, + MapMatcher(expected) + ), + test( + "clear", + clear(buildTestMap()), + MapMatcher([]) + ), + test( + "right fold keys", + natMapOps.foldRight(buildTestMap(), "", concatenateKeys), + M.equals(T.text("210")) + ), + test( + "left fold keys", + natMapOps.foldLeft(buildTestMap(), "", concatenateKeys), + M.equals(T.text("012")) + ), + test( + "right fold values", + natMapOps.foldRight(buildTestMap(), "", concatenateValues), + M.equals(T.text("210")) + ), + test( + "left fold values", + natMapOps.foldLeft(buildTestMap(), "", concatenateValues), + M.equals(T.text("012")) + ), + test( + "traverse map", + natMapOps.map(buildTestMap(), multiplyKeyAndConcat), + MapMatcher([(0, "00"), (1, "21"), (2, "42")]) + ), + test( + "map filter/filter all", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(0, multiplyKeyAndConcat)), + MapMatcher([]) + ), + test( + "map filter/filter one", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(1, multiplyKeyAndConcat)), + MapMatcher([(0, "00")]) + ), + test( + "map filter/no filer", + natMapOps.mapFilter(buildTestMap(), ifKeyLessThan(3, multiplyKeyAndConcat)), + MapMatcher([(0, "00"), (1, "21"), (2, "42")]) + ), + ]; + +buildTestMap := func() : Map.Map { + var rbMap = natMapOps.empty() : Map.Map; + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 1); + rbMap := insert(rbMap, 0); + rbMap +}; + +run(suite("rebalance left, left", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = natMapOps.empty() : Map.Map; + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 1); + rbMap +}; + +run(suite("rebalance left, right", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = natMapOps.empty() : Map.Map; + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 2); + rbMap := insert(rbMap, 1); + rbMap +}; + +run(suite("rebalance right, left", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +buildTestMap := func() : Map.Map { + var rbMap = natMapOps.empty() : Map.Map; + rbMap := insert(rbMap, 0); + rbMap := insert(rbMap, 1); + rbMap := insert(rbMap, 2); + rbMap +}; + +run(suite("rebalance right, right", rebalanceTests(buildTestMap))); + +/* --------------------------------------- */ + +run( + suite( + "repeated operations", + [ + test( + "repeated insert", + do { + var rbMap = buildTestMap(); + assert (natMapOps.get(rbMap, 1) == ?"1"); + rbMap := natMapOps.put(rbMap, 1, "TEST-1"); + natMapOps.get(rbMap, 1) + }, + M.equals(T.optional(T.textTestable, ?"TEST-1")) + ), + test( + "repeated replace", + do { + let rbMap0 = buildTestMap(); + let (rbMap1, firstResult) = natMapOps.replace(rbMap0, 1, "TEST-1"); + assert (firstResult == ?"1"); + let (rbMap2, secondResult) = natMapOps.replace(rbMap1, 1, "1"); + assert (secondResult == ?"TEST-1"); + rbMap2 + }, + MapMatcher(expected) + ), + test( + "repeated remove", + do { + var rbMap0 = buildTestMap(); + let (rbMap1, result) = natMapOps.remove(rbMap0, 1); + assert (result == ?"1"); + checkMap(rbMap1); + natMapOps.remove(rbMap1, 1).1 + }, + M.equals(T.optional(T.textTestable, null : ?Text)) + ), + test( + "repeated delete", + do { + var rbMap = buildTestMap(); + rbMap := natMapOps.delete(rbMap, 1); + natMapOps.delete(rbMap, 1) + }, + MapMatcher(expectedEntries([0, 2])) + ) + ] + ) +);