diff --git a/README.md b/README.md index 434ba14..ae3973b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ## Containers -This package is a collection of various container-like data structures. Currently only `SeqDict` and `SeqSet` are included. +This package is a collection of various container-like data structures including `SeqDict`, `SeqSet`, and bidirectional/multi-value dictionaries. Install using `lamdera install lamdera/containers`. @@ -19,6 +19,135 @@ For example insertions are `O(log(n))` rather than `O(n)` and fromList is `O(n * *Non-equatable Elm values are currently: functions, `Bytes`, `Html`, `Json.Value`, `Task`, `Cmd`, `Sub`, `Never`, `Texture`, `Shader`, and any datastructures containing these types. +## BiSeqDict, MultiSeqDict, and MultiBiSeqDict (bidirectional and multi-value dictionaries) + +These data structures extend the capabilities of `SeqDict` to handle more complex relationships: + +### BiSeqDict (Many-to-One) + +`BiSeqDict` is a bidirectional dictionary that maintains a reverse mapping from values back to keys. This is useful when: +- Multiple keys can map to the same value +- You need efficient lookups in both directions +- You want to find all keys associated with a particular value + +**Example with opaque types:** +```elm +import BiSeqDict exposing (BiSeqDict) + +-- Opaque ID types (not comparable!) +type UserId = UserId Never +type WorkspaceId = WorkspaceId Never + +-- Multiple users can belong to the same workspace +userWorkspaces : BiSeqDict (Id UserId) (Id WorkspaceId) +userWorkspaces = + BiSeqDict.empty + |> BiSeqDict.insert aliceId workspace1 + |> BiSeqDict.insert bobId workspace1 + |> BiSeqDict.insert charlieId workspace2 + +-- Forward lookup: What workspace does alice belong to? +BiSeqDict.get aliceId userWorkspaces +--> Just workspace1 + +-- Reverse lookup: Who are all members of workspace1? +BiSeqDict.getKeys workspace1 userWorkspaces +--> SeqSet.fromList [aliceId, bobId] +``` + +**Note:** This works with opaque ID types that aren't `comparable` - you couldn't do this with regular `Dict`! + +**Performance:** O(log n) for both forward and reverse lookups. + +### MultiSeqDict (One-to-Many) + +`MultiSeqDict` allows one key to map to multiple values. This is useful when: +- A single key naturally has multiple associated values +- You want to maintain a collection of values per key +- You need set semantics (no duplicate values per key) + +**Example with opaque types:** +```elm +import MultiSeqDict exposing (MultiSeqDict) + +type PropertyId = PropertyId Never +type UnitId = UnitId Never + +-- A property can have multiple units +propertyUnits : MultiSeqDict (Id PropertyId) (Id UnitId) +propertyUnits = + MultiSeqDict.empty + |> MultiSeqDict.insert property1 unit101 + |> MultiSeqDict.insert property1 unit102 + |> MultiSeqDict.insert property2 unit201 + +-- Get all units for property1 +MultiSeqDict.getAll property1 propertyUnits +--> SeqSet.fromList [unit101, unit102] + +-- Remove a specific unit +MultiSeqDict.remove property1 unit102 propertyUnits +``` + +**Performance:** O(log n) for lookups and insertions. + +### MultiBiSeqDict (Many-to-Many) + +`MultiBiSeqDict` combines both features: multiple values per key AND efficient reverse lookups. This is useful when: +- You have a many-to-many relationship +- You need lookups in both directions +- Each key can have multiple values and each value can be associated with multiple keys + +**Real-world example: Documents can belong to multiple chats** +```elm +import MultiBiSeqDict exposing (MultiBiSeqDict) + +type ChatId = ChatId Never +type DocumentId = DocumentId Never + +-- Documents can be shared across multiple chats +-- Chats can have multiple documents +-- Documents can be transferred between chats +chatDocuments : MultiBiSeqDict (Id ChatId) (Id DocumentId) +chatDocuments = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert chat1 doc1 + |> MultiBiSeqDict.insert chat1 doc2 + |> MultiBiSeqDict.insert chat2 doc1 -- doc1 is shared! + +-- What documents are in chat1? +MultiBiSeqDict.getAll chat1 chatDocuments +--> SeqSet.fromList [doc1, doc2] + +-- Which chats contain doc1? +MultiBiSeqDict.getKeys doc1 chatDocuments +--> SeqSet.fromList [chat1, chat2] + +-- Transfer doc2 from chat1 to chat3 +chatDocuments + |> MultiBiSeqDict.remove chat1 doc2 + |> MultiBiSeqDict.insert chat3 doc2 +``` + +**Why this is better than regular Dict:** +- ✅ Works with opaque ID types (not `comparable`) +- ✅ O(log n) queries in both directions +- ✅ Automatic consistency when transferring documents +- ❌ Regular `Dict` would require manual index maintenance and comparable keys + +**Performance:** O(log n) for lookups in both directions. + +### Key Features + +All three types: +- ✅ Work with any equatable types (no `comparable` constraint) +- ✅ Preserve insertion order +- ✅ Provide O(log n) performance for core operations +- ✅ Automatically maintain consistency (removing a key updates all related mappings) + +**Wire3 Support:** Full Lamdera Wire3 codec support is included for all three types (`encodeBiSeqDict`, `encodeMultiSeqDict`, `encodeMultiBiSeqDict`), allowing them to be used directly in your Lamdera `BackendModel`, `FrontendModel`, and messages. + + ## Comparison to other Elm packages See miniBill's [comparison of Elm Dict implementations](https://docs.google.com/spreadsheets/d/1j2rHUx5Nf5auvg5ikzYxbW4e1M9g0-hgU8nMogLD4EY) for a meta-analysis of implementation and performance characteristics. diff --git a/elm.json b/elm.json index 7c39142..126d836 100644 --- a/elm.json +++ b/elm.json @@ -6,7 +6,10 @@ "version": "1.0.0", "exposed-modules": [ "SeqDict", - "SeqSet" + "SeqSet", + "BiSeqDict", + "MultiSeqDict", + "MultiBiSeqDict" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { diff --git a/src/BiSeqDict.elm b/src/BiSeqDict.elm new file mode 100644 index 0000000..44cdd08 --- /dev/null +++ b/src/BiSeqDict.elm @@ -0,0 +1,422 @@ +module BiSeqDict exposing + ( BiSeqDict + , toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList + , empty, singleton, insert, update, remove + , isEmpty, member, get, size + , keys, values, toList, fromList + , map, foldl, foldr, filter, partition + , union, intersect, diff, merge + , encodeBiSeqDict, decodeBiSeqDict + ) + +{-| A dictionary that **maintains a mapping from the values back to keys,** +allowing for modelling **many-to-one relationships.** + +Example usage: + + manyToOne : BiSeqDict String Int + manyToOne = + BiSeqDict.empty + |> BiSeqDict.insert "A" 1 + |> BiSeqDict.insert "B" 2 + |> BiSeqDict.insert "C" 1 + |> BiSeqDict.insert "D" 4 + + BiSeqDict.getKeys 1 manyToOne + --> SeqSet.fromList ["A", "C"] + + +# Dictionaries + +@docs BiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList + + +# Build + +@docs empty, singleton, insert, update, remove + + +# Query + +@docs isEmpty, member, get, size + + +# Lists + +@docs keys, values, toList, fromList + + +# Transform + +@docs map, foldl, foldr, filter, partition + + +# Combine + +@docs union, intersect, diff, merge + + +# Internal + +@docs encodeBiSeqDict, decodeBiSeqDict + +-} + +import Bytes.Decode +import Lamdera.Wire3 +import SeqDict exposing (SeqDict) +import SeqSet exposing (SeqSet) + + +{-| The underlying data structure. Think about it as + + type alias BiSeqDict a b = + { forward : SeqDict a b -- just a normal Dict! + , reverse : SeqDict b (SeqSet a) -- the reverse mappings! + } + +-} +type BiSeqDict k v + = BiSeqDict + { forward : SeqDict k v + , reverse : SeqDict v (SeqSet k) + } + + +{-| Create an empty dictionary. +-} +empty : BiSeqDict k v +empty = + BiSeqDict + { forward = SeqDict.empty + , reverse = SeqDict.empty + } + + +{-| Create a dictionary with one key-value pair. +-} +singleton : k -> v -> BiSeqDict k v +singleton from to = + BiSeqDict + { forward = SeqDict.singleton from to + , reverse = SeqDict.singleton to (SeqSet.singleton from) + } + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : k -> v -> BiSeqDict k v -> BiSeqDict k v +insert from to (BiSeqDict d) = + BiSeqDict + { d + | forward = SeqDict.insert from to d.forward + , reverse = + let + oldTo = + SeqDict.get from d.forward + + reverseWithoutOld = + case oldTo of + Nothing -> + d.reverse + + Just oldTo_ -> + d.reverse + |> SeqDict.update oldTo_ + (Maybe.map (SeqSet.remove from) + >> Maybe.andThen normalizeSet + ) + in + reverseWithoutOld + |> SeqDict.update to (Maybe.withDefault SeqSet.empty >> SeqSet.insert from >> Just) + } + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : k -> (Maybe v -> Maybe v) -> BiSeqDict k v -> BiSeqDict k v +update from fn (BiSeqDict d) = + SeqDict.update from fn d.forward + |> fromDict + + +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : SeqSet k -> Maybe (SeqSet k) +normalizeSet set = + if SeqSet.isEmpty set then + Nothing + + else + Just set + + +{-| Remove a key-value pair from a dictionary. If the key is not found, +no changes are made. +-} +remove : k -> BiSeqDict k v -> BiSeqDict k v +remove from (BiSeqDict d) = + BiSeqDict + { d + | forward = SeqDict.remove from d.forward + , reverse = SeqDict.filterMap (\_ set -> SeqSet.remove from set |> normalizeSet) d.reverse + } + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : BiSeqDict k v -> Bool +isEmpty (BiSeqDict d) = + SeqDict.isEmpty d.forward + + +{-| Determine if a key is in a dictionary. +-} +member : k -> BiSeqDict k v -> Bool +member from (BiSeqDict d) = + SeqDict.member from d.forward + + +{-| Get the value associated with a key. If the key is not found, return +`Nothing`. This is useful when you are not sure if a key will be in the +dictionary. + + animals = fromList [ ("Tom", Cat), ("Jerry", Mouse) ] + + get "Tom" animals == Just Cat + get "Jerry" animals == Just Mouse + get "Spike" animals == Nothing + +-} +get : k -> BiSeqDict k v -> Maybe v +get from (BiSeqDict d) = + SeqDict.get from d.forward + + +{-| Get all keys associated with a value. If the value is not found, +return an empty set. +-} +getKeys : v -> BiSeqDict k v -> SeqSet k +getKeys to (BiSeqDict d) = + SeqDict.get to d.reverse + |> Maybe.withDefault SeqSet.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : BiSeqDict k v -> Int +size (BiSeqDict d) = + SeqDict.size d.forward + + +{-| Get all of the keys in a dictionary, sorted from lowest to highest. + + keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] + +-} +keys : BiSeqDict k v -> List k +keys (BiSeqDict d) = + SeqDict.keys d.forward + + +{-| Get all of the values in a dictionary, in the order of their keys. + + values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] + +-} +values : BiSeqDict k v -> List v +values (BiSeqDict d) = + SeqDict.values d.forward + + +{-| Get a list of unique values in the dictionary. +-} +uniqueValues : BiSeqDict k v -> List v +uniqueValues (BiSeqDict d) = + SeqDict.keys d.reverse + + +{-| Get a count of unique values in the dictionary. +-} +uniqueValuesCount : BiSeqDict k v -> Int +uniqueValuesCount (BiSeqDict d) = + SeqDict.size d.reverse + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : BiSeqDict k v -> List ( k, v ) +toList (BiSeqDict d) = + SeqDict.toList d.forward + + +{-| Convert a dictionary into a reverse association list of value-keys pairs. +-} +toReverseList : BiSeqDict k v -> List ( v, SeqSet k ) +toReverseList (BiSeqDict d) = + SeqDict.toList d.reverse + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( k, v ) -> BiSeqDict k v +fromList list = + SeqDict.fromList list + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (k -> v1 -> v2) -> BiSeqDict k v1 -> BiSeqDict k v2 +map fn (BiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.map fn d.forward + |> fromDict + + +{-| Convert BiSeqDict into a SeqDict. (Throw away the reverse mapping.) +-} +toDict : BiSeqDict k v -> SeqDict k v +toDict (BiSeqDict d) = + d.forward + + +{-| Convert Dict into a BiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict k v -> BiSeqDict k v +fromDict forward = + BiSeqDict + { forward = forward + , reverse = + forward + |> SeqDict.foldl + (\key value acc -> + SeqDict.update value + (\maybeKeys -> + Just <| + case maybeKeys of + Nothing -> + SeqSet.singleton key + + Just keys_ -> + SeqSet.insert key keys_ + ) + acc + ) + SeqDict.empty + } + + +{-| Fold over the key-value pairs in a dictionary in insertion order. +-} +foldl : (k -> v -> acc -> acc) -> acc -> BiSeqDict k v -> acc +foldl fn zero (BiSeqDict d) = + SeqDict.foldl fn zero d.forward + + +{-| Fold over the key-value pairs in a dictionary in reverse insertion order. +-} +foldr : (k -> v -> acc -> acc) -> acc -> BiSeqDict k v -> acc +foldr fn zero (BiSeqDict d) = + SeqDict.foldr fn zero d.forward + + +{-| Keep only the key-value pairs that pass the given test. +-} +filter : (k -> v -> Bool) -> BiSeqDict k v -> BiSeqDict k v +filter fn (BiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.filter fn d.forward + |> fromDict + + +{-| Partition a dictionary according to some test. The first dictionary +contains all key-value pairs which passed the test, and the second contains +the pairs that did not. +-} +partition : (k -> v -> Bool) -> BiSeqDict k v -> ( BiSeqDict k v, BiSeqDict k v ) +partition fn (BiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + let + ( forwardTrue, forwardFalse ) = + SeqDict.partition fn d.forward + in + ( fromDict forwardTrue + , fromDict forwardFalse + ) + + +{-| Combine two dictionaries. If there is a collision, preference is given +to the first dictionary. +-} +union : BiSeqDict k v -> BiSeqDict k v -> BiSeqDict k v +union (BiSeqDict left) (BiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.union left.forward right.forward + |> fromDict + + +{-| Keep a key-value pair when its key appears in the second dictionary. +Preference is given to values in the first dictionary. +-} +intersect : BiSeqDict k v -> BiSeqDict k v -> BiSeqDict k v +intersect (BiSeqDict left) (BiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.intersect left.forward right.forward + |> fromDict + + +{-| Keep a key-value pair when its key does not appear in the second dictionary. +-} +diff : BiSeqDict k v -> BiSeqDict k v -> BiSeqDict k v +diff (BiSeqDict left) (BiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.diff left.forward right.forward + |> fromDict + + +{-| The most general way of combining two dictionaries. You provide three +accumulators for when a given key appears: + +1. Only in the left dictionary. +2. In both dictionaries. +3. Only in the right dictionary. + +You then traverse all the keys from lowest to highest, building up whatever +you want. + +-} +merge : + (k -> v1 -> acc -> acc) + -> (k -> v1 -> v2 -> acc -> acc) + -> (k -> v2 -> acc -> acc) + -> BiSeqDict k v1 + -> BiSeqDict k v2 + -> acc + -> acc +merge fnLeft fnBoth fnRight (BiSeqDict left) (BiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left.forward right.forward zero + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +encodeBiSeqDict : (key -> Lamdera.Wire3.Encoder) -> (value -> Lamdera.Wire3.Encoder) -> BiSeqDict key value -> Lamdera.Wire3.Encoder +encodeBiSeqDict encKey encValue d = + Lamdera.Wire3.encodeList (Lamdera.Wire3.encodePair encKey encValue) (toList d) + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +decodeBiSeqDict : Lamdera.Wire3.Decoder k -> Lamdera.Wire3.Decoder value -> Lamdera.Wire3.Decoder (BiSeqDict k value) +decodeBiSeqDict decKey decValue = + Lamdera.Wire3.decodeList (Lamdera.Wire3.decodePair decKey decValue) |> Bytes.Decode.map fromList diff --git a/src/Internal/ListHelpers.elm b/src/Internal/ListHelpers.elm new file mode 100644 index 0000000..7249303 --- /dev/null +++ b/src/Internal/ListHelpers.elm @@ -0,0 +1,45 @@ +module Internal.ListHelpers exposing (gatherEqualsBy) + +{-| Internal list helper functions to avoid external dependencies. +-} + + +{-| Group equal elements together. A function is applied to each element of the list +and then the equality check is performed against the results of that function evaluation. +Elements will be grouped in the same order as they appear in the original list. The +same applies to elements within each group. + + gatherEqualsBy .age [{age=25},{age=23},{age=25}] + --> [({age=25},[{age=25}]),({age=23},[])] + +-} +gatherEqualsBy : (a -> b) -> List a -> List ( a, List a ) +gatherEqualsBy extract list = + gatherWith (\a b -> extract a == extract b) list + + +{-| Group equal elements together using a custom equality function. Elements will be +grouped in the same order as they appear in the original list. The same applies to +elements within each group. + + gatherWith (==) [1,2,1,3,2] + --> [(1,[1]),(2,[2]),(3,[])] + +-} +gatherWith : (a -> a -> Bool) -> List a -> List ( a, List a ) +gatherWith testFn list = + let + helper : List a -> List ( a, List a ) -> List ( a, List a ) + helper scattered gathered = + case scattered of + [] -> + List.reverse gathered + + toGather :: population -> + let + ( gathering, remaining ) = + List.partition (testFn toGather) population + in + helper remaining (( toGather, gathering ) :: gathered) + in + helper list [] diff --git a/src/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm new file mode 100644 index 0000000..e6c6948 --- /dev/null +++ b/src/MultiBiSeqDict.elm @@ -0,0 +1,444 @@ +module MultiBiSeqDict exposing + ( MultiBiSeqDict + , toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList + , empty, singleton, insert, update, remove, removeAll, removeValues + , isEmpty, member, getAll, size + , keys, values, toList, fromList + , map, foldl, foldr, filter, partition + , union, intersect, diff, merge + , encodeMultiBiSeqDict, decodeMultiBiSeqDict + ) + +{-| A dictionary mapping unique keys to **multiple** values, which +**maintains a mapping from the values back to keys,** allowing for +modelling **many-to-many relationships.** + +Example usage: + + manyToMany : MultiBiSeqDict String Int + manyToMany = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "A" 1 + |> MultiBiSeqDict.insert "B" 2 + |> MultiBiSeqDict.insert "C" 3 + |> MultiBiSeqDict.insert "A" 2 + + MultiBiSeqDict.getAll "A" manyToMany + --> SeqSet.fromList [1, 2] + + MultiBiSeqDict.getKeys 2 manyToMany + --> SeqSet.fromList ["A", "B"] + + +# Dictionaries + +@docs MultiBiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList + + +# Build + +@docs empty, singleton, insert, update, remove, removeAll, removeValues + + +# Query + +@docs isEmpty, member, getAll, size + + +# Lists + +@docs keys, values, toList, fromList + + +# Transform + +@docs map, foldl, foldr, filter, partition + + +# Combine + +@docs union, intersect, diff, merge + + +# Internal + +@docs encodeMultiBiSeqDict, decodeMultiBiSeqDict + +-} + +import Bytes.Decode +import Lamdera.Wire3 +import SeqDict exposing (SeqDict) +import SeqSet exposing (SeqSet) + + +{-| The underlying data structure. Think about it as + + type alias MultiBiSeqDict k v = + { forward : SeqDict k (SeqSet v) -- just a normal Dict! + , reverse : SeqDict v (SeqSet k) -- the reverse mappings! + } + +-} +type MultiBiSeqDict k v + = MultiBiSeqDict + { forward : SeqDict k (SeqSet v) + , reverse : SeqDict v (SeqSet k) + } + + +{-| Create an empty dictionary. +-} +empty : MultiBiSeqDict k v +empty = + MultiBiSeqDict + { forward = SeqDict.empty + , reverse = SeqDict.empty + } + + +{-| Create a dictionary with one key-value pair. +-} +singleton : k -> v -> MultiBiSeqDict k v +singleton from to = + MultiBiSeqDict + { forward = SeqDict.singleton from (SeqSet.singleton to) + , reverse = SeqDict.singleton to (SeqSet.singleton from) + } + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : k -> v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +insert from to (MultiBiSeqDict d) = + SeqDict.update + from + (\maybeSet -> + case maybeSet of + Nothing -> + Just (SeqSet.singleton to) + + Just set -> + Just (SeqSet.insert to set) + ) + d.forward + |> fromDict + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : k -> (SeqSet v -> SeqSet v) -> MultiBiSeqDict k v -> MultiBiSeqDict k v +update from fn (MultiBiSeqDict d) = + SeqDict.update from (Maybe.andThen (normalizeSet << fn)) d.forward + |> fromDict + + +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : SeqSet k -> Maybe (SeqSet k) +normalizeSet set = + if SeqSet.isEmpty set then + Nothing + + else + Just set + + +{-| Remove all key-value pairs for the given key from a dictionary. If the key is +not found, no changes are made. +-} +removeAll : k -> MultiBiSeqDict k v -> MultiBiSeqDict k v +removeAll from (MultiBiSeqDict d) = + MultiBiSeqDict + { d + | forward = SeqDict.remove from d.forward + , reverse = SeqDict.filterMap (\_ set -> SeqSet.remove from set |> normalizeSet) d.reverse + } + + +{-| Remove a single key-value pair from a dictionary. If the key is not found, +no changes are made. +-} +remove : k -> v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +remove from to (MultiBiSeqDict d) = + SeqDict.update from (Maybe.andThen (SeqSet.remove to >> normalizeSet)) d.forward + |> fromDict + + +{-| Remove all occurrences of a value from all keys in the dictionary. +-} +removeValues : v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +removeValues value (MultiBiSeqDict d) = + SeqDict.filterMap (\_ set -> SeqSet.remove value set |> normalizeSet) d.forward + |> fromDict + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : MultiBiSeqDict k v -> Bool +isEmpty (MultiBiSeqDict d) = + SeqDict.isEmpty d.forward + + +{-| Determine if a key is in a dictionary. +-} +member : k -> MultiBiSeqDict k v -> Bool +member from (MultiBiSeqDict d) = + SeqDict.member from d.forward + + +{-| Get all values associated with a key. If the key is not found, return +an empty set. + + animals = fromList [ ("Tom", Cat), ("Jerry", Mouse) ] + + getAll "Tom" animals == SeqSet.singleton Cat + getAll "Jerry" animals == SeqSet.singleton Mouse + getAll "Spike" animals == SeqSet.empty + +-} +getAll : k -> MultiBiSeqDict k v -> SeqSet v +getAll from (MultiBiSeqDict d) = + SeqDict.get from d.forward + |> Maybe.withDefault SeqSet.empty + + +{-| Get all keys associated with a value. If the value is not found, +return an empty set. +-} +getKeys : v -> MultiBiSeqDict k v -> SeqSet k +getKeys to (MultiBiSeqDict d) = + SeqDict.get to d.reverse + |> Maybe.withDefault SeqSet.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : MultiBiSeqDict k v -> Int +size (MultiBiSeqDict d) = + SeqDict.foldl (\_ set acc -> SeqSet.size set + acc) 0 d.forward + + +{-| Get all of the keys in a dictionary, sorted from lowest to highest. + + keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] + +-} +keys : MultiBiSeqDict k v -> List k +keys (MultiBiSeqDict d) = + SeqDict.keys d.forward + + +{-| Get all of the values in a dictionary, in the order of their keys. + + values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] + +-} +values : MultiBiSeqDict k v -> List v +values (MultiBiSeqDict d) = + SeqDict.values d.forward + |> List.concatMap SeqSet.toList + + +{-| Get a list of unique values in the dictionary. +-} +uniqueValues : MultiBiSeqDict k v -> List v +uniqueValues (MultiBiSeqDict d) = + SeqDict.keys d.reverse + + +{-| Get a count of unique values in the dictionary. +-} +uniqueValuesCount : MultiBiSeqDict k v -> Int +uniqueValuesCount (MultiBiSeqDict d) = + SeqDict.size d.reverse + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : MultiBiSeqDict k v -> List ( k, SeqSet v ) +toList (MultiBiSeqDict d) = + SeqDict.toList d.forward + + +{-| Convert a dictionary into a reverse association list of value-keys pairs. +-} +toReverseList : MultiBiSeqDict k v -> List ( v, SeqSet k ) +toReverseList (MultiBiSeqDict d) = + SeqDict.toList d.reverse + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( k, SeqSet v ) -> MultiBiSeqDict k v +fromList list = + SeqDict.fromList list + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (k -> v1 -> v2) -> MultiBiSeqDict k v1 -> MultiBiSeqDict k v2 +map fn (MultiBiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.map (\key set -> SeqSet.map (fn key) set) d.forward + |> fromDict + + +{-| Convert MultiBiSeqDict into a SeqDict. (Throw away the reverse mapping.) +-} +toDict : MultiBiSeqDict k v -> SeqDict k (SeqSet v) +toDict (MultiBiSeqDict d) = + d.forward + + +{-| Convert Dict into a MultiBiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict k (SeqSet v) -> MultiBiSeqDict k v +fromDict forward = + MultiBiSeqDict + { forward = forward + , reverse = + SeqDict.foldl + (\key set acc -> + SeqSet.foldl + (\value acc_ -> + SeqDict.update + value + (\maybeSet -> + case maybeSet of + Nothing -> + Just (SeqSet.singleton key) + + Just set_ -> + Just (SeqSet.insert key set_) + ) + acc_ + ) + acc + set + ) + SeqDict.empty + forward + } + + +{-| Fold over the key-value pairs in a dictionary in insertion order. +-} +foldl : (k -> SeqSet v -> acc -> acc) -> acc -> MultiBiSeqDict k v -> acc +foldl fn zero (MultiBiSeqDict d) = + SeqDict.foldl fn zero d.forward + + +{-| Fold over the key-value pairs in a dictionary in reverse insertion order. +-} +foldr : (k -> SeqSet v -> acc -> acc) -> acc -> MultiBiSeqDict k v -> acc +foldr fn zero (MultiBiSeqDict d) = + SeqDict.foldr fn zero d.forward + + +{-| Keep only the mappings that pass the given test. +-} +filter : (k -> v -> Bool) -> MultiBiSeqDict k v -> MultiBiSeqDict k v +filter fn (MultiBiSeqDict d) = + SeqDict.toList d.forward + |> List.filterMap + (\( key, values_ ) -> + values_ + |> SeqSet.filter (fn key) + |> normalizeSet + |> Maybe.map (Tuple.pair key) + ) + |> fromList + + +{-| Partition a dictionary according to some test. The first dictionary +contains all key-value pairs which passed the test, and the second contains +the pairs that did not. +-} +partition : (k -> SeqSet v -> Bool) -> MultiBiSeqDict k v -> ( MultiBiSeqDict k v, MultiBiSeqDict k v ) +partition fn (MultiBiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + let + ( forwardTrue, forwardFalse ) = + SeqDict.partition fn d.forward + in + ( fromDict forwardTrue + , fromDict forwardFalse + ) + + +{-| Combine two dictionaries. If there is a collision, preference is given +to the first dictionary. +-} +union : MultiBiSeqDict k v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +union (MultiBiSeqDict left) (MultiBiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.union left.forward right.forward + |> fromDict + + +{-| Keep a key-value pair when its key appears in the second dictionary. +Preference is given to values in the first dictionary. +-} +intersect : MultiBiSeqDict k v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +intersect (MultiBiSeqDict left) (MultiBiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.intersect left.forward right.forward + |> fromDict + + +{-| Keep a key-value pair when its key does not appear in the second dictionary. +-} +diff : MultiBiSeqDict k v -> MultiBiSeqDict k v -> MultiBiSeqDict k v +diff (MultiBiSeqDict left) (MultiBiSeqDict right) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.diff left.forward right.forward + |> fromDict + + +{-| The most general way of combining two dictionaries. You provide three +accumulators for when a given key appears: + +1. Only in the left dictionary. +2. In both dictionaries. +3. Only in the right dictionary. + +You then traverse all the keys from lowest to highest, building up whatever +you want. + +-} +merge : + (k -> SeqSet v1 -> acc -> acc) + -> (k -> SeqSet v1 -> SeqSet v2 -> acc -> acc) + -> (k -> SeqSet v2 -> acc -> acc) + -> MultiBiSeqDict k v1 + -> MultiBiSeqDict k v2 + -> acc + -> acc +merge fnLeft fnBoth fnRight (MultiBiSeqDict left) (MultiBiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left.forward right.forward zero + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +encodeMultiBiSeqDict : (key -> Lamdera.Wire3.Encoder) -> (value -> Lamdera.Wire3.Encoder) -> MultiBiSeqDict key value -> Lamdera.Wire3.Encoder +encodeMultiBiSeqDict encKey encValue d = + Lamdera.Wire3.encodeList (Lamdera.Wire3.encodePair encKey (SeqSet.encodeSet encValue)) (toList d) + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +decodeMultiBiSeqDict : Lamdera.Wire3.Decoder k -> Lamdera.Wire3.Decoder value -> Lamdera.Wire3.Decoder (MultiBiSeqDict k value) +decodeMultiBiSeqDict decKey decValue = + Lamdera.Wire3.decodeList (Lamdera.Wire3.decodePair decKey (SeqSet.decodeSet decValue)) |> Bytes.Decode.map fromList diff --git a/src/MultiSeqDict.elm b/src/MultiSeqDict.elm new file mode 100644 index 0000000..72f7d20 --- /dev/null +++ b/src/MultiSeqDict.elm @@ -0,0 +1,392 @@ +module MultiSeqDict exposing + ( MultiSeqDict + , toDict, fromDict + , empty, singleton, insert, update, remove, removeAll, removeValues + , isEmpty, member, getAll, size + , keys, values, toList, fromList, fromFlatList + , map, foldl, foldr, filter, partition + , union, intersect, diff, merge + , encodeMultiSeqDict, decodeMultiSeqDict + ) + +{-| A dictionary mapping unique keys to **multiple** values, allowing for +modelling **one-to-many relationships.** + +Example usage: + + oneToMany : MultiSeqDict String Int + oneToMany = + MultiSeqDict.empty + |> MultiSeqDict.insert "A" 1 + |> MultiSeqDict.insert "B" 2 + |> MultiSeqDict.insert "C" 3 + |> MultiSeqDict.insert "A" 2 + + MultiSeqDict.getAll "A" oneToMany + --> SeqSet.fromList [1, 2] + + +# Dictionaries + +@docs MultiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict + + +# Build + +@docs empty, singleton, insert, update, remove, removeAll, removeValues + + +# Query + +@docs isEmpty, member, getAll, size + + +# Lists + +@docs keys, values, toList, fromList, fromFlatList + + +# Transform + +@docs map, foldl, foldr, filter, partition + + +# Combine + +@docs union, intersect, diff, merge + + +# Internal + +@docs encodeMultiSeqDict, decodeMultiSeqDict + +-} + +import Bytes.Decode +import Internal.ListHelpers exposing (gatherEqualsBy) +import Lamdera.Wire3 +import SeqDict exposing (SeqDict) +import SeqSet exposing (SeqSet) + + +{-| The underlying data structure. Think about it as + + type alias MultiSeqDict k v = + Dict k (SeqSet v) -- just a normal Dict! + +-} +type MultiSeqDict k v + = MultiSeqDict (SeqDict k (SeqSet v)) + + +{-| Create an empty dictionary. +-} +empty : MultiSeqDict k v +empty = + MultiSeqDict SeqDict.empty + + +{-| Create a dictionary with one key-value pair. +-} +singleton : k -> v -> MultiSeqDict k v +singleton from to = + MultiSeqDict (SeqDict.singleton from (SeqSet.singleton to)) + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : k -> v -> MultiSeqDict k v -> MultiSeqDict k v +insert from to (MultiSeqDict d) = + MultiSeqDict <| + SeqDict.update + from + (\maybeSet -> + case maybeSet of + Nothing -> + Just (SeqSet.singleton to) + + Just set -> + Just (SeqSet.insert to set) + ) + d + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : k -> (SeqSet v -> SeqSet v) -> MultiSeqDict k v -> MultiSeqDict k v +update from fn (MultiSeqDict d) = + MultiSeqDict <| SeqDict.update from (Maybe.andThen (normalizeSet << fn)) d + + +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : SeqSet k -> Maybe (SeqSet k) +normalizeSet set = + if SeqSet.isEmpty set then + Nothing + + else + Just set + + +{-| Remove all key-value pairs for the given key from a dictionary. If the key is +not found, no changes are made. +-} +removeAll : k -> MultiSeqDict k v -> MultiSeqDict k v +removeAll from (MultiSeqDict d) = + MultiSeqDict (SeqDict.remove from d) + + +{-| Remove a single key-value pair from a dictionary. If the key is not found, +no changes are made. +-} +remove : k -> v -> MultiSeqDict k v -> MultiSeqDict k v +remove from to (MultiSeqDict d) = + MultiSeqDict <| + SeqDict.update from (Maybe.andThen (SeqSet.remove to >> normalizeSet)) d + + +{-| Remove all occurrences of a value from all keys in the dictionary. +-} +removeValues : v -> MultiSeqDict k v -> MultiSeqDict k v +removeValues value (MultiSeqDict d) = + MultiSeqDict <| + SeqDict.filterMap (\_ set -> SeqSet.remove value set |> normalizeSet) d + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : MultiSeqDict k v -> Bool +isEmpty (MultiSeqDict d) = + SeqDict.isEmpty d + + +{-| Determine if a key is in a dictionary. +-} +member : k -> MultiSeqDict k v -> Bool +member from (MultiSeqDict d) = + SeqDict.member from d + + +{-| Get all values associated with a key. If the key is not found, return +an empty set. + + animals = fromList [ ("Tom", "cat"), ("Jerry", "mouse") ] + + getAll "Tom" animals == SeqSet.singleton "cat" + getAll "Jerry" animals == SeqSet.singleton "mouse" + getAll "Spike" animals == SeqSet.empty + +-} +getAll : k -> MultiSeqDict k v -> SeqSet v +getAll from (MultiSeqDict d) = + SeqDict.get from d + |> Maybe.withDefault SeqSet.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : MultiSeqDict k v -> Int +size (MultiSeqDict d) = + SeqDict.foldl (\_ set acc -> SeqSet.size set + acc) 0 d + + +{-| Get all of the keys in a dictionary, sorted from lowest to highest. + + keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] + +-} +keys : MultiSeqDict k v -> List k +keys (MultiSeqDict d) = + SeqDict.keys d + + +{-| Get all of the values in a dictionary, in the order of their keys. + + values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] + +-} +values : MultiSeqDict k v -> List v +values (MultiSeqDict d) = + SeqDict.values d + |> List.concatMap SeqSet.toList + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : MultiSeqDict k v -> List ( k, SeqSet v ) +toList (MultiSeqDict d) = + SeqDict.toList d + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( k, SeqSet v ) -> MultiSeqDict k v +fromList list = + SeqDict.fromList list + |> fromDict + + +{-| Convert an association list into a dictionary. + + fromFlatList + [ ( "foo", 1 ) + , ( "bar", 2 ) + , ( "foo", 3 ) + ] + +results in the same dict as + + fromList + [ ( "foo", SeqSet.fromList [ 1, 3 ] ) + , ( "bar", SeqSet.fromList [ 2 ] ) + ] + +-} +fromFlatList : List ( k, v ) -> MultiSeqDict k v +fromFlatList list = + list + |> gatherEqualsBy Tuple.first + |> List.map + (\( ( key, _ ) as x, xs ) -> + ( key + , SeqSet.fromList <| List.map Tuple.second <| x :: xs + ) + ) + |> SeqDict.fromList + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (k -> v1 -> v2) -> MultiSeqDict k v1 -> MultiSeqDict k v2 +map fn (MultiSeqDict d) = + MultiSeqDict <| SeqDict.map (\key set -> SeqSet.map (fn key) set) d + + +{-| Convert MultiSeqDict into a SeqDict. (Throw away the reverse mapping.) +-} +toDict : MultiSeqDict k v -> SeqDict k (SeqSet v) +toDict (MultiSeqDict d) = + d + + +{-| Convert Dict into a MultiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict k (SeqSet v) -> MultiSeqDict k v +fromDict dict = + MultiSeqDict dict + + +{-| Fold over the key-value pairs in a dictionary in insertion order. +-} +foldl : (k -> SeqSet v -> acc -> acc) -> acc -> MultiSeqDict k v -> acc +foldl fn zero (MultiSeqDict d) = + SeqDict.foldl fn zero d + + +{-| Fold over the key-value pairs in a dictionary in reverse insertion order. +-} +foldr : (k -> SeqSet v -> acc -> acc) -> acc -> MultiSeqDict k v -> acc +foldr fn zero (MultiSeqDict d) = + SeqDict.foldr fn zero d + + +{-| Keep only the mappings that pass the given test. +-} +filter : (k -> v -> Bool) -> MultiSeqDict k v -> MultiSeqDict k v +filter fn (MultiSeqDict d) = + SeqDict.toList d + |> List.filterMap + (\( key, values_ ) -> + values_ + |> SeqSet.filter (fn key) + |> normalizeSet + |> Maybe.map (Tuple.pair key) + ) + |> fromList + + +{-| Partition a dictionary according to some test. The first dictionary +contains all key-value pairs which passed the test, and the second contains +the pairs that did not. +-} +partition : (k -> SeqSet v -> Bool) -> MultiSeqDict k v -> ( MultiSeqDict k v, MultiSeqDict k v ) +partition fn (MultiSeqDict d) = + let + ( true, false ) = + SeqDict.partition fn d + in + ( MultiSeqDict true + , MultiSeqDict false + ) + + +{-| Combine two dictionaries. If there is a collision, preference is given +to the first dictionary. +-} +union : MultiSeqDict k v -> MultiSeqDict k v -> MultiSeqDict k v +union (MultiSeqDict left) (MultiSeqDict right) = + MultiSeqDict <| SeqDict.union left right + + +{-| Keep a key-value pair when its key appears in the second dictionary. +Preference is given to values in the first dictionary. +-} +intersect : MultiSeqDict k v -> MultiSeqDict k v -> MultiSeqDict k v +intersect (MultiSeqDict left) (MultiSeqDict right) = + MultiSeqDict <| SeqDict.intersect left right + + +{-| Keep a key-value pair when its key does not appear in the second dictionary. +-} +diff : MultiSeqDict k v -> MultiSeqDict k v -> MultiSeqDict k v +diff (MultiSeqDict left) (MultiSeqDict right) = + MultiSeqDict <| SeqDict.diff left right + + +{-| The most general way of combining two dictionaries. You provide three +accumulators for when a given key appears: + +1. Only in the left dictionary. +2. In both dictionaries. +3. Only in the right dictionary. + +You then traverse all the keys from lowest to highest, building up whatever +you want. + +-} +merge : + (k -> SeqSet v1 -> acc -> acc) + -> (k -> SeqSet v1 -> SeqSet v2 -> acc -> acc) + -> (k -> SeqSet v2 -> acc -> acc) + -> MultiSeqDict k v1 + -> MultiSeqDict k v2 + -> acc + -> acc +merge fnLeft fnBoth fnRight (MultiSeqDict left) (MultiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left right zero + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +encodeMultiSeqDict : (key -> Lamdera.Wire3.Encoder) -> (value -> Lamdera.Wire3.Encoder) -> MultiSeqDict key value -> Lamdera.Wire3.Encoder +encodeMultiSeqDict encKey encValue d = + Lamdera.Wire3.encodeList (Lamdera.Wire3.encodePair encKey (SeqSet.encodeSet encValue)) (toList d) + + +{-| The Lamdera compiler relies on this function, it is not intended to be used directly. Vendor this function in your own codebase if you want to use it, as the encoding can change without notice. +-} +decodeMultiSeqDict : Lamdera.Wire3.Decoder k -> Lamdera.Wire3.Decoder value -> Lamdera.Wire3.Decoder (MultiSeqDict k value) +decodeMultiSeqDict decKey decValue = + Lamdera.Wire3.decodeList (Lamdera.Wire3.decodePair decKey (SeqSet.decodeSet decValue)) |> Bytes.Decode.map fromList diff --git a/tests/AllTests.elm b/tests/AllTests.elm new file mode 100644 index 0000000..ab14cc9 --- /dev/null +++ b/tests/AllTests.elm @@ -0,0 +1,23 @@ +module AllTests exposing (main) + +import BiSeqDictTests +import MultiBiSeqDictTests +import MultiSeqDictTests +import Test exposing (Test, describe) +import Test.Runner.Html +import Tests + + +suite : Test +suite = + describe "All Tests" + [ Tests.tests + , BiSeqDictTests.tests + , MultiSeqDictTests.tests + , MultiBiSeqDictTests.tests + ] + + +main : Test.Runner.Html.TestProgram +main = + Test.Runner.Html.run suite diff --git a/tests/BiSeqDictTests.elm b/tests/BiSeqDictTests.elm new file mode 100644 index 0000000..6990b7a --- /dev/null +++ b/tests/BiSeqDictTests.elm @@ -0,0 +1,423 @@ +module BiSeqDictTests exposing (tests) + +import BiSeqDict exposing (BiSeqDict) +import Expect +import Fuzz exposing (Fuzzer, int, list, tuple) +import SeqSet exposing (SeqSet) +import Test exposing (..) + + +{-| Non-comparable custom type to prove no comparable constraint +-} +type CustomType + = Foo + | Bar + | Baz + + +type alias CustomRecord = + { name : String + , value : Int + } + + +animals : BiSeqDict String String +animals = + BiSeqDict.fromList [ ( "Tom", "cat" ), ( "Jerry", "mouse" ), ( "Spike", "cat" ) ] + + +fuzzPairs : Fuzzer (List ( Int, Int )) +fuzzPairs = + list (tuple ( int, int )) + + +tests : Test +tests = + describe "BiSeqDict" + [ buildTests + , queryTests + , transformTests + , combineTests + , reverseTests + , customTypeTests + , fuzzTests + ] + + +buildTests : Test +buildTests = + describe "Build Tests" + [ test "empty" <| + \() -> Expect.equal (BiSeqDict.fromList []) BiSeqDict.empty + , test "singleton" <| + \() -> Expect.equal (BiSeqDict.fromList [ ( "k", "v" ) ]) (BiSeqDict.singleton "k" "v") + , test "insert" <| + \() -> Expect.equal (BiSeqDict.fromList [ ( "k", "v" ) ]) (BiSeqDict.insert "k" "v" BiSeqDict.empty) + , test "insert replace" <| + \() -> Expect.equal (BiSeqDict.fromList [ ( "k", "vv" ) ]) (BiSeqDict.insert "k" "vv" (BiSeqDict.singleton "k" "v")) + , test "insert multiple keys same value" <| + \() -> + let + dict = + BiSeqDict.empty + |> BiSeqDict.insert "k1" "v" + |> BiSeqDict.insert "k2" "v" + in + Expect.equal (BiSeqDict.size dict) 2 + , test "remove" <| + \() -> + BiSeqDict.singleton "k" "v" + |> BiSeqDict.remove "k" + |> BiSeqDict.toList + |> Expect.equal [] + , test "remove not found" <| + \() -> Expect.equal (BiSeqDict.singleton "k" "v") (BiSeqDict.remove "kk" (BiSeqDict.singleton "k" "v")) + , test "fromList excludes duplicates" <| + \() -> Expect.equal (BiSeqDict.singleton 1 1) (BiSeqDict.fromList [ ( 1, 1 ), ( 1, 1 ) ]) + , test "size" <| + \() -> + BiSeqDict.empty + |> BiSeqDict.insert "k1" "v" + |> BiSeqDict.insert "k2" "v" + |> BiSeqDict.insert "k1" "y" + |> BiSeqDict.remove "k2" + |> BiSeqDict.size + |> Expect.equal 1 + ] + + +queryTests : Test +queryTests = + describe "Query Tests" + [ test "member 1" <| + \() -> Expect.equal True (BiSeqDict.member "Tom" animals) + , test "member 2" <| + \() -> Expect.equal False (BiSeqDict.member "NotThere" animals) + , test "get 1" <| + \() -> Expect.equal (Just "cat") (BiSeqDict.get "Tom" animals) + , test "get 2" <| + \() -> Expect.equal Nothing (BiSeqDict.get "NotThere" animals) + , test "size of empty dictionary" <| + \() -> Expect.equal 0 (BiSeqDict.size BiSeqDict.empty) + , test "size of example dictionary" <| + \() -> Expect.equal 3 (BiSeqDict.size animals) + , test "isEmpty empty" <| + \() -> Expect.equal True (BiSeqDict.isEmpty BiSeqDict.empty) + , test "isEmpty non-empty" <| + \() -> Expect.equal False (BiSeqDict.isEmpty animals) + , test "keys" <| + \() -> + BiSeqDict.keys animals + |> Expect.equal [ "Tom", "Jerry", "Spike" ] + , test "values" <| + \() -> + BiSeqDict.values animals + |> Expect.equal [ "cat", "mouse", "cat" ] + , test "toList" <| + \() -> + BiSeqDict.toList animals + |> Expect.equal [ ( "Tom", "cat" ), ( "Jerry", "mouse" ), ( "Spike", "cat" ) ] + ] + + +transformTests : Test +transformTests = + describe "Transform Tests" + [ test "map" <| + \() -> + BiSeqDict.map (\k v -> v ++ "!") animals + |> BiSeqDict.get "Tom" + |> Expect.equal (Just "cat!") + , test "foldl" <| + \() -> + BiSeqDict.foldl (\k v acc -> acc ++ k) "" animals + |> Expect.equal "TomJerrySpike" + , test "foldr" <| + \() -> + BiSeqDict.foldr (\k v acc -> acc ++ k) "" animals + |> Expect.equal "SpikJerryTom" + , test "filter" <| + \() -> + BiSeqDict.filter (\k v -> v == "cat") animals + |> BiSeqDict.size + |> Expect.equal 2 + , test "partition" <| + \() -> + let + ( cats, others ) = + BiSeqDict.partition (\k v -> v == "cat") animals + in + Expect.equal ( BiSeqDict.size cats, BiSeqDict.size others ) ( 2, 1 ) + ] + + +combineTests : Test +combineTests = + describe "Combine Tests" + [ test "union" <| + \() -> + let + d1 = + BiSeqDict.singleton "a" "1" + + d2 = + BiSeqDict.singleton "b" "2" + + result = + BiSeqDict.union d1 d2 + in + Expect.equal (BiSeqDict.size result) 2 + , test "union collision" <| + \() -> + let + d1 = + BiSeqDict.singleton "a" "1" + + d2 = + BiSeqDict.singleton "a" "2" + + result = + BiSeqDict.union d1 d2 + in + Expect.equal (BiSeqDict.get "a" result) (Just "1") + , test "intersect" <| + \() -> + let + d1 = + BiSeqDict.fromList [ ( "a", "1" ), ( "b", "2" ) ] + + d2 = + BiSeqDict.singleton "a" "1" + + result = + BiSeqDict.intersect d1 d2 + in + Expect.equal (BiSeqDict.size result) 1 + , test "diff" <| + \() -> + let + d1 = + BiSeqDict.fromList [ ( "a", "1" ), ( "b", "2" ) ] + + d2 = + BiSeqDict.singleton "a" "1" + + result = + BiSeqDict.diff d1 d2 + in + Expect.equal (BiSeqDict.toList result) [ ( "b", "2" ) ] + ] + + +reverseTests : Test +reverseTests = + describe "Reverse Lookup Tests" + [ test "getReverse single key" <| + \() -> + let + dict = + BiSeqDict.singleton "Tom" "cat" + + result = + BiSeqDict.getKeys "cat" dict + in + Expect.equal (SeqSet.size result) 1 + , test "getReverse multiple keys" <| + \() -> + let + result = + BiSeqDict.getKeys "cat" animals + in + Expect.equal (SeqSet.size result) 2 + , test "getReverse not found" <| + \() -> + let + result = + BiSeqDict.getKeys "dog" animals + in + Expect.equal (SeqSet.size result) 0 + , test "getReverse after insert" <| + \() -> + let + dict = + BiSeqDict.empty + |> BiSeqDict.insert "k1" "v" + |> BiSeqDict.insert "k2" "v" + |> BiSeqDict.insert "k3" "v" + + result = + BiSeqDict.getKeys "v" dict + in + Expect.equal (SeqSet.size result) 3 + , test "getReverse after remove" <| + \() -> + let + dict = + BiSeqDict.fromList [ ( "Tom", "cat" ), ( "Spike", "cat" ) ] + |> BiSeqDict.remove "Tom" + + result = + BiSeqDict.getKeys "cat" dict + in + Expect.equal (SeqSet.size result) 1 + , test "reverse index consistency after replace" <| + \() -> + let + dict = + BiSeqDict.singleton "k" "old" + |> BiSeqDict.insert "k" "new" + + oldResult = + BiSeqDict.getKeys "old" dict + + newResult = + BiSeqDict.getKeys "new" dict + in + Expect.equal ( SeqSet.size oldResult, SeqSet.size newResult ) ( 0, 1 ) + ] + + +customTypeTests : Test +customTypeTests = + describe "Custom Type Tests (no comparable constraint)" + [ test "can create and insert custom types" <| + \() -> + let + dict = + BiSeqDict.empty + |> BiSeqDict.insert Foo "hello" + |> BiSeqDict.insert Bar "world" + |> BiSeqDict.insert Baz "hello" + in + Expect.equal (BiSeqDict.size dict) 3 + , test "getReverse works with custom types" <| + \() -> + let + dict = + BiSeqDict.empty + |> BiSeqDict.insert Foo "hello" + |> BiSeqDict.insert Bar "world" + |> BiSeqDict.insert Baz "hello" + + result = + BiSeqDict.getKeys "hello" dict + in + Expect.equal (SeqSet.size result) 2 + , test "can use custom records as values" <| + \() -> + let + dict = + BiSeqDict.empty + |> BiSeqDict.insert "alice" { name = "Alice", value = 1 } + |> BiSeqDict.insert "bob" { name = "Bob", value = 2 } + |> BiSeqDict.insert "charlie" { name = "Alice", value = 1 } + + record = + { name = "Alice", value = 1 } + + reverseKeys = + BiSeqDict.getKeys record dict + in + Expect.equal (SeqSet.size reverseKeys) 2 + ] + + +fuzzTests : Test +fuzzTests = + describe "Fuzz Tests" + [ fuzz2 fuzzPairs int "get works" <| + \pairs num -> + let + dict = + BiSeqDict.fromList pairs + + result = + BiSeqDict.get num dict + + expected = + pairs + |> List.filter (\( k, _ ) -> k == num) + |> List.head + |> Maybe.map Tuple.second + in + Expect.equal result expected + , fuzz fuzzPairs "fromList and toList roundtrip" <| + \pairs -> + let + dict = + BiSeqDict.fromList pairs + + uniquePairs = + pairs + |> List.foldl + (\( k, v ) acc -> + if List.any (\( k2, _ ) -> k2 == k) acc then + acc + + else + acc ++ [ ( k, v ) ] + ) + [] + in + BiSeqDict.toList dict + |> Expect.equal uniquePairs + , fuzz2 fuzzPairs int "insert works" <| + \pairs num -> + let + dict = + BiSeqDict.fromList pairs + |> BiSeqDict.insert num num + in + BiSeqDict.get num dict + |> Expect.equal (Just num) + , fuzz2 fuzzPairs int "remove works" <| + \pairs num -> + let + dict = + BiSeqDict.fromList pairs + |> BiSeqDict.remove num + in + BiSeqDict.get num dict + |> Expect.equal Nothing + , fuzz fuzzPairs "reverse index is consistent" <| + \pairs -> + let + dict = + BiSeqDict.fromList pairs + + checkConsistency ( k, v ) = + BiSeqDict.getKeys v dict + |> SeqSet.member k + in + BiSeqDict.toList dict + |> List.all checkConsistency + |> Expect.equal True + , fuzz2 fuzzPairs fuzzPairs "union works" <| + \pairs1 pairs2 -> + let + d1 = + BiSeqDict.fromList pairs1 + + d2 = + BiSeqDict.fromList pairs2 + + result = + BiSeqDict.union d1 d2 + + expectedSize = + (pairs1 ++ pairs2) + |> List.map Tuple.first + |> List.foldl + (\k acc -> + if List.member k acc then + acc + + else + k :: acc + ) + [] + |> List.length + in + BiSeqDict.size result + |> Expect.equal expectedSize + ] diff --git a/tests/MultiBiSeqDictTests.elm b/tests/MultiBiSeqDictTests.elm new file mode 100644 index 0000000..f60651f --- /dev/null +++ b/tests/MultiBiSeqDictTests.elm @@ -0,0 +1,912 @@ +module MultiBiSeqDictTests exposing (tests) + +import Expect +import Fuzz exposing (Fuzzer, int, list, tuple) +import MultiBiSeqDict exposing (MultiBiSeqDict) +import SeqSet exposing (SeqSet) +import Test exposing (..) + + +{-| Non-comparable custom type to prove no comparable constraint +-} +type CustomType + = Foo + | Bar + | Baz + + +type alias CustomRecord = + { name : String + , value : Int + } + + +{-| Example dictionary: many-to-many relationships +-} +chatDocuments : MultiBiSeqDict String String +chatDocuments = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "chat1" "doc1" + |> MultiBiSeqDict.insert "chat1" "doc2" + |> MultiBiSeqDict.insert "chat2" "doc1" + |> MultiBiSeqDict.insert "chat2" "doc3" + |> MultiBiSeqDict.insert "chat3" "doc2" + + +fuzzPairs : Fuzzer (List ( Int, Int )) +fuzzPairs = + list (tuple ( int, int )) + + +tests : Test +tests = + describe "MultiBiSeqDict" + [ buildTests + , queryTests + , reverseTests + , listsTests + , transformTests + , combineTests + , customTypeTests + , fuzzTests + ] + + +buildTests : Test +buildTests = + describe "Build Tests" + [ test "empty" <| + \() -> + let + dict = + MultiBiSeqDict.empty + in + Expect.equal (MultiBiSeqDict.size dict) 0 + , test "singleton" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "v" + + values = + MultiBiSeqDict.getAll "k" dict + + keys = + MultiBiSeqDict.getKeys "v" dict + in + Expect.equal ( SeqSet.size values, SeqSet.size keys ) ( 1, 1 ) + , test "insert" <| + \() -> + let + dict = + MultiBiSeqDict.insert "k" "v" MultiBiSeqDict.empty + + values = + MultiBiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "insert multiple values same key" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k" "v1" + |> MultiBiSeqDict.insert "k" "v2" + |> MultiBiSeqDict.insert "k" "v3" + + values = + MultiBiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 3 + , test "insert multiple keys same value" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v" + |> MultiBiSeqDict.insert "k2" "v" + |> MultiBiSeqDict.insert "k3" "v" + + keys = + MultiBiSeqDict.getKeys "v" dict + in + Expect.equal (SeqSet.size keys) 3 + , test "insert duplicate value" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k" "v" + |> MultiBiSeqDict.insert "k" "v" + + values = + MultiBiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "update" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "v1" + |> MultiBiSeqDict.update "k" (SeqSet.insert "v2") + + values = + MultiBiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 2 + , test "update to empty removes key" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "v" + |> MultiBiSeqDict.update "k" (\_ -> SeqSet.empty) + in + Expect.equal (MultiBiSeqDict.member "k" dict) False + , test "update maintains reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "v1" + |> MultiBiSeqDict.update "k" (SeqSet.insert "v2") + + keysForV2 = + MultiBiSeqDict.getKeys "v2" dict + in + Expect.equal (SeqSet.member "k" keysForV2) True + , test "remove single value" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k" "v1" + |> MultiBiSeqDict.insert "k" "v2" + |> MultiBiSeqDict.remove "k" "v1" + + values = + MultiBiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "remove updates reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v" + |> MultiBiSeqDict.insert "k2" "v" + |> MultiBiSeqDict.remove "k1" "v" + + keys = + MultiBiSeqDict.getKeys "v" dict + in + Expect.equal (SeqSet.size keys) 1 + , test "remove last value removes key" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "v" + |> MultiBiSeqDict.remove "k" "v" + in + Expect.equal (MultiBiSeqDict.member "k" dict) False + , test "remove not found" <| + \() -> + let + original = + MultiBiSeqDict.singleton "k" "v" + + modified = + MultiBiSeqDict.remove "k" "notfound" original + in + Expect.equal original modified + , test "removeAll" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k" "v1" + |> MultiBiSeqDict.insert "k" "v2" + |> MultiBiSeqDict.insert "k" "v3" + |> MultiBiSeqDict.removeAll "k" + in + Expect.equal (MultiBiSeqDict.member "k" dict) False + , test "removeAll updates reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k" "v1" + |> MultiBiSeqDict.insert "k" "v2" + |> MultiBiSeqDict.insert "j" "v1" + |> MultiBiSeqDict.removeAll "k" + + keysForV1 = + MultiBiSeqDict.getKeys "v1" dict + + keysForV2 = + MultiBiSeqDict.getKeys "v2" dict + in + Expect.equal ( SeqSet.size keysForV1, SeqSet.size keysForV2 ) ( 1, 0 ) + , test "removeAll not found" <| + \() -> + let + original = + MultiBiSeqDict.singleton "k" "v" + + modified = + MultiBiSeqDict.removeAll "notfound" original + in + Expect.equal original modified + , test "size counts all key-value pairs" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v1" + |> MultiBiSeqDict.insert "k1" "v2" + |> MultiBiSeqDict.insert "k2" "v3" + |> MultiBiSeqDict.remove "k1" "v1" + in + Expect.equal (MultiBiSeqDict.size dict) 2 + ] + + +queryTests : Test +queryTests = + describe "Query Tests" + [ test "member found" <| + \() -> Expect.equal True (MultiBiSeqDict.member "chat1" chatDocuments) + , test "member not found" <| + \() -> Expect.equal False (MultiBiSeqDict.member "chat99" chatDocuments) + , test "get returns all values" <| + \() -> + let + docs = + MultiBiSeqDict.getAll "chat1" chatDocuments + in + Expect.equal (SeqSet.size docs) 2 + , test "get not found returns empty" <| + \() -> + let + docs = + MultiBiSeqDict.getAll "chat99" chatDocuments + in + Expect.equal (SeqSet.isEmpty docs) True + , test "size of empty dictionary" <| + \() -> Expect.equal 0 (MultiBiSeqDict.size MultiBiSeqDict.empty) + , test "size of example dictionary" <| + \() -> Expect.equal 5 (MultiBiSeqDict.size chatDocuments) + , test "isEmpty empty" <| + \() -> Expect.equal True (MultiBiSeqDict.isEmpty MultiBiSeqDict.empty) + , test "isEmpty non-empty" <| + \() -> Expect.equal False (MultiBiSeqDict.isEmpty chatDocuments) + ] + + +reverseTests : Test +reverseTests = + describe "Reverse Lookup Tests" + [ test "getReverse returns all keys" <| + \() -> + let + chats = + MultiBiSeqDict.getKeys "doc1" chatDocuments + in + Expect.equal (SeqSet.size chats) 2 + , test "getReverse not found returns empty" <| + \() -> + let + chats = + MultiBiSeqDict.getKeys "doc99" chatDocuments + in + Expect.equal (SeqSet.isEmpty chats) True + , test "getReverse after insert" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v" + |> MultiBiSeqDict.insert "k2" "v" + |> MultiBiSeqDict.insert "k3" "v" + + keys = + MultiBiSeqDict.getKeys "v" dict + in + Expect.equal (SeqSet.size keys) 3 + , test "getReverse after remove" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v" + |> MultiBiSeqDict.insert "k2" "v" + |> MultiBiSeqDict.remove "k1" "v" + + keys = + MultiBiSeqDict.getKeys "v" dict + in + Expect.equal (SeqSet.size keys) 1 + , test "reverse index consistency after replace" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "k" "old" + |> MultiBiSeqDict.update "k" (\_ -> SeqSet.singleton "new") + + oldKeys = + MultiBiSeqDict.getKeys "old" dict + + newKeys = + MultiBiSeqDict.getKeys "new" dict + in + Expect.equal ( SeqSet.size oldKeys, SeqSet.size newKeys ) ( 0, 1 ) + , test "reverse consistency with multiple updates" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v1" + |> MultiBiSeqDict.insert "k1" "v2" + |> MultiBiSeqDict.insert "k2" "v1" + |> MultiBiSeqDict.remove "k1" "v1" + + keysForV1 = + MultiBiSeqDict.getKeys "v1" dict + + keysForV2 = + MultiBiSeqDict.getKeys "v2" dict + in + Expect.equal ( SeqSet.size keysForV1, SeqSet.size keysForV2 ) ( 1, 1 ) + , test "uniqueValues returns all unique values" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "k1" "v1" + |> MultiBiSeqDict.insert "k1" "v2" + |> MultiBiSeqDict.insert "k2" "v1" + + uniques = + MultiBiSeqDict.uniqueValues dict + in + Expect.equal (List.length uniques) 2 + , test "uniqueValuesCount" <| + \() -> + Expect.equal (MultiBiSeqDict.uniqueValuesCount chatDocuments) 3 + ] + + +listsTests : Test +listsTests = + describe "Lists Tests" + [ test "keys" <| + \() -> + MultiBiSeqDict.keys chatDocuments + |> Expect.equal [ "chat1", "chat2", "chat3" ] + , test "values preserves all values" <| + \() -> + let + vals = + MultiBiSeqDict.values chatDocuments + in + Expect.equal (List.length vals) 5 + , test "values order" <| + \() -> + MultiBiSeqDict.values chatDocuments + |> Expect.equal [ "doc1", "doc2", "doc1", "doc3", "doc2" ] + , test "toList" <| + \() -> + let + list = + MultiBiSeqDict.toList chatDocuments + + keys = + List.map Tuple.first list + in + Expect.equal keys [ "chat1", "chat2", "chat3" ] + , test "toReverseList" <| + \() -> + let + list = + MultiBiSeqDict.toReverseList chatDocuments + + values = + List.map Tuple.first list + in + Expect.equal (List.length values) 3 + , test "fromList" <| + \() -> + let + dict = + MultiBiSeqDict.fromList + [ ( "k1", SeqSet.fromList [ 1, 2 ] ) + , ( "k2", SeqSet.fromList [ 3 ] ) + ] + + keysFor3 = + MultiBiSeqDict.getKeys 3 dict + in + Expect.equal (SeqSet.size keysFor3) 1 + , test "toList/fromList roundtrip" <| + \() -> + let + roundtrip = + chatDocuments + |> MultiBiSeqDict.toList + |> MultiBiSeqDict.fromList + in + Expect.equal chatDocuments roundtrip + ] + + +transformTests : Test +transformTests = + describe "Transform Tests" + [ test "map" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + + mapped = + MultiBiSeqDict.map (\k v -> v * 2) dict + + values = + MultiBiSeqDict.getAll "a" mapped + |> SeqSet.toList + |> List.sort + in + Expect.equal values [ 2, 4 ] + , test "map maintains reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 1 + + mapped = + MultiBiSeqDict.map (\k v -> v * 10) dict + + keysFor10 = + MultiBiSeqDict.getKeys 10 mapped + in + Expect.equal (SeqSet.size keysFor10) 2 + , test "foldl" <| + \() -> + let + sum = + MultiBiSeqDict.foldl (\k values acc -> acc + SeqSet.size values) 0 chatDocuments + in + Expect.equal sum 5 + , test "foldr" <| + \() -> + let + keys = + MultiBiSeqDict.foldr (\k _ acc -> k :: acc) [] chatDocuments + in + Expect.equal keys [ "chat3", "chat2", "chat1" ] + , test "filter" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + |> MultiBiSeqDict.insert "b" 3 + + filtered = + MultiBiSeqDict.filter (\k v -> v > 1) dict + in + Expect.equal (MultiBiSeqDict.size filtered) 2 + , test "filter maintains reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 1 + |> MultiBiSeqDict.insert "c" 2 + + filtered = + MultiBiSeqDict.filter (\k v -> v > 1) dict + + keysFor1 = + MultiBiSeqDict.getKeys 1 filtered + in + Expect.equal (SeqSet.size keysFor1) 0 + , test "filter removes empty keys" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + + filtered = + MultiBiSeqDict.filter (\k v -> v > 5) dict + in + Expect.equal (MultiBiSeqDict.isEmpty filtered) True + , test "partition" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + |> MultiBiSeqDict.insert "b" 3 + + ( hasTwo, noTwo ) = + MultiBiSeqDict.partition (\k values -> SeqSet.size values == 2) dict + in + Expect.equal ( MultiBiSeqDict.size hasTwo, MultiBiSeqDict.size noTwo ) ( 2, 1 ) + , test "partition maintains reverse index" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + |> MultiBiSeqDict.insert "b" 1 + + ( hasTwo, noTwo ) = + MultiBiSeqDict.partition (\k values -> SeqSet.size values == 2) dict + + keysFor1InHasTwo = + MultiBiSeqDict.getKeys 1 hasTwo + + keysFor1InNoTwo = + MultiBiSeqDict.getKeys 1 noTwo + in + Expect.equal ( SeqSet.size keysFor1InHasTwo, SeqSet.size keysFor1InNoTwo ) ( 1, 1 ) + ] + + +combineTests : Test +combineTests = + describe "Combine Tests" + [ test "union" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "b" 2 + + result = + MultiBiSeqDict.union d1 d2 + in + Expect.equal (MultiBiSeqDict.size result) 2 + , test "union prefers left" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 2 + + result = + MultiBiSeqDict.union d1 d2 + + values = + MultiBiSeqDict.getAll "a" result + in + Expect.equal (SeqSet.size values) 1 + , test "union maintains reverse index" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "b" 2 + + result = + MultiBiSeqDict.union d1 d2 + + keysFor1 = + MultiBiSeqDict.getKeys 1 result + + keysFor2 = + MultiBiSeqDict.getKeys 2 result + in + Expect.equal ( SeqSet.size keysFor1, SeqSet.size keysFor2 ) ( 1, 1 ) + , test "intersect" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 2 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 3 + + result = + MultiBiSeqDict.intersect d1 d2 + in + Expect.equal (MultiBiSeqDict.size result) 1 + , test "intersect maintains reverse index" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "a" 2 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 3 + + result = + MultiBiSeqDict.intersect d1 d2 + + keysFor1 = + MultiBiSeqDict.getKeys 1 result + in + Expect.equal (SeqSet.size keysFor1) 1 + , test "diff" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 2 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 3 + + result = + MultiBiSeqDict.diff d1 d2 + in + Expect.equal (MultiBiSeqDict.keys result) [ "b" ] + , test "diff maintains reverse index" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 2 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 3 + + result = + MultiBiSeqDict.diff d1 d2 + + keysFor1 = + MultiBiSeqDict.getKeys 1 result + + keysFor2 = + MultiBiSeqDict.getKeys 2 result + in + Expect.equal ( SeqSet.size keysFor1, SeqSet.size keysFor2 ) ( 0, 1 ) + , test "merge" <| + \() -> + let + d1 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "a" 1 + |> MultiBiSeqDict.insert "b" 2 + + d2 = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "b" 3 + |> MultiBiSeqDict.insert "c" 4 + + result = + MultiBiSeqDict.merge + (\k _ acc -> acc + 1) + (\k _ _ acc -> acc + 10) + (\k _ acc -> acc + 100) + d1 + d2 + 0 + in + Expect.equal result 111 + ] + + +customTypeTests : Test +customTypeTests = + describe "Custom Type Tests (no comparable constraint)" + [ test "can create and insert custom types" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo 1 + |> MultiBiSeqDict.insert Foo 2 + |> MultiBiSeqDict.insert Bar 2 + + fooValues = + MultiBiSeqDict.getAll Foo dict + + keysFor2 = + MultiBiSeqDict.getKeys 2 dict + in + Expect.equal ( SeqSet.size fooValues, SeqSet.size keysFor2 ) ( 2, 2 ) + , test "getReverse works with custom types" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo "hello" + |> MultiBiSeqDict.insert Bar "world" + |> MultiBiSeqDict.insert Baz "hello" + + keys = + MultiBiSeqDict.getKeys "hello" dict + in + Expect.equal (SeqSet.size keys) 2 + , test "can use custom records as values" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert "alice" { name = "Alice", value = 1 } + |> MultiBiSeqDict.insert "alice" { name = "Alice", value = 2 } + |> MultiBiSeqDict.insert "bob" { name = "Alice", value = 1 } + + record = + { name = "Alice", value = 1 } + + keys = + MultiBiSeqDict.getKeys record dict + in + Expect.equal (SeqSet.size keys) 2 + ] + + +fuzzTests : Test +fuzzTests = + describe "Fuzz Tests" + [ fuzz2 fuzzPairs int "get works" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + + result = + MultiBiSeqDict.getAll num dict + + expected = + pairs + |> List.filter (\( k, _ ) -> k == num) + |> List.map Tuple.second + |> SeqSet.fromList + in + Expect.equal result expected + , fuzz2 fuzzPairs int "getReverse works" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + + result = + MultiBiSeqDict.getKeys num dict + + expected = + pairs + |> List.filter (\( _, v ) -> v == num) + |> List.map Tuple.first + |> SeqSet.fromList + in + Expect.equal result expected + , fuzz fuzzPairs "reverse index is consistent" <| + \pairs -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + + checkConsistency ( k, values ) = + SeqSet.foldl + (\v acc -> + acc && SeqSet.member k (MultiBiSeqDict.getKeys v dict) + ) + True + values + in + MultiBiSeqDict.toList dict + |> List.all checkConsistency + |> Expect.equal True + , fuzz2 fuzzPairs int "insert works" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + |> MultiBiSeqDict.insert num num + + values = + MultiBiSeqDict.getAll num dict + in + Expect.equal (SeqSet.member num values) True + , fuzz2 fuzzPairs int "remove works" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + |> MultiBiSeqDict.remove num num + + values = + MultiBiSeqDict.getAll num dict + in + Expect.equal (SeqSet.member num values) False + , fuzz2 fuzzPairs int "remove maintains reverse index" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + |> MultiBiSeqDict.remove num num + + keys = + MultiBiSeqDict.getKeys num dict + in + Expect.equal (SeqSet.member num keys) False + , fuzz2 fuzzPairs int "removeAll works" <| + \pairs num -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + |> MultiBiSeqDict.removeAll num + in + Expect.equal (MultiBiSeqDict.member num dict) False + , fuzz2 fuzzPairs fuzzPairs "union works" <| + \pairs1 pairs2 -> + let + d1 = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs1 + + d2 = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs2 + + result = + MultiBiSeqDict.union d1 d2 + + unionKeys = + (pairs1 ++ pairs2) + |> List.map Tuple.first + |> SeqSet.fromList + |> SeqSet.size + + resultKeys = + MultiBiSeqDict.keys result |> List.length + in + Expect.equal unionKeys resultKeys + , fuzz fuzzPairs "size counts all pairs" <| + \pairs -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + + uniquePairs = + pairs + |> SeqSet.fromList + |> SeqSet.size + in + Expect.equal (MultiBiSeqDict.size dict) uniquePairs + , fuzz fuzzPairs "uniqueValuesCount is accurate" <| + \pairs -> + let + dict = + List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs + + uniqueValues = + pairs + |> List.map Tuple.second + |> SeqSet.fromList + |> SeqSet.size + in + Expect.equal (MultiBiSeqDict.uniqueValuesCount dict) uniqueValues + ] diff --git a/tests/MultiSeqDictTests.elm b/tests/MultiSeqDictTests.elm new file mode 100644 index 0000000..abe926b --- /dev/null +++ b/tests/MultiSeqDictTests.elm @@ -0,0 +1,597 @@ +module MultiSeqDictTests exposing (tests) + +import Expect +import Fuzz exposing (Fuzzer, int, list, tuple) +import MultiSeqDict exposing (MultiSeqDict) +import SeqSet exposing (SeqSet) +import Test exposing (..) + + +{-| Non-comparable custom type to prove no comparable constraint +-} +type CustomType + = Foo + | Bar + | Baz + + +type alias CustomRecord = + { name : String + , value : Int + } + + +{-| Example dictionary: one key can have multiple values +-} +properties : MultiSeqDict String String +properties = + MultiSeqDict.empty + |> MultiSeqDict.insert "colors" "red" + |> MultiSeqDict.insert "colors" "blue" + |> MultiSeqDict.insert "numbers" "1" + |> MultiSeqDict.insert "numbers" "2" + |> MultiSeqDict.insert "letters" "a" + + +fuzzPairs : Fuzzer (List ( Int, Int )) +fuzzPairs = + list (tuple ( int, int )) + + +tests : Test +tests = + describe "MultiSeqDict" + [ buildTests + , queryTests + , listsTests + , transformTests + , combineTests + , customTypeTests + , fuzzTests + ] + + +buildTests : Test +buildTests = + describe "Build Tests" + [ test "empty" <| + \() -> Expect.equal (MultiSeqDict.fromFlatList []) MultiSeqDict.empty + , test "singleton" <| + \() -> + let + dict = + MultiSeqDict.singleton "k" "v" + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "insert" <| + \() -> + let + dict = + MultiSeqDict.insert "k" "v" MultiSeqDict.empty + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "insert multiple values same key" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "k" "v1" + |> MultiSeqDict.insert "k" "v2" + |> MultiSeqDict.insert "k" "v3" + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 3 + , test "insert duplicate value" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "k" "v" + |> MultiSeqDict.insert "k" "v" + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "update" <| + \() -> + let + dict = + MultiSeqDict.singleton "k" "v1" + |> MultiSeqDict.update "k" (SeqSet.insert "v2") + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 2 + , test "update to empty removes key" <| + \() -> + let + dict = + MultiSeqDict.singleton "k" "v" + |> MultiSeqDict.update "k" (\_ -> SeqSet.empty) + in + Expect.equal (MultiSeqDict.member "k" dict) False + , test "remove single value" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "k" "v1" + |> MultiSeqDict.insert "k" "v2" + |> MultiSeqDict.remove "k" "v1" + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "remove last value removes key" <| + \() -> + let + dict = + MultiSeqDict.singleton "k" "v" + |> MultiSeqDict.remove "k" "v" + in + Expect.equal (MultiSeqDict.member "k" dict) False + , test "remove not found" <| + \() -> + let + original = + MultiSeqDict.singleton "k" "v" + + modified = + MultiSeqDict.remove "k" "notfound" original + in + Expect.equal original modified + , test "removeAll" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "k" "v1" + |> MultiSeqDict.insert "k" "v2" + |> MultiSeqDict.insert "k" "v3" + |> MultiSeqDict.removeAll "k" + in + Expect.equal (MultiSeqDict.member "k" dict) False + , test "removeAll not found" <| + \() -> + let + original = + MultiSeqDict.singleton "k" "v" + + modified = + MultiSeqDict.removeAll "notfound" original + in + Expect.equal original modified + , test "size counts all key-value pairs" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "k1" "v1" + |> MultiSeqDict.insert "k1" "v2" + |> MultiSeqDict.insert "k2" "v3" + |> MultiSeqDict.remove "k1" "v1" + in + Expect.equal (MultiSeqDict.size dict) 2 + ] + + +queryTests : Test +queryTests = + describe "Query Tests" + [ test "member found" <| + \() -> Expect.equal True (MultiSeqDict.member "colors" properties) + , test "member not found" <| + \() -> Expect.equal False (MultiSeqDict.member "shapes" properties) + , test "get returns all values" <| + \() -> + let + colors = + MultiSeqDict.getAll "colors" properties + in + Expect.equal (SeqSet.size colors) 2 + , test "get not found returns empty" <| + \() -> + let + shapes = + MultiSeqDict.getAll "shapes" properties + in + Expect.equal (SeqSet.isEmpty shapes) True + , test "size of empty dictionary" <| + \() -> Expect.equal 0 (MultiSeqDict.size MultiSeqDict.empty) + , test "size of example dictionary" <| + \() -> Expect.equal 5 (MultiSeqDict.size properties) + , test "isEmpty empty" <| + \() -> Expect.equal True (MultiSeqDict.isEmpty MultiSeqDict.empty) + , test "isEmpty non-empty" <| + \() -> Expect.equal False (MultiSeqDict.isEmpty properties) + ] + + +listsTests : Test +listsTests = + describe "Lists Tests" + [ test "keys" <| + \() -> + MultiSeqDict.keys properties + |> Expect.equal [ "colors", "numbers", "letters" ] + , test "values preserves all values" <| + \() -> + let + vals = + MultiSeqDict.values properties + in + Expect.equal (List.length vals) 5 + , test "values order" <| + \() -> + MultiSeqDict.values properties + |> Expect.equal [ "red", "blue", "1", "2", "a" ] + , test "toList" <| + \() -> + let + list = + MultiSeqDict.toList properties + + keys = + List.map Tuple.first list + in + Expect.equal keys [ "colors", "numbers", "letters" ] + , test "fromList" <| + \() -> + let + dict = + MultiSeqDict.fromList + [ ( "k1", SeqSet.fromList [ 1, 2 ] ) + , ( "k2", SeqSet.fromList [ 3 ] ) + ] + in + Expect.equal (MultiSeqDict.size dict) 3 + , test "fromFlatList" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList + [ ( "k", 1 ) + , ( "k", 2 ) + , ( "j", 3 ) + ] + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 2 + , test "fromFlatList excludes duplicates" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList + [ ( "k", 1 ) + , ( "k", 1 ) + ] + + values = + MultiSeqDict.getAll "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "toList/fromList roundtrip" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList + [ ( "a", 1 ) + , ( "a", 2 ) + , ( "b", 3 ) + ] + + roundtrip = + dict + |> MultiSeqDict.toList + |> MultiSeqDict.fromList + in + Expect.equal dict roundtrip + ] + + +transformTests : Test +transformTests = + describe "Transform Tests" + [ test "map" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "a", 2 ) ] + + mapped = + MultiSeqDict.map (\k v -> v * 2) dict + + values = + MultiSeqDict.getAll "a" mapped + |> SeqSet.toList + |> List.sort + in + Expect.equal values [ 2, 4 ] + , test "foldl" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "a", 2 ), ( "b", 3 ) ] + + sum = + MultiSeqDict.foldl (\k values acc -> acc + SeqSet.size values) 0 dict + in + Expect.equal sum 3 + , test "foldr" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "b", 2 ), ( "c", 3 ) ] + + keys = + MultiSeqDict.foldr (\k _ acc -> k :: acc) [] dict + in + Expect.equal keys [ "c", "b", "a" ] + , test "filter" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "a", 2 ), ( "b", 3 ) ] + + filtered = + MultiSeqDict.filter (\k v -> v > 1) dict + in + Expect.equal (MultiSeqDict.size filtered) 2 + , test "filter removes empty keys" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "a", 2 ) ] + + filtered = + MultiSeqDict.filter (\k v -> v > 5) dict + in + Expect.equal (MultiSeqDict.isEmpty filtered) True + , test "partition" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "a", 2 ), ( "b", 3 ) ] + + ( hasTwo, noTwo ) = + MultiSeqDict.partition (\k values -> SeqSet.size values == 2) dict + in + Expect.equal ( MultiSeqDict.size hasTwo, MultiSeqDict.size noTwo ) ( 2, 1 ) + ] + + +combineTests : Test +combineTests = + describe "Combine Tests" + [ test "union" <| + \() -> + let + d1 = + MultiSeqDict.fromFlatList [ ( "a", 1 ) ] + + d2 = + MultiSeqDict.fromFlatList [ ( "b", 2 ) ] + + result = + MultiSeqDict.union d1 d2 + in + Expect.equal (MultiSeqDict.size result) 2 + , test "union prefers left" <| + \() -> + let + d1 = + MultiSeqDict.fromFlatList [ ( "a", 1 ) ] + + d2 = + MultiSeqDict.fromFlatList [ ( "a", 2 ) ] + + result = + MultiSeqDict.union d1 d2 + + values = + MultiSeqDict.getAll "a" result + in + Expect.equal (SeqSet.size values) 1 + , test "intersect" <| + \() -> + let + d1 = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "b", 2 ) ] + + d2 = + MultiSeqDict.fromFlatList [ ( "a", 3 ) ] + + result = + MultiSeqDict.intersect d1 d2 + in + Expect.equal (MultiSeqDict.size result) 1 + , test "diff" <| + \() -> + let + d1 = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "b", 2 ) ] + + d2 = + MultiSeqDict.fromFlatList [ ( "a", 3 ) ] + + result = + MultiSeqDict.diff d1 d2 + in + Expect.equal (MultiSeqDict.keys result) [ "b" ] + , test "merge" <| + \() -> + let + d1 = + MultiSeqDict.fromFlatList [ ( "a", 1 ), ( "b", 2 ) ] + + d2 = + MultiSeqDict.fromFlatList [ ( "b", 3 ), ( "c", 4 ) ] + + result = + MultiSeqDict.merge + (\k _ acc -> acc + 1) + (\k _ _ acc -> acc + 10) + (\k _ acc -> acc + 100) + d1 + d2 + 0 + in + Expect.equal result 111 + ] + + +customTypeTests : Test +customTypeTests = + describe "Custom Type Tests (no comparable constraint)" + [ test "can create and insert custom types" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert Foo 1 + |> MultiSeqDict.insert Foo 2 + |> MultiSeqDict.insert Bar 3 + + fooValues = + MultiSeqDict.getAll Foo dict + in + Expect.equal (SeqSet.size fooValues) 2 + , test "multiple custom type keys" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert Foo "hello" + |> MultiSeqDict.insert Bar "world" + |> MultiSeqDict.insert Baz "hello" + in + Expect.equal (MultiSeqDict.size dict) 3 + , test "can use custom records as values" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "alice" { name = "Alice", value = 1 } + |> MultiSeqDict.insert "alice" { name = "Alice", value = 2 } + |> MultiSeqDict.insert "bob" { name = "Bob", value = 1 } + + aliceValues = + MultiSeqDict.getAll "alice" dict + in + Expect.equal (SeqSet.size aliceValues) 2 + ] + + +fuzzTests : Test +fuzzTests = + describe "Fuzz Tests" + [ fuzz2 fuzzPairs int "get works" <| + \pairs num -> + let + dict = + MultiSeqDict.fromFlatList pairs + + result = + MultiSeqDict.getAll num dict + + expected = + pairs + |> List.filter (\( k, _ ) -> k == num) + |> List.map Tuple.second + |> SeqSet.fromList + in + Expect.equal result expected + , fuzz fuzzPairs "fromFlatList creates valid dict" <| + \pairs -> + let + dict = + MultiSeqDict.fromFlatList pairs + + uniqueKeys = + pairs + |> List.map Tuple.first + |> SeqSet.fromList + |> SeqSet.size + + dictKeys = + MultiSeqDict.keys dict |> List.length + in + Expect.equal uniqueKeys dictKeys + , fuzz2 fuzzPairs int "insert works" <| + \pairs num -> + let + dict = + MultiSeqDict.fromFlatList pairs + |> MultiSeqDict.insert num num + + values = + MultiSeqDict.getAll num dict + in + Expect.equal (SeqSet.member num values) True + , fuzz2 fuzzPairs int "remove works" <| + \pairs num -> + let + dict = + MultiSeqDict.fromFlatList pairs + |> MultiSeqDict.remove num num + + values = + MultiSeqDict.getAll num dict + in + Expect.equal (SeqSet.member num values) False + , fuzz2 fuzzPairs int "removeAll works" <| + \pairs num -> + let + dict = + MultiSeqDict.fromFlatList pairs + |> MultiSeqDict.removeAll num + in + Expect.equal (MultiSeqDict.member num dict) False + , fuzz2 fuzzPairs fuzzPairs "union works" <| + \pairs1 pairs2 -> + let + d1 = + MultiSeqDict.fromFlatList pairs1 + + d2 = + MultiSeqDict.fromFlatList pairs2 + + result = + MultiSeqDict.union d1 d2 + + unionKeys = + (pairs1 ++ pairs2) + |> List.map Tuple.first + |> SeqSet.fromList + |> SeqSet.size + + resultKeys = + MultiSeqDict.keys result |> List.length + in + Expect.equal unionKeys resultKeys + , fuzz fuzzPairs "size counts all pairs" <| + \pairs -> + let + dict = + MultiSeqDict.fromFlatList pairs + + uniquePairs = + pairs + |> SeqSet.fromList + |> SeqSet.size + in + Expect.equal (MultiSeqDict.size dict) uniquePairs + ]