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..8727336 --- /dev/null +++ b/src/stdlib/Collections.fs @@ -0,0 +1,293 @@ +/// 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 the elements (keys) of the counter + member _.keys() : seq<'T> = nativeOnly + + /// Return the counts (values) of the counter + member _.values() : seq = 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 all elements and their counts, ordered from most common to least common. + /// 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; 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; 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 + + /// Remove and return the count for key, or raise KeyError if missing. + member _.pop(key: 'T) : int = nativeOnly + + /// Remove and return the count for key, or return defaultValue if missing. + [] + member _.pop(key: 'T, defaultValue: int) : int = nativeOnly + + /// Remove all items + member _.clear() : unit = nativeOnly + + /// Check if a key is present in the counter + [] + member _.contains(key: 'T) : bool = 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. +/// +/// Use the `withFactory` static method to attach a factory; the empty +/// constructor produces a defaultdict with no factory (missing keys raise KeyError). +/// See https://docs.python.org/3/library/collections.html#collections.defaultdict +[] +type defaultdict<'TKey, 'TValue>() = + /// Create a defaultdict with the given factory for missing keys. + /// The factory is invoked with no arguments and must return a new value of type 'TValue. + [] + static member withFactory(defaultFactory: unit -> 'TValue) : defaultdict<'TKey, 'TValue> = nativeOnly + + /// 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 + + /// Merge an iterable of key-value pairs into this dict + member _.update(items: seq<'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 + + /// Merge an iterable of key-value pairs into this dict + member _.update(items: seq<'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..a47f6c3 --- /dev/null +++ b/test/TestCollections.fs @@ -0,0 +1,258 @@ +module Fable.Python.Tests.Collections + +open Fable.Python.Testing +open Fable.Python.Collections + +// ============================================================================ +// Counter tests +// ============================================================================ + +[] +let ``test Counter empty counter has zero count for missing key`` () = + let c = Counter() + c.Item("x") |> equal 0 + +[] +let ``test 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 ``test Counter missing key returns 0`` () = + let c = Counter.ofSeq [ "a"; "b" ] + c.Item("z") |> equal 0 + +[] +let ``test 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 ``test 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 ``test 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 ``test Counter total sums all counts`` () = + let c = Counter.ofSeq [ "a"; "b"; "a"; "c" ] + c.total() |> equal 4 + +[] +let ``test 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 ``test Counter subtract reduces counts`` () = + let c = Counter.ofSeq [ "a"; "a"; "b" ] + c.subtract([ "a" ]) + c.Item("a") |> equal 1 + +[] +let ``test Counter contains reflects key presence`` () = + let c = Counter.ofSeq [ "a"; "b" ] + c.contains("a") |> equal true + c.contains("z") |> equal false + +[] +let ``test Counter keys and values enumerate the counter`` () = + let c = Counter.ofSeq [ "a"; "b"; "a" ] + c.keys() |> Seq.toList |> List.sort |> equal [ "a"; "b" ] + c.values() |> Seq.sum |> equal 3 + +[] +let ``test Counter pop removes and returns count`` () = + let c = Counter.ofSeq [ "a"; "a"; "b" ] + c.pop("a") |> equal 2 + c.contains("a") |> equal false + +// ============================================================================ +// defaultdict tests +// ============================================================================ + +[] +let ``test defaultdict missing key invokes factory`` () = + let d = defaultdict>.withFactory(fun () -> ResizeArray()) + let list = d.Item("key") + list.Count |> equal 0 + +[] +let ``test defaultdict factory creates separate instances`` () = + let d = defaultdict>.withFactory(fun () -> ResizeArray()) + let list1 = d.Item("a") + list1.Add(1) + let list2 = d.Item("b") + list2.Count |> equal 0 + +[] +let ``test defaultdict int factory starts at zero`` () = + let d = defaultdict.withFactory(fun () -> 0) + d.Item("key") |> equal 0 + +[] +let ``test defaultdict get returns None for missing key without invoking factory`` () = + let mutable factoryCalled = false + let d = defaultdict.withFactory(fun () -> factoryCalled <- true; 0) + let result = d.get("missing") + result |> equal None + factoryCalled |> equal false + +[] +let ``test defaultdict get with default returns default for missing key`` () = + let d = defaultdict.withFactory(fun () -> 0) + d.get("missing", 42) |> equal 42 + +[] +let ``test defaultdict contains returns false for missing key`` () = + let d = defaultdict.withFactory(fun () -> 0) + d.contains("key") |> equal false + +[] +let ``test defaultdict contains returns true after access`` () = + let d = defaultdict.withFactory(fun () -> 99) + let _ = d.Item("key") + d.contains("key") |> equal true + +// ============================================================================ +// deque tests +// ============================================================================ + +[] +let ``test deque empty deque has length 0`` () = + let d = deque() + d.length() |> equal 0 + +[] +let ``test deque ofSeq creates deque from sequence`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + d.length() |> equal 3 + +[] +let ``test deque append adds to right`` () = + let d = deque.ofSeq [ 1; 2 ] + d.append(3) + d.Item(2) |> equal 3 + +[] +let ``test deque appendleft adds to left`` () = + let d = deque.ofSeq [ 1; 2 ] + d.appendleft(0) + d.Item(0) |> equal 0 + d.length() |> equal 3 + +[] +let ``test deque pop removes from right`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + let v = d.pop() + v |> equal 3 + d.length() |> equal 2 + +[] +let ``test deque popleft removes from left`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + let v = d.popleft() + v |> equal 1 + d.length() |> equal 2 + +[] +let ``test 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 ``test deque maxlen is None for unbounded deque`` () = + let d = deque.ofSeq [ 1; 2; 3 ] + d.maxlen |> equal None + +[] +let ``test 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 ``test 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 ``test deque count occurrences`` () = + let d = deque.ofSeq [ 1; 2; 1; 3; 1 ] + d.count(1) |> equal 3 + +[] +let ``test deque extendleft reverses iterable order`` () = + let d = deque.ofSeq [ 3 ] + d.extendleft([ 1; 2 ]) + // Each element pushed onto the left in turn => final order [2; 1; 3] + d.Item(0) |> equal 2 + d.Item(1) |> equal 1 + d.Item(2) |> equal 3 + +// ============================================================================ +// OrderedDict tests +// ============================================================================ + +[] +let ``test 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 ``test OrderedDict get existing key`` () = + let od = OrderedDict() + od.set("x", 42) + od.Item("x") |> equal 42 + +[] +let ``test OrderedDict get returns None for missing key`` () = + let od = OrderedDict() + od.get("missing") |> equal None + +[] +let ``test 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 ``test 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 ``test OrderedDict contains returns correct result`` () = + let od = OrderedDict() + od.set("a", 1) + od.contains("a") |> equal true + od.contains("b") |> equal false