diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj index 3dff498..fc91a6f 100644 --- a/src/Fable.Python.fsproj +++ b/src/Fable.Python.fsproj @@ -23,6 +23,7 @@ + diff --git a/src/stdlib/Collections.fs b/src/stdlib/Collections.fs new file mode 100644 index 0000000..d5d61e6 --- /dev/null +++ b/src/stdlib/Collections.fs @@ -0,0 +1,264 @@ +/// Type bindings for Python collections module: https://docs.python.org/3/library/collections.html +module Fable.Python.Collections + +open Fable.Core + +// fsharplint:disable MemberNames + +// ============================================================================ +// Counter +// ============================================================================ + +/// A dict subclass for counting hashable objects. +/// Elements are stored as dictionary keys and their counts are stored as values. +/// Counts are allowed to be any integer value including zero or negative counts. +/// See https://docs.python.org/3/library/collections.html#collections.Counter +[] +type Counter<'T>() = + /// Get the count for key; missing keys return 0 (unlike a regular dict) + [] + member _.Item(key: 'T) : int = nativeOnly + + /// Return elements and their counts as key-value pairs + member _.items() : seq<'T * int> = nativeOnly + + /// Return an iterator over elements, repeating each as many times as its count. + /// Elements with counts <= 0 are not included. + /// See https://docs.python.org/3/library/collections.html#collections.Counter.elements + member _.elements() : seq<'T> = nativeOnly + + /// Return the n most common elements and their counts (most common first). + /// If n is omitted, return all elements in counter order. + /// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common + member _.most_common() : seq<'T * int> = nativeOnly + + /// Return the n most common elements and their counts (most common first). + /// See https://docs.python.org/3/library/collections.html#collections.Counter.most_common + [] + member _.most_common(n: int) : seq<'T * int> = nativeOnly + + /// Return the total of all counts (requires Python 3.10+). + /// See https://docs.python.org/3/library/collections.html#collections.Counter.total + member _.total() : int = nativeOnly + + /// Add counts from the iterable or mapping; count becomes sum of old and new counts. + /// See https://docs.python.org/3/library/collections.html#collections.Counter.update + member _.update(iterable: 'T seq) : unit = nativeOnly + + /// Subtract counts from the iterable or mapping; count becomes difference. + /// Counts can become negative. + /// See https://docs.python.org/3/library/collections.html#collections.Counter.subtract + member _.subtract(iterable: 'T seq) : unit = nativeOnly + + /// Return a Counter from a sequence of elements. + /// See https://docs.python.org/3/library/collections.html#collections.Counter + [] + static member ofSeq(iterable: 'T seq) : Counter<'T> = nativeOnly + +// ============================================================================ +// defaultdict +// ============================================================================ + +/// A dict subclass that calls a factory to supply missing values. +/// When a key is not found, the factory function (called with no arguments) +/// is called to produce a new value, which is then stored and returned. +/// If the factory is not set (None), missing keys raise KeyError as normal. +/// See https://docs.python.org/3/library/collections.html#collections.defaultdict +[] +type defaultdict<'TKey, 'TValue>(defaultFactory: unit -> 'TValue) = + /// Get or set the value for key; missing keys invoke the factory + [] + member _.Item(key: 'TKey) : 'TValue = nativeOnly + + /// Set value for key + [] + member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly + + /// Return key-value pairs + member _.items() : seq<'TKey * 'TValue> = nativeOnly + + /// Return keys + member _.keys() : seq<'TKey> = nativeOnly + + /// Return values + member _.values() : seq<'TValue> = nativeOnly + + /// Return value for key if present, otherwise None. + /// Does NOT invoke the factory. + member _.get(key: 'TKey) : 'TValue option = nativeOnly + + /// Return value for key if present, otherwise defaultValue. + /// Does NOT invoke the factory. + [] + member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly + + /// If key is in the dict, return its value. + /// If not, insert key with the factory's value and return that value. + member _.setdefault(key: 'TKey) : 'TValue = nativeOnly + + /// Remove and return the value for key, or raise KeyError. + member _.pop(key: 'TKey) : 'TValue = nativeOnly + + /// Remove and return the value for key, or return defaultValue. + [] + member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly + + /// Merge another dict into this one + member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly + + /// Remove all items + member _.clear() : unit = nativeOnly + + /// Return a shallow copy + member _.copy() : defaultdict<'TKey, 'TValue> = nativeOnly + + /// Check if a key is present (does NOT invoke factory) + [] + member _.contains(key: 'TKey) : bool = nativeOnly + +// ============================================================================ +// deque +// ============================================================================ + +/// A double-ended queue with O(1) appends and pops from either end. +/// If maxlen is set, the deque is bounded to that maximum length; items are +/// discarded from the opposite end when the bound is reached. +/// See https://docs.python.org/3/library/collections.html#collections.deque +[] +type deque<'T>() = + /// Number of elements in the deque + [] + member _.length() : int = nativeOnly + + /// Get element at index + [] + member _.Item(index: int) : 'T = nativeOnly + + /// Maximum length of the deque, or None if unbounded + member _.maxlen : int option = nativeOnly + + /// Add item to the right end + member _.append(item: 'T) : unit = nativeOnly + + /// Add item to the left end + member _.appendleft(item: 'T) : unit = nativeOnly + + /// Remove and return item from the right end + member _.pop() : 'T = nativeOnly + + /// Remove and return item from the left end + member _.popleft() : 'T = nativeOnly + + /// Extend the right side of the deque by appending elements from iterable + member _.extend(iterable: 'T seq) : unit = nativeOnly + + /// Extend the left side of the deque by appending elements from iterable. + /// Note: each element is appended to the left, reversing the iterable order. + member _.extendleft(iterable: 'T seq) : unit = nativeOnly + + /// Rotate the deque n steps to the right. If n is negative, rotate left. + member _.rotate(n: int) : unit = nativeOnly + + /// Count the number of occurrences of value + [] + member _.count(value: 'T) : int = nativeOnly + + /// Return the position of value (raise ValueError if not found) + member _.index(value: 'T) : int = nativeOnly + + /// Insert value before position i + member _.insert(i: int, value: 'T) : unit = nativeOnly + + /// Remove the first occurrence of value (raise ValueError if not found) + member _.remove(value: 'T) : unit = nativeOnly + + /// Reverse the deque in-place + member _.reverse() : unit = nativeOnly + + /// Remove all elements + member _.clear() : unit = nativeOnly + + /// Return a shallow copy + member _.copy() : deque<'T> = nativeOnly + + /// Create a deque from a sequence + [] + static member ofSeq(iterable: 'T seq) : deque<'T> = nativeOnly + + /// Create a bounded deque from a sequence with maximum length + [] + static member ofSeq(iterable: 'T seq, maxlen: int) : deque<'T> = nativeOnly + + /// Create an empty bounded deque with maximum length + [] + static member withMaxlen(maxlen: int) : deque<'T> = nativeOnly + +// ============================================================================ +// OrderedDict +// ============================================================================ + +/// A dict subclass that remembers insertion order. Since Python 3.7, all dicts +/// maintain insertion order, but OrderedDict has a few extra features: +/// `move_to_end` and order-sensitive equality. +/// See https://docs.python.org/3/library/collections.html#collections.OrderedDict +[] +type OrderedDict<'TKey, 'TValue>() = + /// Get or set value for key + [] + member _.Item(key: 'TKey) : 'TValue = nativeOnly + + /// Set value for key + [] + member _.set(key: 'TKey, value: 'TValue) : unit = nativeOnly + + /// Return key-value pairs in insertion order + member _.items() : seq<'TKey * 'TValue> = nativeOnly + + /// Return keys in insertion order + member _.keys() : seq<'TKey> = nativeOnly + + /// Return values in insertion order + member _.values() : seq<'TValue> = nativeOnly + + /// Get value for key, or None if missing + member _.get(key: 'TKey) : 'TValue option = nativeOnly + + /// Get value for key, or defaultValue if missing + [] + member _.get(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly + + /// Remove and return the value for key (or raise KeyError) + member _.pop(key: 'TKey) : 'TValue = nativeOnly + + /// Remove and return the value for key, or return defaultValue + [] + member _.pop(key: 'TKey, defaultValue: 'TValue) : 'TValue = nativeOnly + + /// Move key to the end. If last is False, move to the beginning. + /// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end + member _.move_to_end(key: 'TKey) : unit = nativeOnly + + /// Move key to the end (last=True) or beginning (last=False). + [] + member _.move_to_end(key: 'TKey, last: bool) : unit = nativeOnly + + /// Remove and return a (key, value) pair. last=True removes from the end. + /// See https://docs.python.org/3/library/collections.html#collections.OrderedDict.popitem + member _.popitem() : 'TKey * 'TValue = nativeOnly + + /// Remove and return from end (last=True) or beginning (last=False). + [] + member _.popitem(last: bool) : 'TKey * 'TValue = nativeOnly + + /// Merge another dict into this one + member _.update(other: System.Collections.Generic.IDictionary<'TKey, 'TValue>) : unit = nativeOnly + + /// Remove all items + member _.clear() : unit = nativeOnly + + /// Return a shallow copy + member _.copy() : OrderedDict<'TKey, 'TValue> = nativeOnly + + /// Check if key is present + [] + member _.contains(key: 'TKey) : bool = nativeOnly diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj index 441f8a7..69c1f74 100644 --- a/test/Fable.Python.Test.fsproj +++ b/test/Fable.Python.Test.fsproj @@ -17,6 +17,7 @@ + diff --git a/test/TestCollections.fs b/test/TestCollections.fs new file mode 100644 index 0000000..931e48c --- /dev/null +++ b/test/TestCollections.fs @@ -0,0 +1,231 @@ +module Fable.Python.Tests.Collections + +open Fable.Python.Testing +open Fable.Python.Collections + +// ============================================================================ +// Counter tests +// ============================================================================ + +[] +let ``Counter: empty counter has zero count for missing key`` () = + let c = Counter() + c.Item("x") |> equal 0 + +[] +let ``Counter: ofSeq counts elements`` () = + let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ] + c.Item("a") |> equal 3 + c.Item("b") |> equal 2 + c.Item("c") |> equal 1 + +[] +let ``Counter: missing key returns 0`` () = + let c = Counter.ofSeq [ "a"; "b" ] + c.Item("z") |> equal 0 + +[] +let ``Counter: most_common returns all elements sorted by count`` () = + let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ] + let top = c.most_common() |> Seq.head + top |> equal ("a", 3) + +[] +let ``Counter: most_common n returns top n elements`` () = + let c = Counter.ofSeq [ "a"; "b"; "a"; "c"; "a"; "b" ] + let topTwo = c.most_common(2) |> Seq.toList + topTwo |> List.length |> equal 2 + topTwo |> List.head |> equal ("a", 3) + +[] +let ``Counter: elements returns repeated sequence`` () = + let c = Counter.ofSeq [ "a"; "a"; "b" ] + let elems = c.elements() |> Seq.toList |> List.sort + elems |> equal [ "a"; "a"; "b" ] + +[] +let ``Counter: total sums all counts`` () = + let c = Counter.ofSeq [ "a"; "b"; "a"; "c" ] + c.total() |> equal 4 + +[] +let ``Counter: update adds counts`` () = + let c = Counter.ofSeq [ "a"; "b" ] + c.update([ "a"; "c" ]) + c.Item("a") |> equal 2 + c.Item("c") |> equal 1 + +[] +let ``Counter: subtract reduces counts`` () = + let c = Counter.ofSeq [ "a"; "a"; "b" ] + c.subtract([ "a" ]) + c.Item("a") |> equal 1 + +// ============================================================================ +// defaultdict tests +// ============================================================================ + +[] +let ``defaultdict: missing key invokes factory`` () = + let d = defaultdict>(fun () -> ResizeArray()) + let list = d.Item("key") + list.Count |> equal 0 + +[] +let ``defaultdict: factory creates separate instances`` () = + let d = defaultdict>(fun () -> ResizeArray()) + let list1 = d.Item("a") + list1.Add(1) + let list2 = d.Item("b") + list2.Count |> equal 0 + +[] +let ``defaultdict: int factory starts at zero`` () = + let d = defaultdict(fun () -> 0) + d.Item("key") |> equal 0 + +[] +let ``defaultdict: get returns None for missing key without invoking factory`` () = + let mutable factoryCalled = false + let d = defaultdict(fun () -> factoryCalled <- true; 0) + let result = d.get("missing") + result |> equal None + factoryCalled |> equal false + +[] +let ``defaultdict: get with default returns default for missing key`` () = + let d = defaultdict(fun () -> 0) + d.get("missing", 42) |> equal 42 + +[] +let ``defaultdict: contains returns false for missing key`` () = + let d = defaultdict(fun () -> 0) + d.contains("key") |> equal false + +[] +let ``defaultdict: contains returns true after access`` () = + let d = defaultdict(fun () -> 99) + let _ = d.Item("key") + d.contains("key") |> equal true + +// ============================================================================ +// deque tests +// ============================================================================ + +[] +let ``deque: empty deque has length 0`` () = + let d = deque() + d.length() |> equal 0 + +[] +let ``deque: ofSeq creates deque from sequence`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + d.length() |> equal 3 + +[] +let ``deque: append adds to right`` () = + let d = deque.ofSeq [ 1; 2 ] + d.append(3) + d.Item(2) |> equal 3 + +[] +let ``deque: appendleft adds to left`` () = + let d = deque.ofSeq [ 1; 2 ] + d.appendleft(0) + d.Item(0) |> equal 0 + d.length() |> equal 3 + +[] +let ``deque: pop removes from right`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + let v = d.pop() + v |> equal 3 + d.length() |> equal 2 + +[] +let ``deque: popleft removes from left`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + let v = d.popleft() + v |> equal 1 + d.length() |> equal 2 + +[] +let ``deque: rotate shifts elements right`` () = + let d = deque.ofSeq [ 1; 2; 3; 4; 5 ] + d.rotate(2) + d.Item(0) |> equal 4 + d.Item(1) |> equal 5 + +[] +let ``deque: maxlen is None for unbounded deque`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + d.maxlen |> equal None + +[] +let ``deque: withMaxlen creates bounded deque`` () = + let d = deque.withMaxlen(3) + d.append(1) + d.append(2) + d.append(3) + d.append(4) // should push out 1 + d.length() |> equal 3 + d.Item(0) |> equal 2 + +[] +let ``deque: ofSeq with maxlen creates bounded deque`` () = + let d = deque.ofSeq ([ 1; 2; 3; 4; 5 ], 3) + d.length() |> equal 3 + d.maxlen |> equal (Some 3) + +[] +let ``deque: count occurrences`` () = + let d = deque.ofSeq [ 1; 2; 1; 3; 1 ] + d.count(1) |> equal 3 + +// ============================================================================ +// OrderedDict tests +// ============================================================================ + +[] +let ``OrderedDict: preserves insertion order`` () = + let od = OrderedDict() + od.set("a", 1) + od.set("b", 2) + od.set("c", 3) + od.keys() |> Seq.toList |> equal [ "a"; "b"; "c" ] + +[] +let ``OrderedDict: get existing key`` () = + let od = OrderedDict() + od.set("x", 42) + od.Item("x") |> equal 42 + +[] +let ``OrderedDict: get returns None for missing key`` () = + let od = OrderedDict() + od.get("missing") |> equal None + +[] +let ``OrderedDict: move_to_end moves last element`` () = + let od = OrderedDict() + od.set("a", 1) + od.set("b", 2) + od.set("c", 3) + od.move_to_end("a") + od.keys() |> Seq.toList |> equal [ "b"; "c"; "a" ] + +[] +let ``OrderedDict: move_to_end with last false moves to front`` () = + let od = OrderedDict() + od.set("a", 1) + od.set("b", 2) + od.set("c", 3) + od.move_to_end("c", false) + od.keys() |> Seq.toList |> equal [ "c"; "a"; "b" ] + +[] +let ``OrderedDict: contains returns correct result`` () = + let od = OrderedDict() + od.set("a", 1) + od.contains("a") |> equal true + od.contains("b") |> equal false