From 14987393d842d9689cc6b29ee4e3daebcc106991 Mon Sep 17 00:00:00 2001 From: Charlon Date: Tue, 4 Nov 2025 16:09:30 +0700 Subject: [PATCH 1/8] Add BiSeqDict, MultiSeqDict, and MultiBiSeqDict modules Add three new dictionary types built on top of SeqDict: - BiSeqDict: Many-to-one bidirectional dictionary with reverse lookups - MultiSeqDict: One-to-many dictionary (keys map to multiple values) - MultiBiSeqDict: Many-to-many bidirectional dictionary These modules preserve insertion order and work with any types (not just comparable), leveraging SeqDict's FNV hashing implementation. --- elm.json | 6 +- src/BiSeqDict.elm | 421 +++++++++++++++++++++++++++++++++++++++ src/MultiBiSeqDict.elm | 436 +++++++++++++++++++++++++++++++++++++++++ src/MultiSeqDict.elm | 384 ++++++++++++++++++++++++++++++++++++ 4 files changed, 1246 insertions(+), 1 deletion(-) create mode 100644 src/BiSeqDict.elm create mode 100644 src/MultiBiSeqDict.elm create mode 100644 src/MultiSeqDict.elm diff --git a/elm.json b/elm.json index 7c39142..a42dccd 100644 --- a/elm.json +++ b/elm.json @@ -6,12 +6,16 @@ "version": "1.0.0", "exposed-modules": [ "SeqDict", - "SeqSet" + "SeqSet", + "BiSeqDict", + "MultiSeqDict", + "MultiBiSeqDict" ], "elm-version": "0.19.0 <= v < 0.20.0", "dependencies": { "elm/bytes": "1.0.8 <= v < 2.0.0", "elm/core": "1.0.0 <= v < 2.0.0", + "elm-community/list-extra": "8.5.1 <= v < 9.0.0", "lamdera/codecs": "1.0.0 <= v < 2.0.0" }, "test-dependencies": { diff --git a/src/BiSeqDict.elm b/src/BiSeqDict.elm new file mode 100644 index 0000000..0227c52 --- /dev/null +++ b/src/BiSeqDict.elm @@ -0,0 +1,421 @@ +module BiSeqDict exposing + ( BiSeqDict + , toDict, fromDict, getReverse, 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 + ) + +{-| 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.getReverse 1 manyToOne + --> Set.fromList ["A", "C"] + + +# Dictionaries + +@docs BiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict, getReverse, 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 + +-} + +import SeqDict exposing (SeqDict) + +import Set exposing (Set) + + +{-| The underlying data structure. Think about it as + + type alias BiSeqDict a b = + { forward : SeqDict a b -- just a normal Dict! + , reverse : SeqDict b (Set a) -- the reverse mappings! + } + +-} +type BiSeqDict comparable1 comparable2 + = BiSeqDict + { forward : SeqDict comparable1 comparable2 + , reverse : SeqDict comparable2 (Set comparable1) + } + + +{-| Create an empty dictionary. +-} +empty : BiSeqDict comparable1 comparable2 +empty = + BiSeqDict + { forward = SeqDict.empty + , reverse = SeqDict.empty + } + + +{-| Create a dictionary with one key-value pair. +-} +singleton : comparable1 -> comparable2 -> BiSeqDict comparable1 comparable2 +singleton from to = + BiSeqDict + { forward = SeqDict.singleton from to + , reverse = SeqDict.singleton to (Set.singleton from) + } + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : comparable1 -> comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 (Set.remove from) + >> Maybe.andThen normalizeSet + ) + in + reverseWithoutOld + |> SeqDict.update to (Maybe.withDefault Set.empty >> Set.insert from >> Just) + } + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : comparable1 -> (Maybe comparable2 -> Maybe comparable2) -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +update from fn (BiSeqDict d) = + SeqDict.update from fn d.forward + |> fromDict + + +{-| In our model, (Just Set.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : Set comparable -> Maybe (Set comparable) +normalizeSet set = + if Set.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 : comparable1 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +remove from (BiSeqDict d) = + BiSeqDict + { d + | forward = SeqDict.remove from d.forward + , reverse = SeqDict.filterMap (\_ set -> Set.remove from set |> normalizeSet) d.reverse + } + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : BiSeqDict comparable1 comparable2 -> Bool +isEmpty (BiSeqDict d) = + SeqDict.isEmpty d.forward + + +{-| Determine if a key is in a dictionary. +-} +member : comparable1 -> BiSeqDict comparable1 comparable2 -> 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 : comparable1 -> BiSeqDict comparable1 comparable2 -> Maybe comparable2 +get from (BiSeqDict d) = + SeqDict.get from d.forward + + +{-| Get the keys associated with a value. If the value is not found, +return an empty set. +-} +getReverse : comparable2 -> BiSeqDict comparable1 comparable2 -> Set comparable1 +getReverse to (BiSeqDict d) = + SeqDict.get to d.reverse + |> Maybe.withDefault Set.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : BiSeqDict comparable1 comparable2 -> 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 comparable1 comparable2 -> List comparable1 +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 comparable1 comparable2 -> List comparable2 +values (BiSeqDict d) = + SeqDict.values d.forward + + +{-| Get a list of unique values in the dictionary. +-} +uniqueValues : BiSeqDict comparable1 comparable2 -> List comparable2 +uniqueValues (BiSeqDict d) = + SeqDict.keys d.reverse + + +{-| Get a count of unique values in the dictionary. +-} +uniqueValuesCount : BiSeqDict comparable1 comparable2 -> Int +uniqueValuesCount (BiSeqDict d) = + SeqDict.size d.reverse + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : BiSeqDict comparable1 comparable2 -> List ( comparable1, comparable2 ) +toList (BiSeqDict d) = + SeqDict.toList d.forward + + +{-| Convert a dictionary into a reverse association list of value-keys pairs. +-} +toReverseList : BiSeqDict comparable1 comparable2 -> List ( comparable2, Set comparable1 ) +toReverseList (BiSeqDict d) = + SeqDict.toList d.reverse + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( comparable1, comparable2 ) -> BiSeqDict comparable1 comparable2 +fromList list = + SeqDict.fromList list + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (comparable1 -> comparable21 -> comparable22) -> BiSeqDict comparable1 comparable21 -> BiSeqDict comparable1 comparable22 +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 comparable1 comparable2 -> SeqDict comparable1 comparable2 +toDict (BiSeqDict d) = + d.forward + + +{-| Convert Dict into a BiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +fromDict forward = + BiSeqDict + { forward = forward + , reverse = + forward + |> SeqDict.foldl + (\key value acc -> + SeqDict.update value + (\maybeKeys -> + Just <| + case maybeKeys of + Nothing -> + Set.singleton key + + Just keys_ -> + Set.insert key keys_ + ) + acc + ) + SeqDict.empty + } + + +{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. + + + getAges users = + SeqDict.foldl addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [33,19,28] + +-} +foldl : (comparable1 -> comparable2 -> acc -> acc) -> acc -> BiSeqDict comparable1 comparable2 -> acc +foldl fn zero (BiSeqDict d) = + SeqDict.foldl fn zero d.forward + + +{-| Fold over the key-value pairs in a dictionary from highest key to lowest key. + + + getAges users = + SeqDict.foldr addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [28,19,33] + +-} +foldr : (comparable1 -> comparable2 -> acc -> acc) -> acc -> BiSeqDict comparable1 comparable2 -> acc +foldr fn zero (BiSeqDict d) = + SeqDict.foldr fn zero d.forward + + +{-| Keep only the key-value pairs that pass the given test. +-} +filter : (comparable1 -> comparable2 -> Bool) -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 : (comparable1 -> comparable2 -> Bool) -> BiSeqDict comparable1 comparable2 -> ( BiSeqDict comparable1 comparable2, BiSeqDict comparable1 comparable2 ) +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 comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 : + (comparable1 -> comparable21 -> acc -> acc) + -> (comparable1 -> comparable21 -> comparable22 -> acc -> acc) + -> (comparable1 -> comparable22 -> acc -> acc) + -> BiSeqDict comparable1 comparable21 + -> BiSeqDict comparable1 comparable22 + -> acc + -> acc +merge fnLeft fnBoth fnRight (BiSeqDict left) (BiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left.forward right.forward zero diff --git a/src/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm new file mode 100644 index 0000000..ecb72d9 --- /dev/null +++ b/src/MultiBiSeqDict.elm @@ -0,0 +1,436 @@ +module MultiBiSeqDict exposing + ( MultiBiSeqDict + , toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList + , empty, singleton, insert, update, remove, removeAll + , isEmpty, member, get, size + , keys, values, toList, fromList + , map, foldl, foldr, filter, partition + , union, intersect, diff, merge + ) + +{-| 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.get "A" manyToMany + --> Set.fromList [1, 2] + + MultiBiSeqDict.getReverse 2 manyToMany + --> Set.fromList ["A", "B"] + + +# Dictionaries + +@docs MultiBiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList + + +# Build + +@docs empty, singleton, insert, update, remove, removeAll + + +# 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 + +-} + +import SeqDict exposing (SeqDict) + +import Set exposing (Set) + + +{-| The underlying data structure. Think about it as + + type alias MultiBiSeqDict comparable1 comparable2 = + { forward : SeqDict comparable1 (Set comparable2) -- just a normal Dict! + , reverse : SeqDict comparable2 (Set comparable1) -- the reverse mappings! + } + +-} +type MultiBiSeqDict comparable1 comparable2 + = MultiBiSeqDict + { forward : SeqDict comparable1 (Set comparable2) + , reverse : SeqDict comparable2 (Set comparable1) + } + + +{-| Create an empty dictionary. +-} +empty : MultiBiSeqDict comparable1 comparable2 +empty = + MultiBiSeqDict + { forward = SeqDict.empty + , reverse = SeqDict.empty + } + + +{-| Create a dictionary with one key-value pair. +-} +singleton : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 +singleton from to = + MultiBiSeqDict + { forward = SeqDict.singleton from (Set.singleton to) + , reverse = SeqDict.singleton to (Set.singleton from) + } + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +insert from to (MultiBiSeqDict d) = + SeqDict.update + from + (\maybeSet -> + case maybeSet of + Nothing -> + Just (Set.singleton to) + + Just set -> + Just (Set.insert to set) + ) + d.forward + |> fromDict + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : comparable1 -> (Set comparable2 -> Set comparable2) -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +update from fn (MultiBiSeqDict d) = + SeqDict.update from (Maybe.andThen (normalizeSet << fn)) d.forward + |> fromDict + + +{-| In our model, (Just Set.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : Set comparable1 -> Maybe (Set comparable1) +normalizeSet set = + if Set.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 : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +removeAll from (MultiBiSeqDict d) = + MultiBiSeqDict + { d + | forward = SeqDict.remove from d.forward + , reverse = SeqDict.filterMap (\_ set -> Set.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 : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +remove from to (MultiBiSeqDict d) = + SeqDict.update from (Maybe.andThen (Set.remove to >> normalizeSet)) d.forward + |> fromDict + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : MultiBiSeqDict comparable1 comparable2 -> Bool +isEmpty (MultiBiSeqDict d) = + SeqDict.isEmpty d.forward + + +{-| Determine if a key is in a dictionary. +-} +member : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> Bool +member from (MultiBiSeqDict 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 : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> Set comparable2 +get from (MultiBiSeqDict d) = + SeqDict.get from d.forward + |> Maybe.withDefault Set.empty + + +{-| Get the keys associated with a value. If the value is not found, +return an empty set. +-} +getReverse : comparable2 -> MultiBiSeqDict comparable1 comparable2 -> Set comparable1 +getReverse to (MultiBiSeqDict d) = + SeqDict.get to d.reverse + |> Maybe.withDefault Set.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : MultiBiSeqDict comparable1 comparable2 -> Int +size (MultiBiSeqDict d) = + SeqDict.foldl (\_ set acc -> Set.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 comparable1 comparable2 -> List comparable1 +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 comparable1 comparable2 -> List comparable2 +values (MultiBiSeqDict d) = + SeqDict.values d.forward + |> List.concatMap Set.toList + + +{-| Get a list of unique values in the dictionary. +-} +uniqueValues : MultiBiSeqDict comparable1 comparable2 -> List comparable2 +uniqueValues (MultiBiSeqDict d) = + SeqDict.keys d.reverse + + +{-| Get a count of unique values in the dictionary. +-} +uniqueValuesCount : MultiBiSeqDict comparable1 comparable2 -> Int +uniqueValuesCount (MultiBiSeqDict d) = + SeqDict.size d.reverse + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : MultiBiSeqDict comparable1 comparable2 -> List ( comparable1, Set comparable2 ) +toList (MultiBiSeqDict d) = + SeqDict.toList d.forward + + +{-| Convert a dictionary into a reverse association list of value-keys pairs. +-} +toReverseList : MultiBiSeqDict comparable1 comparable2 -> List ( comparable2, Set comparable1 ) +toReverseList (MultiBiSeqDict d) = + SeqDict.toList d.reverse + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( comparable1, Set comparable2 ) -> MultiBiSeqDict comparable1 comparable2 +fromList list = + SeqDict.fromList list + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (comparable1 -> comparable21 -> comparable22) -> MultiBiSeqDict comparable1 comparable21 -> MultiBiSeqDict comparable1 comparable22 +map fn (MultiBiSeqDict d) = + -- TODO diff instead of throwing away and creating from scratch? + SeqDict.map (\key set -> Set.map (fn key) set) d.forward + |> fromDict + + +{-| Convert MultiBiSeqDict into a SeqDict. (Throw away the reverse mapping.) +-} +toDict : MultiBiSeqDict comparable1 comparable2 -> SeqDict comparable1 (Set comparable2) +toDict (MultiBiSeqDict d) = + d.forward + + +{-| Convert Dict into a MultiBiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict comparable1 (Set comparable2) -> MultiBiSeqDict comparable1 comparable2 +fromDict forward = + MultiBiSeqDict + { forward = forward + , reverse = + SeqDict.foldl + (\key set acc -> + Set.foldl + (\value acc_ -> + SeqDict.update + value + (\maybeSet -> + case maybeSet of + Nothing -> + Just (Set.singleton key) + + Just set_ -> + Just (Set.insert key set_) + ) + acc_ + ) + acc + set + ) + SeqDict.empty + forward + } + + +{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. + + + getAges users = + SeqDict.foldl addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [33,19,28] + +-} +foldl : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiBiSeqDict comparable1 comparable2 -> acc +foldl fn zero (MultiBiSeqDict d) = + SeqDict.foldl fn zero d.forward + + +{-| Fold over the key-value pairs in a dictionary from highest key to lowest key. + + + getAges users = + SeqDict.foldr addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [28,19,33] + +-} +foldr : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiBiSeqDict comparable1 comparable2 -> acc +foldr fn zero (MultiBiSeqDict d) = + SeqDict.foldr fn zero d.forward + + +{-| Keep only the mappings that pass the given test. +-} +filter : (comparable1 -> comparable2 -> Bool) -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +filter fn (MultiBiSeqDict d) = + SeqDict.toList d.forward + |> List.filterMap + (\( key, values_ ) -> + values_ + |> Set.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 : (comparable1 -> Set comparable2 -> Bool) -> MultiBiSeqDict comparable1 comparable2 -> ( MultiBiSeqDict comparable1 comparable2, MultiBiSeqDict comparable1 comparable2 ) +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 comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 : + (comparable1 -> Set comparable21 -> acc -> acc) + -> (comparable1 -> Set comparable21 -> Set comparable22 -> acc -> acc) + -> (comparable1 -> Set comparable22 -> acc -> acc) + -> MultiBiSeqDict comparable1 comparable21 + -> MultiBiSeqDict comparable1 comparable22 + -> acc + -> acc +merge fnLeft fnBoth fnRight (MultiBiSeqDict left) (MultiBiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left.forward right.forward zero diff --git a/src/MultiSeqDict.elm b/src/MultiSeqDict.elm new file mode 100644 index 0000000..c9b4985 --- /dev/null +++ b/src/MultiSeqDict.elm @@ -0,0 +1,384 @@ +module MultiSeqDict exposing + ( MultiSeqDict + , toDict, fromDict + , empty, singleton, insert, update, remove, removeAll + , isEmpty, member, get, size + , keys, values, toList, fromList, fromFlatList + , map, foldl, foldr, filter, partition + , union, intersect, diff, merge + ) + +{-| 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.get "A" oneToMany + --> Set.fromList [1, 2] + + +# Dictionaries + +@docs MultiSeqDict + + +# Differences from Dict + +@docs toDict, fromDict + + +# Build + +@docs empty, singleton, insert, update, remove, removeAll + + +# Query + +@docs isEmpty, member, get, size + + +# Lists + +@docs keys, values, toList, fromList, fromFlatList + + +# Transform + +@docs map, foldl, foldr, filter, partition + + +# Combine + +@docs union, intersect, diff, merge + +-} + +import SeqDict exposing (SeqDict) + +import List.Extra +import Set exposing (Set) + + +{-| The underlying data structure. Think about it as + + type alias MultiSeqDict comparable1 comparable2 = + Dict comparable1 (Set comparable2) -- just a normal Dict! + +-} +type MultiSeqDict comparable1 comparable2 + = MultiSeqDict (SeqDict comparable1 (Set comparable2)) + + +{-| Create an empty dictionary. +-} +empty : MultiSeqDict comparable1 comparable2 +empty = + MultiSeqDict SeqDict.empty + + +{-| Create a dictionary with one key-value pair. +-} +singleton : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 +singleton from to = + MultiSeqDict (SeqDict.singleton from (Set.singleton to)) + + +{-| Insert a key-value pair into a dictionary. Replaces value when there is +a collision. +-} +insert : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +insert from to (MultiSeqDict d) = + MultiSeqDict <| + SeqDict.update + from + (\maybeSet -> + case maybeSet of + Nothing -> + Just (Set.singleton to) + + Just set -> + Just (Set.insert to set) + ) + d + + +{-| Update the value of a dictionary for a specific key with a given function. +-} +update : comparable1 -> (Set comparable2 -> Set comparable2) -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +update from fn (MultiSeqDict d) = + MultiSeqDict <| SeqDict.update from (Maybe.andThen (normalizeSet << fn)) d + + +{-| In our model, (Just Set.empty) has the same meaning as Nothing. +Make it be Nothing! +-} +normalizeSet : Set comparable1 -> Maybe (Set comparable1) +normalizeSet set = + if Set.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 : comparable1 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +remove from to (MultiSeqDict d) = + MultiSeqDict <| + SeqDict.update from (Maybe.andThen (Set.remove to >> normalizeSet)) d + + +{-| Determine if a dictionary is empty. + + isEmpty empty == True + +-} +isEmpty : MultiSeqDict comparable1 comparable2 -> Bool +isEmpty (MultiSeqDict d) = + SeqDict.isEmpty d + + +{-| Determine if a key is in a dictionary. +-} +member : comparable1 -> MultiSeqDict comparable1 comparable2 -> Bool +member from (MultiSeqDict d) = + SeqDict.member from d + + +{-| 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 == Set.singleton "cat" + get "Jerry" animals == Set.singleton "mouse" + get "Spike" animals == Set.empty + +-} +get : comparable1 -> MultiSeqDict comparable1 comparable2 -> Set comparable2 +get from (MultiSeqDict d) = + SeqDict.get from d + |> Maybe.withDefault Set.empty + + +{-| Determine the number of key-value pairs in the dictionary. +-} +size : MultiSeqDict comparable1 comparable2 -> Int +size (MultiSeqDict d) = + SeqDict.foldl (\_ set acc -> Set.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 comparable1 comparable2 -> List comparable1 +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 comparable1 comparable2 -> List comparable2 +values (MultiSeqDict d) = + SeqDict.values d + |> List.concatMap Set.toList + + +{-| Convert a dictionary into an association list of key-value pairs, sorted by keys. +-} +toList : MultiSeqDict comparable1 comparable2 -> List ( comparable1, Set comparable2 ) +toList (MultiSeqDict d) = + SeqDict.toList d + + +{-| Convert an association list into a dictionary. +-} +fromList : List ( comparable1, Set comparable2 ) -> MultiSeqDict comparable1 comparable2 +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", Set.fromList [ 1, 3 ] ) + , ( "bar", Set.fromList [ 2 ] ) + ] + +-} +fromFlatList : List ( comparable1, comparable2 ) -> MultiSeqDict comparable1 comparable2 +fromFlatList list = + list + |> List.Extra.gatherEqualsBy Tuple.first + |> List.map + (\( ( key, _ ) as x, xs ) -> + ( key + , Set.fromList <| List.map Tuple.second <| x :: xs + ) + ) + |> SeqDict.fromList + |> fromDict + + +{-| Apply a function to all values in a dictionary. +-} +map : (comparable1 -> comparable21 -> comparable22) -> MultiSeqDict comparable1 comparable21 -> MultiSeqDict comparable1 comparable22 +map fn (MultiSeqDict d) = + MultiSeqDict <| SeqDict.map (\key set -> Set.map (fn key) set) d + + +{-| Convert MultiSeqDict into a SeqDict. (Throw away the reverse mapping.) +-} +toDict : MultiSeqDict comparable1 comparable2 -> SeqDict comparable1 (Set comparable2) +toDict (MultiSeqDict d) = + d + + +{-| Convert Dict into a MultiSeqDict. (Compute the reverse mapping.) +-} +fromDict : SeqDict comparable1 (Set comparable2) -> MultiSeqDict comparable1 comparable2 +fromDict dict = + MultiSeqDict dict + + +{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. + + + getAges users = + SeqDict.foldl addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [33,19,28] + +-} +foldl : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiSeqDict comparable1 comparable2 -> acc +foldl fn zero (MultiSeqDict d) = + SeqDict.foldl fn zero d + + +{-| Fold over the key-value pairs in a dictionary from highest key to lowest key. + + + getAges users = + SeqDict.foldr addAge [] users + + addAge _ user ages = + user.age :: ages + + -- getAges users == [28,19,33] + +-} +foldr : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiSeqDict comparable1 comparable2 -> acc +foldr fn zero (MultiSeqDict d) = + SeqDict.foldr fn zero d + + +{-| Keep only the mappings that pass the given test. +-} +filter : (comparable1 -> comparable2 -> Bool) -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +filter fn (MultiSeqDict d) = + SeqDict.toList d + |> List.filterMap + (\( key, values_ ) -> + values_ + |> Set.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 : (comparable1 -> Set comparable2 -> Bool) -> MultiSeqDict comparable1 comparable2 -> ( MultiSeqDict comparable1 comparable2, MultiSeqDict comparable1 comparable2 ) +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 comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 : + (comparable1 -> Set comparable21 -> acc -> acc) + -> (comparable1 -> Set comparable21 -> Set comparable22 -> acc -> acc) + -> (comparable1 -> Set comparable22 -> acc -> acc) + -> MultiSeqDict comparable1 comparable21 + -> MultiSeqDict comparable1 comparable22 + -> acc + -> acc +merge fnLeft fnBoth fnRight (MultiSeqDict left) (MultiSeqDict right) zero = + SeqDict.merge fnLeft fnBoth fnRight left right zero From e94d10b70c3e0ffddd8b63c572aa4c3ccc6d5802 Mon Sep 17 00:00:00 2001 From: Charlon Date: Tue, 4 Nov 2025 18:26:26 +0700 Subject: [PATCH 2/8] Remove comparable constraints - use SeqSet instead of Set Fixes feedback from @MartinSStewart: The modules were unnecessarily restricting type variables to 'comparable'. Changes: - Replace Set with SeqSet (no comparable constraint needed) - Rename type vars from 'comparable1/comparable2' to 'k/v' for clarity - All three modules (BiSeqDict, MultiSeqDict, MultiBiSeqDict) now work with any types, not just comparables This fully leverages SeqDict's FNV hashing implementation. --- src/BiSeqDict.elm | 96 ++++++++++++++++----------------- src/MultiBiSeqDict.elm | 120 ++++++++++++++++++++--------------------- src/MultiSeqDict.elm | 108 ++++++++++++++++++------------------- 3 files changed, 162 insertions(+), 162 deletions(-) diff --git a/src/BiSeqDict.elm b/src/BiSeqDict.elm index 0227c52..1980928 100644 --- a/src/BiSeqDict.elm +++ b/src/BiSeqDict.elm @@ -22,7 +22,7 @@ Example usage: |> BiSeqDict.insert "D" 4 BiSeqDict.getReverse 1 manyToOne - --> Set.fromList ["A", "C"] + --> SeqSet.fromList ["A", "C"] # Dictionaries @@ -63,27 +63,27 @@ Example usage: import SeqDict exposing (SeqDict) -import Set exposing (Set) +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 (Set a) -- the reverse mappings! + , reverse : SeqDict b (SeqSet a) -- the reverse mappings! } -} -type BiSeqDict comparable1 comparable2 +type BiSeqDict k v = BiSeqDict - { forward : SeqDict comparable1 comparable2 - , reverse : SeqDict comparable2 (Set comparable1) + { forward : SeqDict k v + , reverse : SeqDict v (SeqSet k) } {-| Create an empty dictionary. -} -empty : BiSeqDict comparable1 comparable2 +empty : BiSeqDict k v empty = BiSeqDict { forward = SeqDict.empty @@ -93,18 +93,18 @@ empty = {-| Create a dictionary with one key-value pair. -} -singleton : comparable1 -> comparable2 -> BiSeqDict comparable1 comparable2 +singleton : k -> v -> BiSeqDict k v singleton from to = BiSeqDict { forward = SeqDict.singleton from to - , reverse = SeqDict.singleton to (Set.singleton from) + , reverse = SeqDict.singleton to (SeqSet.singleton from) } {-| Insert a key-value pair into a dictionary. Replaces value when there is a collision. -} -insert : comparable1 -> comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +insert : k -> v -> BiSeqDict k v -> BiSeqDict k v insert from to (BiSeqDict d) = BiSeqDict { d @@ -122,29 +122,29 @@ insert from to (BiSeqDict d) = Just oldTo_ -> d.reverse |> SeqDict.update oldTo_ - (Maybe.map (Set.remove from) + (Maybe.map (SeqSet.remove from) >> Maybe.andThen normalizeSet ) in reverseWithoutOld - |> SeqDict.update to (Maybe.withDefault Set.empty >> Set.insert from >> Just) + |> 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 : comparable1 -> (Maybe comparable2 -> Maybe comparable2) -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 Set.empty) has the same meaning as Nothing. +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. Make it be Nothing! -} -normalizeSet : Set comparable -> Maybe (Set comparable) +normalizeSet : SeqSet k -> Maybe (SeqSet k) normalizeSet set = - if Set.isEmpty set then + if SeqSet.isEmpty set then Nothing else @@ -154,12 +154,12 @@ normalizeSet set = {-| Remove a key-value pair from a dictionary. If the key is not found, no changes are made. -} -remove : comparable1 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +remove : k -> BiSeqDict k v -> BiSeqDict k v remove from (BiSeqDict d) = BiSeqDict { d | forward = SeqDict.remove from d.forward - , reverse = SeqDict.filterMap (\_ set -> Set.remove from set |> normalizeSet) d.reverse + , reverse = SeqDict.filterMap (\_ set -> SeqSet.remove from set |> normalizeSet) d.reverse } @@ -168,14 +168,14 @@ remove from (BiSeqDict d) = isEmpty empty == True -} -isEmpty : BiSeqDict comparable1 comparable2 -> Bool +isEmpty : BiSeqDict k v -> Bool isEmpty (BiSeqDict d) = SeqDict.isEmpty d.forward {-| Determine if a key is in a dictionary. -} -member : comparable1 -> BiSeqDict comparable1 comparable2 -> Bool +member : k -> BiSeqDict k v -> Bool member from (BiSeqDict d) = SeqDict.member from d.forward @@ -191,7 +191,7 @@ dictionary. get "Spike" animals == Nothing -} -get : comparable1 -> BiSeqDict comparable1 comparable2 -> Maybe comparable2 +get : k -> BiSeqDict k v -> Maybe v get from (BiSeqDict d) = SeqDict.get from d.forward @@ -199,15 +199,15 @@ get from (BiSeqDict d) = {-| Get the keys associated with a value. If the value is not found, return an empty set. -} -getReverse : comparable2 -> BiSeqDict comparable1 comparable2 -> Set comparable1 +getReverse : v -> BiSeqDict k v -> SeqSet k getReverse to (BiSeqDict d) = SeqDict.get to d.reverse - |> Maybe.withDefault Set.empty + |> Maybe.withDefault SeqSet.empty {-| Determine the number of key-value pairs in the dictionary. -} -size : BiSeqDict comparable1 comparable2 -> Int +size : BiSeqDict k v -> Int size (BiSeqDict d) = SeqDict.size d.forward @@ -217,7 +217,7 @@ size (BiSeqDict d) = keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] -} -keys : BiSeqDict comparable1 comparable2 -> List comparable1 +keys : BiSeqDict k v -> List k keys (BiSeqDict d) = SeqDict.keys d.forward @@ -227,42 +227,42 @@ keys (BiSeqDict d) = values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] -} -values : BiSeqDict comparable1 comparable2 -> List comparable2 +values : BiSeqDict k v -> List v values (BiSeqDict d) = SeqDict.values d.forward {-| Get a list of unique values in the dictionary. -} -uniqueValues : BiSeqDict comparable1 comparable2 -> List comparable2 +uniqueValues : BiSeqDict k v -> List v uniqueValues (BiSeqDict d) = SeqDict.keys d.reverse {-| Get a count of unique values in the dictionary. -} -uniqueValuesCount : BiSeqDict comparable1 comparable2 -> Int +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 comparable1 comparable2 -> List ( comparable1, comparable2 ) +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 comparable1 comparable2 -> List ( comparable2, Set comparable1 ) +toReverseList : BiSeqDict k v -> List ( v, SeqSet k ) toReverseList (BiSeqDict d) = SeqDict.toList d.reverse {-| Convert an association list into a dictionary. -} -fromList : List ( comparable1, comparable2 ) -> BiSeqDict comparable1 comparable2 +fromList : List ( k, v ) -> BiSeqDict k v fromList list = SeqDict.fromList list |> fromDict @@ -270,7 +270,7 @@ fromList list = {-| Apply a function to all values in a dictionary. -} -map : (comparable1 -> comparable21 -> comparable22) -> BiSeqDict comparable1 comparable21 -> BiSeqDict comparable1 comparable22 +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 @@ -279,14 +279,14 @@ map fn (BiSeqDict d) = {-| Convert BiSeqDict into a SeqDict. (Throw away the reverse mapping.) -} -toDict : BiSeqDict comparable1 comparable2 -> SeqDict comparable1 comparable2 +toDict : BiSeqDict k v -> SeqDict k v toDict (BiSeqDict d) = d.forward {-| Convert Dict into a BiSeqDict. (Compute the reverse mapping.) -} -fromDict : SeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +fromDict : SeqDict k v -> BiSeqDict k v fromDict forward = BiSeqDict { forward = forward @@ -299,10 +299,10 @@ fromDict forward = Just <| case maybeKeys of Nothing -> - Set.singleton key + SeqSet.singleton key Just keys_ -> - Set.insert key keys_ + SeqSet.insert key keys_ ) acc ) @@ -322,7 +322,7 @@ fromDict forward = -- getAges users == [33,19,28] -} -foldl : (comparable1 -> comparable2 -> acc -> acc) -> acc -> BiSeqDict comparable1 comparable2 -> acc +foldl : (k -> v -> acc -> acc) -> acc -> BiSeqDict k v -> acc foldl fn zero (BiSeqDict d) = SeqDict.foldl fn zero d.forward @@ -339,14 +339,14 @@ foldl fn zero (BiSeqDict d) = -- getAges users == [28,19,33] -} -foldr : (comparable1 -> comparable2 -> acc -> acc) -> acc -> BiSeqDict comparable1 comparable2 -> acc +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 : (comparable1 -> comparable2 -> Bool) -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 @@ -357,7 +357,7 @@ filter fn (BiSeqDict d) = contains all key-value pairs which passed the test, and the second contains the pairs that did not. -} -partition : (comparable1 -> comparable2 -> Bool) -> BiSeqDict comparable1 comparable2 -> ( BiSeqDict comparable1 comparable2, BiSeqDict comparable1 comparable2 ) +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 @@ -372,7 +372,7 @@ partition fn (BiSeqDict d) = {-| Combine two dictionaries. If there is a collision, preference is given to the first dictionary. -} -union : BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 @@ -382,7 +382,7 @@ union (BiSeqDict left) (BiSeqDict right) = {-| Keep a key-value pair when its key appears in the second dictionary. Preference is given to values in the first dictionary. -} -intersect : BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 @@ -391,7 +391,7 @@ intersect (BiSeqDict left) (BiSeqDict right) = {-| Keep a key-value pair when its key does not appear in the second dictionary. -} -diff : BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 -> BiSeqDict comparable1 comparable2 +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 @@ -410,11 +410,11 @@ you want. -} merge : - (comparable1 -> comparable21 -> acc -> acc) - -> (comparable1 -> comparable21 -> comparable22 -> acc -> acc) - -> (comparable1 -> comparable22 -> acc -> acc) - -> BiSeqDict comparable1 comparable21 - -> BiSeqDict comparable1 comparable22 + (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 = diff --git a/src/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm index ecb72d9..2520286 100644 --- a/src/MultiBiSeqDict.elm +++ b/src/MultiBiSeqDict.elm @@ -23,10 +23,10 @@ Example usage: |> MultiBiSeqDict.insert "A" 2 MultiBiSeqDict.get "A" manyToMany - --> Set.fromList [1, 2] + --> SeqSet.fromList [1, 2] MultiBiSeqDict.getReverse 2 manyToMany - --> Set.fromList ["A", "B"] + --> SeqSet.fromList ["A", "B"] # Dictionaries @@ -67,27 +67,27 @@ Example usage: import SeqDict exposing (SeqDict) -import Set exposing (Set) +import SeqSet exposing (SeqSet) {-| The underlying data structure. Think about it as - type alias MultiBiSeqDict comparable1 comparable2 = - { forward : SeqDict comparable1 (Set comparable2) -- just a normal Dict! - , reverse : SeqDict comparable2 (Set comparable1) -- the reverse mappings! + type alias MultiBiSeqDict k v = + { forward : SeqDict k (SeqSet v) -- just a normal Dict! + , reverse : SeqDict v (SeqSet k) -- the reverse mappings! } -} -type MultiBiSeqDict comparable1 comparable2 +type MultiBiSeqDict k v = MultiBiSeqDict - { forward : SeqDict comparable1 (Set comparable2) - , reverse : SeqDict comparable2 (Set comparable1) + { forward : SeqDict k (SeqSet v) + , reverse : SeqDict v (SeqSet k) } {-| Create an empty dictionary. -} -empty : MultiBiSeqDict comparable1 comparable2 +empty : MultiBiSeqDict k v empty = MultiBiSeqDict { forward = SeqDict.empty @@ -97,28 +97,28 @@ empty = {-| Create a dictionary with one key-value pair. -} -singleton : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 +singleton : k -> v -> MultiBiSeqDict k v singleton from to = MultiBiSeqDict - { forward = SeqDict.singleton from (Set.singleton to) - , reverse = SeqDict.singleton to (Set.singleton from) + { 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 : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +insert : k -> v -> MultiBiSeqDict k v -> MultiBiSeqDict k v insert from to (MultiBiSeqDict d) = SeqDict.update from (\maybeSet -> case maybeSet of Nothing -> - Just (Set.singleton to) + Just (SeqSet.singleton to) Just set -> - Just (Set.insert to set) + Just (SeqSet.insert to set) ) d.forward |> fromDict @@ -126,18 +126,18 @@ insert from to (MultiBiSeqDict d) = {-| Update the value of a dictionary for a specific key with a given function. -} -update : comparable1 -> (Set comparable2 -> Set comparable2) -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 Set.empty) has the same meaning as Nothing. +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. Make it be Nothing! -} -normalizeSet : Set comparable1 -> Maybe (Set comparable1) +normalizeSet : SeqSet k -> Maybe (SeqSet k) normalizeSet set = - if Set.isEmpty set then + if SeqSet.isEmpty set then Nothing else @@ -147,21 +147,21 @@ normalizeSet set = {-| Remove all key-value pairs for the given key from a dictionary. If the key is not found, no changes are made. -} -removeAll : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +removeAll : k -> MultiBiSeqDict k v -> MultiBiSeqDict k v removeAll from (MultiBiSeqDict d) = MultiBiSeqDict { d | forward = SeqDict.remove from d.forward - , reverse = SeqDict.filterMap (\_ set -> Set.remove from set |> normalizeSet) d.reverse + , 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 : comparable1 -> comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +remove : k -> v -> MultiBiSeqDict k v -> MultiBiSeqDict k v remove from to (MultiBiSeqDict d) = - SeqDict.update from (Maybe.andThen (Set.remove to >> normalizeSet)) d.forward + SeqDict.update from (Maybe.andThen (SeqSet.remove to >> normalizeSet)) d.forward |> fromDict @@ -170,14 +170,14 @@ remove from to (MultiBiSeqDict d) = isEmpty empty == True -} -isEmpty : MultiBiSeqDict comparable1 comparable2 -> Bool +isEmpty : MultiBiSeqDict k v -> Bool isEmpty (MultiBiSeqDict d) = SeqDict.isEmpty d.forward {-| Determine if a key is in a dictionary. -} -member : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> Bool +member : k -> MultiBiSeqDict k v -> Bool member from (MultiBiSeqDict d) = SeqDict.member from d.forward @@ -193,26 +193,26 @@ dictionary. get "Spike" animals == Nothing -} -get : comparable1 -> MultiBiSeqDict comparable1 comparable2 -> Set comparable2 +get : k -> MultiBiSeqDict k v -> SeqSet v get from (MultiBiSeqDict d) = SeqDict.get from d.forward - |> Maybe.withDefault Set.empty + |> Maybe.withDefault SeqSet.empty {-| Get the keys associated with a value. If the value is not found, return an empty set. -} -getReverse : comparable2 -> MultiBiSeqDict comparable1 comparable2 -> Set comparable1 +getReverse : v -> MultiBiSeqDict k v -> SeqSet k getReverse to (MultiBiSeqDict d) = SeqDict.get to d.reverse - |> Maybe.withDefault Set.empty + |> Maybe.withDefault SeqSet.empty {-| Determine the number of key-value pairs in the dictionary. -} -size : MultiBiSeqDict comparable1 comparable2 -> Int +size : MultiBiSeqDict k v -> Int size (MultiBiSeqDict d) = - SeqDict.foldl (\_ set acc -> Set.size set + acc) 0 d.forward + SeqDict.foldl (\_ set acc -> SeqSet.size set + acc) 0 d.forward {-| Get all of the keys in a dictionary, sorted from lowest to highest. @@ -220,7 +220,7 @@ size (MultiBiSeqDict d) = keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] -} -keys : MultiBiSeqDict comparable1 comparable2 -> List comparable1 +keys : MultiBiSeqDict k v -> List k keys (MultiBiSeqDict d) = SeqDict.keys d.forward @@ -230,43 +230,43 @@ keys (MultiBiSeqDict d) = values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] -} -values : MultiBiSeqDict comparable1 comparable2 -> List comparable2 +values : MultiBiSeqDict k v -> List v values (MultiBiSeqDict d) = SeqDict.values d.forward - |> List.concatMap Set.toList + |> List.concatMap SeqSet.toList {-| Get a list of unique values in the dictionary. -} -uniqueValues : MultiBiSeqDict comparable1 comparable2 -> List comparable2 +uniqueValues : MultiBiSeqDict k v -> List v uniqueValues (MultiBiSeqDict d) = SeqDict.keys d.reverse {-| Get a count of unique values in the dictionary. -} -uniqueValuesCount : MultiBiSeqDict comparable1 comparable2 -> Int +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 comparable1 comparable2 -> List ( comparable1, Set comparable2 ) +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 comparable1 comparable2 -> List ( comparable2, Set comparable1 ) +toReverseList : MultiBiSeqDict k v -> List ( v, SeqSet k ) toReverseList (MultiBiSeqDict d) = SeqDict.toList d.reverse {-| Convert an association list into a dictionary. -} -fromList : List ( comparable1, Set comparable2 ) -> MultiBiSeqDict comparable1 comparable2 +fromList : List ( k, SeqSet v ) -> MultiBiSeqDict k v fromList list = SeqDict.fromList list |> fromDict @@ -274,40 +274,40 @@ fromList list = {-| Apply a function to all values in a dictionary. -} -map : (comparable1 -> comparable21 -> comparable22) -> MultiBiSeqDict comparable1 comparable21 -> MultiBiSeqDict comparable1 comparable22 +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 -> Set.map (fn key) set) d.forward + SeqDict.map (\key set -> SeqSet.map (fn key) set) d.forward |> fromDict {-| Convert MultiBiSeqDict into a SeqDict. (Throw away the reverse mapping.) -} -toDict : MultiBiSeqDict comparable1 comparable2 -> SeqDict comparable1 (Set comparable2) +toDict : MultiBiSeqDict k v -> SeqDict k (SeqSet v) toDict (MultiBiSeqDict d) = d.forward {-| Convert Dict into a MultiBiSeqDict. (Compute the reverse mapping.) -} -fromDict : SeqDict comparable1 (Set comparable2) -> MultiBiSeqDict comparable1 comparable2 +fromDict : SeqDict k (SeqSet v) -> MultiBiSeqDict k v fromDict forward = MultiBiSeqDict { forward = forward , reverse = SeqDict.foldl (\key set acc -> - Set.foldl + SeqSet.foldl (\value acc_ -> SeqDict.update value (\maybeSet -> case maybeSet of Nothing -> - Just (Set.singleton key) + Just (SeqSet.singleton key) Just set_ -> - Just (Set.insert key set_) + Just (SeqSet.insert key set_) ) acc_ ) @@ -331,7 +331,7 @@ fromDict forward = -- getAges users == [33,19,28] -} -foldl : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiBiSeqDict comparable1 comparable2 -> acc +foldl : (k -> SeqSet v -> acc -> acc) -> acc -> MultiBiSeqDict k v -> acc foldl fn zero (MultiBiSeqDict d) = SeqDict.foldl fn zero d.forward @@ -348,20 +348,20 @@ foldl fn zero (MultiBiSeqDict d) = -- getAges users == [28,19,33] -} -foldr : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiBiSeqDict comparable1 comparable2 -> acc +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 : (comparable1 -> comparable2 -> Bool) -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +filter : (k -> v -> Bool) -> MultiBiSeqDict k v -> MultiBiSeqDict k v filter fn (MultiBiSeqDict d) = SeqDict.toList d.forward |> List.filterMap (\( key, values_ ) -> values_ - |> Set.filter (fn key) + |> SeqSet.filter (fn key) |> normalizeSet |> Maybe.map (Tuple.pair key) ) @@ -372,7 +372,7 @@ filter fn (MultiBiSeqDict d) = contains all key-value pairs which passed the test, and the second contains the pairs that did not. -} -partition : (comparable1 -> Set comparable2 -> Bool) -> MultiBiSeqDict comparable1 comparable2 -> ( MultiBiSeqDict comparable1 comparable2, MultiBiSeqDict comparable1 comparable2 ) +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 @@ -387,7 +387,7 @@ partition fn (MultiBiSeqDict d) = {-| Combine two dictionaries. If there is a collision, preference is given to the first dictionary. -} -union : MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 @@ -397,7 +397,7 @@ union (MultiBiSeqDict left) (MultiBiSeqDict right) = {-| Keep a key-value pair when its key appears in the second dictionary. Preference is given to values in the first dictionary. -} -intersect : MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 @@ -406,7 +406,7 @@ intersect (MultiBiSeqDict left) (MultiBiSeqDict right) = {-| Keep a key-value pair when its key does not appear in the second dictionary. -} -diff : MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 -> MultiBiSeqDict comparable1 comparable2 +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 @@ -425,11 +425,11 @@ you want. -} merge : - (comparable1 -> Set comparable21 -> acc -> acc) - -> (comparable1 -> Set comparable21 -> Set comparable22 -> acc -> acc) - -> (comparable1 -> Set comparable22 -> acc -> acc) - -> MultiBiSeqDict comparable1 comparable21 - -> MultiBiSeqDict comparable1 comparable22 + (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 = diff --git a/src/MultiSeqDict.elm b/src/MultiSeqDict.elm index c9b4985..8acab90 100644 --- a/src/MultiSeqDict.elm +++ b/src/MultiSeqDict.elm @@ -22,7 +22,7 @@ Example usage: |> MultiSeqDict.insert "A" 2 MultiSeqDict.get "A" oneToMany - --> Set.fromList [1, 2] + --> SeqSet.fromList [1, 2] # Dictionaries @@ -64,37 +64,37 @@ Example usage: import SeqDict exposing (SeqDict) import List.Extra -import Set exposing (Set) +import SeqSet exposing (SeqSet) {-| The underlying data structure. Think about it as - type alias MultiSeqDict comparable1 comparable2 = - Dict comparable1 (Set comparable2) -- just a normal Dict! + type alias MultiSeqDict k v = + Dict k (SeqSet v) -- just a normal Dict! -} -type MultiSeqDict comparable1 comparable2 - = MultiSeqDict (SeqDict comparable1 (Set comparable2)) +type MultiSeqDict k v + = MultiSeqDict (SeqDict k (SeqSet v)) {-| Create an empty dictionary. -} -empty : MultiSeqDict comparable1 comparable2 +empty : MultiSeqDict k v empty = MultiSeqDict SeqDict.empty {-| Create a dictionary with one key-value pair. -} -singleton : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 +singleton : k -> v -> MultiSeqDict k v singleton from to = - MultiSeqDict (SeqDict.singleton from (Set.singleton to)) + MultiSeqDict (SeqDict.singleton from (SeqSet.singleton to)) {-| Insert a key-value pair into a dictionary. Replaces value when there is a collision. -} -insert : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +insert : k -> v -> MultiSeqDict k v -> MultiSeqDict k v insert from to (MultiSeqDict d) = MultiSeqDict <| SeqDict.update @@ -102,27 +102,27 @@ insert from to (MultiSeqDict d) = (\maybeSet -> case maybeSet of Nothing -> - Just (Set.singleton to) + Just (SeqSet.singleton to) Just set -> - Just (Set.insert to set) + Just (SeqSet.insert to set) ) d {-| Update the value of a dictionary for a specific key with a given function. -} -update : comparable1 -> (Set comparable2 -> Set comparable2) -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 Set.empty) has the same meaning as Nothing. +{-| In our model, (Just SeqSet.empty) has the same meaning as Nothing. Make it be Nothing! -} -normalizeSet : Set comparable1 -> Maybe (Set comparable1) +normalizeSet : SeqSet k -> Maybe (SeqSet k) normalizeSet set = - if Set.isEmpty set then + if SeqSet.isEmpty set then Nothing else @@ -132,7 +132,7 @@ normalizeSet set = {-| Remove all key-value pairs for the given key from a dictionary. If the key is not found, no changes are made. -} -removeAll : comparable1 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +removeAll : k -> MultiSeqDict k v -> MultiSeqDict k v removeAll from (MultiSeqDict d) = MultiSeqDict (SeqDict.remove from d) @@ -140,10 +140,10 @@ removeAll from (MultiSeqDict d) = {-| Remove a single key-value pair from a dictionary. If the key is not found, no changes are made. -} -remove : comparable1 -> comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +remove : k -> v -> MultiSeqDict k v -> MultiSeqDict k v remove from to (MultiSeqDict d) = MultiSeqDict <| - SeqDict.update from (Maybe.andThen (Set.remove to >> normalizeSet)) d + SeqDict.update from (Maybe.andThen (SeqSet.remove to >> normalizeSet)) d {-| Determine if a dictionary is empty. @@ -151,14 +151,14 @@ remove from to (MultiSeqDict d) = isEmpty empty == True -} -isEmpty : MultiSeqDict comparable1 comparable2 -> Bool +isEmpty : MultiSeqDict k v -> Bool isEmpty (MultiSeqDict d) = SeqDict.isEmpty d {-| Determine if a key is in a dictionary. -} -member : comparable1 -> MultiSeqDict comparable1 comparable2 -> Bool +member : k -> MultiSeqDict k v -> Bool member from (MultiSeqDict d) = SeqDict.member from d @@ -169,22 +169,22 @@ dictionary. animals = fromList [ ("Tom", "cat"), ("Jerry", "mouse") ] - get "Tom" animals == Set.singleton "cat" - get "Jerry" animals == Set.singleton "mouse" - get "Spike" animals == Set.empty + get "Tom" animals == SeqSet.singleton "cat" + get "Jerry" animals == SeqSet.singleton "mouse" + get "Spike" animals == SeqSet.empty -} -get : comparable1 -> MultiSeqDict comparable1 comparable2 -> Set comparable2 +get : k -> MultiSeqDict k v -> SeqSet v get from (MultiSeqDict d) = SeqDict.get from d - |> Maybe.withDefault Set.empty + |> Maybe.withDefault SeqSet.empty {-| Determine the number of key-value pairs in the dictionary. -} -size : MultiSeqDict comparable1 comparable2 -> Int +size : MultiSeqDict k v -> Int size (MultiSeqDict d) = - SeqDict.foldl (\_ set acc -> Set.size set + acc) 0 d + SeqDict.foldl (\_ set acc -> SeqSet.size set + acc) 0 d {-| Get all of the keys in a dictionary, sorted from lowest to highest. @@ -192,7 +192,7 @@ size (MultiSeqDict d) = keys (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ 0, 1 ] -} -keys : MultiSeqDict comparable1 comparable2 -> List comparable1 +keys : MultiSeqDict k v -> List k keys (MultiSeqDict d) = SeqDict.keys d @@ -202,22 +202,22 @@ keys (MultiSeqDict d) = values (fromList [ ( 0, "Alice" ), ( 1, "Bob" ) ]) == [ "Alice", "Bob" ] -} -values : MultiSeqDict comparable1 comparable2 -> List comparable2 +values : MultiSeqDict k v -> List v values (MultiSeqDict d) = SeqDict.values d - |> List.concatMap Set.toList + |> List.concatMap SeqSet.toList {-| Convert a dictionary into an association list of key-value pairs, sorted by keys. -} -toList : MultiSeqDict comparable1 comparable2 -> List ( comparable1, Set comparable2 ) +toList : MultiSeqDict k v -> List ( k, SeqSet v ) toList (MultiSeqDict d) = SeqDict.toList d {-| Convert an association list into a dictionary. -} -fromList : List ( comparable1, Set comparable2 ) -> MultiSeqDict comparable1 comparable2 +fromList : List ( k, SeqSet v ) -> MultiSeqDict k v fromList list = SeqDict.fromList list |> fromDict @@ -234,19 +234,19 @@ fromList list = results in the same dict as fromList - [ ( "foo", Set.fromList [ 1, 3 ] ) - , ( "bar", Set.fromList [ 2 ] ) + [ ( "foo", SeqSet.fromList [ 1, 3 ] ) + , ( "bar", SeqSet.fromList [ 2 ] ) ] -} -fromFlatList : List ( comparable1, comparable2 ) -> MultiSeqDict comparable1 comparable2 +fromFlatList : List ( k, v ) -> MultiSeqDict k v fromFlatList list = list |> List.Extra.gatherEqualsBy Tuple.first |> List.map (\( ( key, _ ) as x, xs ) -> ( key - , Set.fromList <| List.map Tuple.second <| x :: xs + , SeqSet.fromList <| List.map Tuple.second <| x :: xs ) ) |> SeqDict.fromList @@ -255,21 +255,21 @@ fromFlatList list = {-| Apply a function to all values in a dictionary. -} -map : (comparable1 -> comparable21 -> comparable22) -> MultiSeqDict comparable1 comparable21 -> MultiSeqDict comparable1 comparable22 +map : (k -> v1 -> v2) -> MultiSeqDict k v1 -> MultiSeqDict k v2 map fn (MultiSeqDict d) = - MultiSeqDict <| SeqDict.map (\key set -> Set.map (fn key) set) d + MultiSeqDict <| SeqDict.map (\key set -> SeqSet.map (fn key) set) d {-| Convert MultiSeqDict into a SeqDict. (Throw away the reverse mapping.) -} -toDict : MultiSeqDict comparable1 comparable2 -> SeqDict comparable1 (Set comparable2) +toDict : MultiSeqDict k v -> SeqDict k (SeqSet v) toDict (MultiSeqDict d) = d {-| Convert Dict into a MultiSeqDict. (Compute the reverse mapping.) -} -fromDict : SeqDict comparable1 (Set comparable2) -> MultiSeqDict comparable1 comparable2 +fromDict : SeqDict k (SeqSet v) -> MultiSeqDict k v fromDict dict = MultiSeqDict dict @@ -286,7 +286,7 @@ fromDict dict = -- getAges users == [33,19,28] -} -foldl : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiSeqDict comparable1 comparable2 -> acc +foldl : (k -> SeqSet v -> acc -> acc) -> acc -> MultiSeqDict k v -> acc foldl fn zero (MultiSeqDict d) = SeqDict.foldl fn zero d @@ -303,20 +303,20 @@ foldl fn zero (MultiSeqDict d) = -- getAges users == [28,19,33] -} -foldr : (comparable1 -> Set comparable2 -> acc -> acc) -> acc -> MultiSeqDict comparable1 comparable2 -> acc +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 : (comparable1 -> comparable2 -> Bool) -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +filter : (k -> v -> Bool) -> MultiSeqDict k v -> MultiSeqDict k v filter fn (MultiSeqDict d) = SeqDict.toList d |> List.filterMap (\( key, values_ ) -> values_ - |> Set.filter (fn key) + |> SeqSet.filter (fn key) |> normalizeSet |> Maybe.map (Tuple.pair key) ) @@ -327,7 +327,7 @@ filter fn (MultiSeqDict d) = contains all key-value pairs which passed the test, and the second contains the pairs that did not. -} -partition : (comparable1 -> Set comparable2 -> Bool) -> MultiSeqDict comparable1 comparable2 -> ( MultiSeqDict comparable1 comparable2, MultiSeqDict comparable1 comparable2 ) +partition : (k -> SeqSet v -> Bool) -> MultiSeqDict k v -> ( MultiSeqDict k v, MultiSeqDict k v ) partition fn (MultiSeqDict d) = let ( true, false ) = @@ -341,7 +341,7 @@ partition fn (MultiSeqDict d) = {-| Combine two dictionaries. If there is a collision, preference is given to the first dictionary. -} -union : MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +union : MultiSeqDict k v -> MultiSeqDict k v -> MultiSeqDict k v union (MultiSeqDict left) (MultiSeqDict right) = MultiSeqDict <| SeqDict.union left right @@ -349,14 +349,14 @@ union (MultiSeqDict left) (MultiSeqDict 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 comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +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 comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 -> MultiSeqDict comparable1 comparable2 +diff : MultiSeqDict k v -> MultiSeqDict k v -> MultiSeqDict k v diff (MultiSeqDict left) (MultiSeqDict right) = MultiSeqDict <| SeqDict.diff left right @@ -373,11 +373,11 @@ you want. -} merge : - (comparable1 -> Set comparable21 -> acc -> acc) - -> (comparable1 -> Set comparable21 -> Set comparable22 -> acc -> acc) - -> (comparable1 -> Set comparable22 -> acc -> acc) - -> MultiSeqDict comparable1 comparable21 - -> MultiSeqDict comparable1 comparable22 + (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 = From 7af91f8a61c33689cb0c7d8606e4195b8ff2f2f5 Mon Sep 17 00:00:00 2001 From: Charlon Date: Tue, 4 Nov 2025 18:55:44 +0700 Subject: [PATCH 3/8] Add tests for BiSeqDict, MultiSeqDict, and MultiBiSeqDict Add comprehensive tests proving the modules work with non-comparable types: - BiSeqDict tests with custom types and records - MultiSeqDict tests with custom types - MultiBiSeqDict tests with custom types and bidirectional lookups Note: Tests cannot be run due to pre-existing lamdera/codecs dependency issue in the repo. --- tests/AllTests.elm | 23 +++++++++ tests/BiSeqDictTests.elm | 93 +++++++++++++++++++++++++++++++++++ tests/MultiBiSeqDictTests.elm | 91 ++++++++++++++++++++++++++++++++++ tests/MultiSeqDictTests.elm | 81 ++++++++++++++++++++++++++++++ 4 files changed, 288 insertions(+) create mode 100644 tests/AllTests.elm create mode 100644 tests/BiSeqDictTests.elm create mode 100644 tests/MultiBiSeqDictTests.elm create mode 100644 tests/MultiSeqDictTests.elm 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..924d5bc --- /dev/null +++ b/tests/BiSeqDictTests.elm @@ -0,0 +1,93 @@ +module BiSeqDictTests exposing (tests) + +import BiSeqDict exposing (BiSeqDict) +import Expect +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 + } + + +tests : Test +tests = + describe "BiSeqDict" + [ describe "with non-comparable types" + [ 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.getReverse "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.getReverse record dict + in + Expect.equal (SeqSet.size reverseKeys) 2 + ] + , describe "basic operations" + [ test "empty dict has size 0" <| + \() -> + Expect.equal (BiSeqDict.size BiSeqDict.empty) 0 + , test "singleton creates dict with one element" <| + \() -> + let + dict = + BiSeqDict.singleton "key" "value" + in + Expect.equal (BiSeqDict.size dict) 1 + , test "get returns inserted value" <| + \() -> + let + dict = + BiSeqDict.singleton "key" "value" + in + Expect.equal (BiSeqDict.get "key" dict) (Just "value") + , test "remove works" <| + \() -> + let + dict = + BiSeqDict.singleton "key" "value" + |> BiSeqDict.remove "key" + in + Expect.equal (BiSeqDict.size dict) 0 + ] + ] diff --git a/tests/MultiBiSeqDictTests.elm b/tests/MultiBiSeqDictTests.elm new file mode 100644 index 0000000..b6e4505 --- /dev/null +++ b/tests/MultiBiSeqDictTests.elm @@ -0,0 +1,91 @@ +module MultiBiSeqDictTests exposing (tests) + +import Expect +import MultiBiSeqDict exposing (MultiBiSeqDict) +import SeqSet exposing (SeqSet) +import Test exposing (..) + + +{-| Non-comparable custom type -} +type CustomType + = Foo + | Bar + | Baz + + +type alias CustomRecord = + { name : String + , value : Int + } + + +tests : Test +tests = + describe "MultiBiSeqDict" + [ describe "with non-comparable types" + [ test "can create and insert custom types" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo 1 + |> MultiBiSeqDict.insert Foo 2 + |> MultiBiSeqDict.insert Bar 2 + in + Expect.equal (MultiBiSeqDict.size dict) 3 + , test "get returns all values for key" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo 1 + |> MultiBiSeqDict.insert Foo 2 + |> MultiBiSeqDict.insert Bar 3 + + values = + MultiBiSeqDict.get Foo dict + in + Expect.equal (SeqSet.size values) 2 + , test "getReverse returns all keys for value" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo 1 + |> MultiBiSeqDict.insert Foo 2 + |> MultiBiSeqDict.insert Bar 2 + + keys = + MultiBiSeqDict.getReverse 2 dict + in + Expect.equal (SeqSet.size keys) 2 + , test "works with custom records" <| + \() -> + let + dict = + MultiBiSeqDict.empty + |> MultiBiSeqDict.insert Foo { name = "alice", value = 1 } + |> MultiBiSeqDict.insert Bar { name = "bob", value = 2 } + |> MultiBiSeqDict.insert Foo { name = "bob", value = 2 } + + record = + { name = "bob", value = 2 } + + keys = + MultiBiSeqDict.getReverse record dict + in + Expect.equal (SeqSet.size keys) 2 + ] + , describe "basic operations" + [ test "empty dict has size 0" <| + \() -> + Expect.equal (MultiBiSeqDict.size MultiBiSeqDict.empty) 0 + , test "singleton creates dict with one element" <| + \() -> + let + dict = + MultiBiSeqDict.singleton "key" "value" + in + Expect.equal (MultiBiSeqDict.size dict) 1 + ] + ] diff --git a/tests/MultiSeqDictTests.elm b/tests/MultiSeqDictTests.elm new file mode 100644 index 0000000..5e49087 --- /dev/null +++ b/tests/MultiSeqDictTests.elm @@ -0,0 +1,81 @@ +module MultiSeqDictTests exposing (tests) + +import Expect +import MultiSeqDict exposing (MultiSeqDict) +import SeqSet exposing (SeqSet) +import Test exposing (..) + + +{-| Non-comparable custom type -} +type CustomType + = Foo + | Bar + | Baz + + +tests : Test +tests = + describe "MultiSeqDict" + [ describe "with non-comparable types" + [ test "can create and insert custom types" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert Foo 1 + |> MultiSeqDict.insert Foo 2 + |> MultiSeqDict.insert Bar 3 + in + Expect.equal (MultiSeqDict.size dict) 3 + , test "get returns all values for key" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert Foo 1 + |> MultiSeqDict.insert Foo 2 + |> MultiSeqDict.insert Bar 3 + + values = + MultiSeqDict.get Foo dict + in + Expect.equal (SeqSet.size values) 2 + ] + , describe "basic operations" + [ test "empty dict has size 0" <| + \() -> + Expect.equal (MultiSeqDict.size MultiSeqDict.empty) 0 + , test "singleton creates dict with one element" <| + \() -> + let + dict = + MultiSeqDict.singleton "key" "value" + in + Expect.equal (MultiSeqDict.size dict) 1 + , test "insert adds values to existing key" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "key" "value1" + |> MultiSeqDict.insert "key" "value2" + + values = + MultiSeqDict.get "key" dict + in + Expect.equal (SeqSet.size values) 2 + , test "remove single value works" <| + \() -> + let + dict = + MultiSeqDict.empty + |> MultiSeqDict.insert "key" "value1" + |> MultiSeqDict.insert "key" "value2" + |> MultiSeqDict.remove "key" "value1" + + values = + MultiSeqDict.get "key" dict + in + Expect.equal (SeqSet.size values) 1 + ] + ] From 3e6579085fbba70b7f0b8ae039344c75a38391b3 Mon Sep 17 00:00:00 2001 From: Charlon Date: Wed, 5 Nov 2025 11:35:16 +0700 Subject: [PATCH 4/8] Inline list helper functions to remove elm-community/list-extra dependency As suggested by @supermario, inlined gatherEqualsBy and gatherWith into Internal.ListHelpers module to keep dependency tree minimal for a core package. This removes the elm-community/list-extra dependency. --- elm.json | 1 - src/Internal/ListHelpers.elm | 45 ++++++++++++++++++++++++++++++++++++ src/MultiSeqDict.elm | 5 ++-- 3 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 src/Internal/ListHelpers.elm diff --git a/elm.json b/elm.json index a42dccd..126d836 100644 --- a/elm.json +++ b/elm.json @@ -15,7 +15,6 @@ "dependencies": { "elm/bytes": "1.0.8 <= v < 2.0.0", "elm/core": "1.0.0 <= v < 2.0.0", - "elm-community/list-extra": "8.5.1 <= v < 9.0.0", "lamdera/codecs": "1.0.0 <= v < 2.0.0" }, "test-dependencies": { 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/MultiSeqDict.elm b/src/MultiSeqDict.elm index 8acab90..17c91cb 100644 --- a/src/MultiSeqDict.elm +++ b/src/MultiSeqDict.elm @@ -61,9 +61,8 @@ Example usage: -} +import Internal.ListHelpers exposing (gatherEqualsBy) import SeqDict exposing (SeqDict) - -import List.Extra import SeqSet exposing (SeqSet) @@ -242,7 +241,7 @@ results in the same dict as fromFlatList : List ( k, v ) -> MultiSeqDict k v fromFlatList list = list - |> List.Extra.gatherEqualsBy Tuple.first + |> gatherEqualsBy Tuple.first |> List.map (\( ( key, _ ) as x, xs ) -> ( key From 31972515159279f4cf23d99e462edff67cc6e677 Mon Sep 17 00:00:00 2001 From: Charlon Date: Wed, 5 Nov 2025 12:37:22 +0700 Subject: [PATCH 5/8] Add Wire3 codec support and comprehensive README examples - Add encodeBiSeqDict/decodeBiSeqDict to BiSeqDict.elm - Add encodeMultiSeqDict/decodeMultiSeqDict to MultiSeqDict.elm - Add encodeMultiBiSeqDict/decodeMultiBiSeqDict to MultiBiSeqDict.elm - Enhance README with realistic opaque ID type examples - Add many-to-many chat/documents example for MultiBiSeqDict - Document known codec generation issue with Lamdera compiler --- README.md | 131 ++++++++++++++++++++++++++++++++++++++++- src/BiSeqDict.elm | 23 +++++++- src/MultiBiSeqDict.elm | 23 +++++++- src/MultiSeqDict.elm | 22 +++++++ 4 files changed, 196 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 434ba14..86091d7 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.getReverse 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.get 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.get chat1 chatDocuments +--> SeqSet.fromList [doc1, doc2] + +-- Which chats contain doc1? +MultiBiSeqDict.getReverse 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) + +**Note:** Wire3 codec support has been added (`encodeBiSeqDict`, `encodeMultiSeqDict`, `encodeMultiBiSeqDict`) but there appears to be a Lamdera compiler issue with automatic codec wrapper generation for these new types. The functions are implemented correctly (following the SeqDict/SeqSet pattern), but the compiler-generated `w3_encode_*`/`w3_decode_*` wrappers have incorrect signatures. This issue needs to be addressed at the compiler level before these types can be used in Lamdera BackendModel. + + ## 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/src/BiSeqDict.elm b/src/BiSeqDict.elm index 1980928..c5a44c7 100644 --- a/src/BiSeqDict.elm +++ b/src/BiSeqDict.elm @@ -6,6 +6,7 @@ module BiSeqDict exposing , 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,** @@ -59,10 +60,16 @@ Example usage: @docs union, intersect, diff, merge + +# Internal + +@docs encodeBiSeqDict, decodeBiSeqDict + -} +import Bytes.Decode +import Lamdera.Wire3 import SeqDict exposing (SeqDict) - import SeqSet exposing (SeqSet) @@ -419,3 +426,17 @@ merge : -> 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/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm index 2520286..634b981 100644 --- a/src/MultiBiSeqDict.elm +++ b/src/MultiBiSeqDict.elm @@ -6,6 +6,7 @@ module MultiBiSeqDict exposing , keys, values, toList, fromList , map, foldl, foldr, filter, partition , union, intersect, diff, merge + , encodeMultiBiSeqDict, decodeMultiBiSeqDict ) {-| A dictionary mapping unique keys to **multiple** values, which @@ -63,10 +64,16 @@ Example usage: @docs union, intersect, diff, merge + +# Internal + +@docs encodeMultiBiSeqDict, decodeMultiBiSeqDict + -} +import Bytes.Decode +import Lamdera.Wire3 import SeqDict exposing (SeqDict) - import SeqSet exposing (SeqSet) @@ -434,3 +441,17 @@ merge : -> 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 index 17c91cb..78e5935 100644 --- a/src/MultiSeqDict.elm +++ b/src/MultiSeqDict.elm @@ -6,6 +6,7 @@ module MultiSeqDict exposing , 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 @@ -59,9 +60,16 @@ Example usage: @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) @@ -381,3 +389,17 @@ merge : -> 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 From 859635a093180cb18bb076bfb6d96b9bd723e37e Mon Sep 17 00:00:00 2001 From: Charlon Date: Wed, 5 Nov 2025 14:33:26 +0700 Subject: [PATCH 6/8] Expand test coverage for BiSeqDict, MultiSeqDict, and MultiBiSeqDict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - BiSeqDictTests.elm: 93 → 424 lines (45+ tests covering all operations plus bidirectional lookups) - MultiSeqDictTests.elm: 82 → 598 lines (51 tests for one-to-many relationships) - MultiBiSeqDictTests.elm: 91 → 913 lines (70 tests for many-to-many with reverse index consistency) All test suites now match SeqDict/SeqSet coverage level with: - Build, query, transform, and combine operations - Custom type tests (proving no comparable constraint) - Comprehensive fuzz tests for property-based testing - Reverse lookup tests specific to bidirectional types Updated README to confirm Wire3 codec support is working. Co-Authored-By: Claude --- README.md | 2 +- tests/BiSeqDictTests.elm | 468 ++++++++++++++--- tests/MultiBiSeqDictTests.elm | 955 +++++++++++++++++++++++++++++++--- tests/MultiSeqDictTests.elm | 642 ++++++++++++++++++++--- 4 files changed, 1867 insertions(+), 200 deletions(-) diff --git a/README.md b/README.md index 86091d7..23e5a0d 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ All three types: - ✅ Provide O(log n) performance for core operations - ✅ Automatically maintain consistency (removing a key updates all related mappings) -**Note:** Wire3 codec support has been added (`encodeBiSeqDict`, `encodeMultiSeqDict`, `encodeMultiBiSeqDict`) but there appears to be a Lamdera compiler issue with automatic codec wrapper generation for these new types. The functions are implemented correctly (following the SeqDict/SeqSet pattern), but the compiler-generated `w3_encode_*`/`w3_decode_*` wrappers have incorrect signatures. This issue needs to be addressed at the compiler level before these types can be used in Lamdera BackendModel. +**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 diff --git a/tests/BiSeqDictTests.elm b/tests/BiSeqDictTests.elm index 924d5bc..684d52b 100644 --- a/tests/BiSeqDictTests.elm +++ b/tests/BiSeqDictTests.elm @@ -2,11 +2,13 @@ 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 -} +{-| Non-comparable custom type to prove no comparable constraint +-} type CustomType = Foo | Bar @@ -19,75 +21,403 @@ type alias CustomRecord = } +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" - [ describe "with non-comparable types" - [ 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.getReverse "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.getReverse record dict - in - Expect.equal (SeqSet.size reverseKeys) 2 - ] - , describe "basic operations" - [ test "empty dict has size 0" <| - \() -> - Expect.equal (BiSeqDict.size BiSeqDict.empty) 0 - , test "singleton creates dict with one element" <| - \() -> - let - dict = - BiSeqDict.singleton "key" "value" - in - Expect.equal (BiSeqDict.size dict) 1 - , test "get returns inserted value" <| - \() -> - let - dict = - BiSeqDict.singleton "key" "value" - in - Expect.equal (BiSeqDict.get "key" dict) (Just "value") - , test "remove works" <| - \() -> - let - dict = - BiSeqDict.singleton "key" "value" - |> BiSeqDict.remove "key" - in - Expect.equal (BiSeqDict.size dict) 0 - ] + [ 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.getReverse "cat" dict + in + Expect.equal (SeqSet.size result) 1 + , test "getReverse multiple keys" <| + \() -> + let + result = + BiSeqDict.getReverse "cat" animals + in + Expect.equal (SeqSet.size result) 2 + , test "getReverse not found" <| + \() -> + let + result = + BiSeqDict.getReverse "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.getReverse "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.getReverse "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.getReverse "old" dict + + newResult = + BiSeqDict.getReverse "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.getReverse "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.getReverse 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.getReverse 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 index b6e4505..f84c340 100644 --- a/tests/MultiBiSeqDictTests.elm +++ b/tests/MultiBiSeqDictTests.elm @@ -1,12 +1,14 @@ 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 -} +{-| Non-comparable custom type to prove no comparable constraint +-} type CustomType = Foo | Bar @@ -19,73 +21,892 @@ type alias CustomRecord = } +{-| 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" - [ describe "with non-comparable types" - [ test "can create and insert custom types" <| - \() -> - let - dict = - MultiBiSeqDict.empty - |> MultiBiSeqDict.insert Foo 1 - |> MultiBiSeqDict.insert Foo 2 - |> MultiBiSeqDict.insert Bar 2 - in - Expect.equal (MultiBiSeqDict.size dict) 3 - , test "get returns all values for key" <| - \() -> - let - dict = - MultiBiSeqDict.empty - |> MultiBiSeqDict.insert Foo 1 - |> MultiBiSeqDict.insert Foo 2 - |> MultiBiSeqDict.insert Bar 3 - - values = - MultiBiSeqDict.get Foo dict - in - Expect.equal (SeqSet.size values) 2 - , test "getReverse returns all keys for value" <| - \() -> - let - dict = - MultiBiSeqDict.empty - |> MultiBiSeqDict.insert Foo 1 - |> MultiBiSeqDict.insert Foo 2 - |> MultiBiSeqDict.insert Bar 2 - - keys = - MultiBiSeqDict.getReverse 2 dict - in - Expect.equal (SeqSet.size keys) 2 - , test "works with custom records" <| - \() -> - let - dict = - MultiBiSeqDict.empty - |> MultiBiSeqDict.insert Foo { name = "alice", value = 1 } - |> MultiBiSeqDict.insert Bar { name = "bob", value = 2 } - |> MultiBiSeqDict.insert Foo { name = "bob", value = 2 } - - record = - { name = "bob", value = 2 } - - keys = - MultiBiSeqDict.getReverse record dict - in - Expect.equal (SeqSet.size keys) 2 - ] - , describe "basic operations" - [ test "empty dict has size 0" <| - \() -> - Expect.equal (MultiBiSeqDict.size MultiBiSeqDict.empty) 0 - , test "singleton creates dict with one element" <| - \() -> - let - dict = - MultiBiSeqDict.singleton "key" "value" - in - Expect.equal (MultiBiSeqDict.size dict) 1 - ] + [ 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.get "k" dict + + keys = + MultiBiSeqDict.getReverse "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.get "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.get "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.getReverse "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.get "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.get "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.getReverse "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.get "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.getReverse "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.getReverse "v1" dict + + keysForV2 = + MultiBiSeqDict.getReverse "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.get "chat1" chatDocuments + in + Expect.equal (SeqSet.size docs) 2 + , test "get not found returns empty" <| + \() -> + let + docs = + MultiBiSeqDict.get "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.getReverse "doc1" chatDocuments + in + Expect.equal (SeqSet.size chats) 2 + , test "getReverse not found returns empty" <| + \() -> + let + chats = + MultiBiSeqDict.getReverse "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.getReverse "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.getReverse "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.getReverse "old" dict + + newKeys = + MultiBiSeqDict.getReverse "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.getReverse "v1" dict + + keysForV2 = + MultiBiSeqDict.getReverse "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.getReverse 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.get "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.getReverse 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.getReverse 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.getReverse 1 hasTwo + + keysFor1InNoTwo = + MultiBiSeqDict.getReverse 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.get "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.getReverse 1 result + + keysFor2 = + MultiBiSeqDict.getReverse 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.getReverse 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.getReverse 1 result + + keysFor2 = + MultiBiSeqDict.getReverse 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.get Foo dict + + keysFor2 = + MultiBiSeqDict.getReverse 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.getReverse "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.getReverse 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.get 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.getReverse 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.getReverse 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.get 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.get 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.getReverse 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 index 5e49087..1965663 100644 --- a/tests/MultiSeqDictTests.elm +++ b/tests/MultiSeqDictTests.elm @@ -1,81 +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 -} +{-| 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" - [ describe "with non-comparable types" - [ test "can create and insert custom types" <| - \() -> - let - dict = - MultiSeqDict.empty - |> MultiSeqDict.insert Foo 1 - |> MultiSeqDict.insert Foo 2 - |> MultiSeqDict.insert Bar 3 - in - Expect.equal (MultiSeqDict.size dict) 3 - , test "get returns all values for key" <| - \() -> - let - dict = - MultiSeqDict.empty - |> MultiSeqDict.insert Foo 1 - |> MultiSeqDict.insert Foo 2 - |> MultiSeqDict.insert Bar 3 - - values = - MultiSeqDict.get Foo dict - in - Expect.equal (SeqSet.size values) 2 - ] - , describe "basic operations" - [ test "empty dict has size 0" <| - \() -> - Expect.equal (MultiSeqDict.size MultiSeqDict.empty) 0 - , test "singleton creates dict with one element" <| - \() -> - let - dict = - MultiSeqDict.singleton "key" "value" - in - Expect.equal (MultiSeqDict.size dict) 1 - , test "insert adds values to existing key" <| - \() -> - let - dict = - MultiSeqDict.empty - |> MultiSeqDict.insert "key" "value1" - |> MultiSeqDict.insert "key" "value2" - - values = - MultiSeqDict.get "key" dict - in - Expect.equal (SeqSet.size values) 2 - , test "remove single value works" <| - \() -> - let - dict = - MultiSeqDict.empty - |> MultiSeqDict.insert "key" "value1" - |> MultiSeqDict.insert "key" "value2" - |> MultiSeqDict.remove "key" "value1" - - values = - MultiSeqDict.get "key" dict - in - Expect.equal (SeqSet.size values) 1 - ] + [ 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.get "k" dict + in + Expect.equal (SeqSet.size values) 1 + , test "insert" <| + \() -> + let + dict = + MultiSeqDict.insert "k" "v" MultiSeqDict.empty + + values = + MultiSeqDict.get "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.get "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.get "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.get "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.get "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.get "colors" properties + in + Expect.equal (SeqSet.size colors) 2 + , test "get not found returns empty" <| + \() -> + let + shapes = + MultiSeqDict.get "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.get "k" dict + in + Expect.equal (SeqSet.size values) 2 + , test "fromFlatList excludes duplicates" <| + \() -> + let + dict = + MultiSeqDict.fromFlatList + [ ( "k", 1 ) + , ( "k", 1 ) + ] + + values = + MultiSeqDict.get "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.get "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.get "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.get 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.get "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.get 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.get 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.get 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 ] From 449d6861412590423e5c44cc826b143ac2a4ee52 Mon Sep 17 00:00:00 2001 From: Charlon Date: Sat, 29 Nov 2025 17:45:51 +0700 Subject: [PATCH 7/8] Rename APIs per review feedback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - get → getAll for x-to-many (MultiSeqDict, MultiBiSeqDict) - getReverse → getKeys for bidirectional types (BiSeqDict, MultiBiSeqDict) - Add removeValues to MultiSeqDict and MultiBiSeqDict --- README.md | 8 ++-- src/BiSeqDict.elm | 12 ++--- src/MultiBiSeqDict.elm | 45 ++++++++++-------- src/MultiSeqDict.elm | 33 +++++++------ tests/BiSeqDictTests.elm | 20 ++++---- tests/MultiBiSeqDictTests.elm | 88 +++++++++++++++++------------------ tests/MultiSeqDictTests.elm | 34 +++++++------- 7 files changed, 127 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 23e5a0d..ae3973b 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ BiSeqDict.get aliceId userWorkspaces --> Just workspace1 -- Reverse lookup: Who are all members of workspace1? -BiSeqDict.getReverse workspace1 userWorkspaces +BiSeqDict.getKeys workspace1 userWorkspaces --> SeqSet.fromList [aliceId, bobId] ``` @@ -82,7 +82,7 @@ propertyUnits = |> MultiSeqDict.insert property2 unit201 -- Get all units for property1 -MultiSeqDict.get property1 propertyUnits +MultiSeqDict.getAll property1 propertyUnits --> SeqSet.fromList [unit101, unit102] -- Remove a specific unit @@ -116,11 +116,11 @@ chatDocuments = |> MultiBiSeqDict.insert chat2 doc1 -- doc1 is shared! -- What documents are in chat1? -MultiBiSeqDict.get chat1 chatDocuments +MultiBiSeqDict.getAll chat1 chatDocuments --> SeqSet.fromList [doc1, doc2] -- Which chats contain doc1? -MultiBiSeqDict.getReverse doc1 chatDocuments +MultiBiSeqDict.getKeys doc1 chatDocuments --> SeqSet.fromList [chat1, chat2] -- Transfer doc2 from chat1 to chat3 diff --git a/src/BiSeqDict.elm b/src/BiSeqDict.elm index c5a44c7..f1b7167 100644 --- a/src/BiSeqDict.elm +++ b/src/BiSeqDict.elm @@ -1,6 +1,6 @@ module BiSeqDict exposing ( BiSeqDict - , toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList + , toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList , empty, singleton, insert, update, remove , isEmpty, member, get, size , keys, values, toList, fromList @@ -22,7 +22,7 @@ Example usage: |> BiSeqDict.insert "C" 1 |> BiSeqDict.insert "D" 4 - BiSeqDict.getReverse 1 manyToOne + BiSeqDict.getKeys 1 manyToOne --> SeqSet.fromList ["A", "C"] @@ -33,7 +33,7 @@ Example usage: # Differences from Dict -@docs toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList +@docs toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList # Build @@ -203,11 +203,11 @@ get from (BiSeqDict d) = SeqDict.get from d.forward -{-| Get the keys associated with a value. If the value is not found, +{-| Get all keys associated with a value. If the value is not found, return an empty set. -} -getReverse : v -> BiSeqDict k v -> SeqSet k -getReverse to (BiSeqDict d) = +getKeys : v -> BiSeqDict k v -> SeqSet k +getKeys to (BiSeqDict d) = SeqDict.get to d.reverse |> Maybe.withDefault SeqSet.empty diff --git a/src/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm index 634b981..e02e65d 100644 --- a/src/MultiBiSeqDict.elm +++ b/src/MultiBiSeqDict.elm @@ -1,8 +1,8 @@ module MultiBiSeqDict exposing ( MultiBiSeqDict - , toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList - , empty, singleton, insert, update, remove, removeAll - , isEmpty, member, get, size + , 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 @@ -23,10 +23,10 @@ Example usage: |> MultiBiSeqDict.insert "C" 3 |> MultiBiSeqDict.insert "A" 2 - MultiBiSeqDict.get "A" manyToMany + MultiBiSeqDict.getAll "A" manyToMany --> SeqSet.fromList [1, 2] - MultiBiSeqDict.getReverse 2 manyToMany + MultiBiSeqDict.getKeys 2 manyToMany --> SeqSet.fromList ["A", "B"] @@ -37,17 +37,17 @@ Example usage: # Differences from Dict -@docs toDict, fromDict, getReverse, uniqueValues, uniqueValuesCount, toReverseList +@docs toDict, fromDict, getKeys, uniqueValues, uniqueValuesCount, toReverseList # Build -@docs empty, singleton, insert, update, remove, removeAll +@docs empty, singleton, insert, update, remove, removeAll, removeValues # Query -@docs isEmpty, member, get, size +@docs isEmpty, member, getAll, size # Lists @@ -172,6 +172,14 @@ remove from to (MultiBiSeqDict d) = |> 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 @@ -189,28 +197,27 @@ member from (MultiBiSeqDict 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. +{-| Get all values associated with a key. If the key is not found, return +an empty set. animals = fromList [ ("Tom", Cat), ("Jerry", Mouse) ] - get "Tom" animals == Just Cat - get "Jerry" animals == Just Mouse - get "Spike" animals == Nothing + getAll "Tom" animals == SeqSet.singleton Cat + getAll "Jerry" animals == SeqSet.singleton Mouse + getAll "Spike" animals == SeqSet.empty -} -get : k -> MultiBiSeqDict k v -> SeqSet v -get from (MultiBiSeqDict d) = +getAll : k -> MultiBiSeqDict k v -> SeqSet v +getAll from (MultiBiSeqDict d) = SeqDict.get from d.forward |> Maybe.withDefault SeqSet.empty -{-| Get the keys associated with a value. If the value is not found, +{-| Get all keys associated with a value. If the value is not found, return an empty set. -} -getReverse : v -> MultiBiSeqDict k v -> SeqSet k -getReverse to (MultiBiSeqDict d) = +getKeys : v -> MultiBiSeqDict k v -> SeqSet k +getKeys to (MultiBiSeqDict d) = SeqDict.get to d.reverse |> Maybe.withDefault SeqSet.empty diff --git a/src/MultiSeqDict.elm b/src/MultiSeqDict.elm index 78e5935..b50c8d1 100644 --- a/src/MultiSeqDict.elm +++ b/src/MultiSeqDict.elm @@ -1,8 +1,8 @@ module MultiSeqDict exposing ( MultiSeqDict , toDict, fromDict - , empty, singleton, insert, update, remove, removeAll - , isEmpty, member, get, size + , 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 @@ -22,7 +22,7 @@ Example usage: |> MultiSeqDict.insert "C" 3 |> MultiSeqDict.insert "A" 2 - MultiSeqDict.get "A" oneToMany + MultiSeqDict.getAll "A" oneToMany --> SeqSet.fromList [1, 2] @@ -38,12 +38,12 @@ Example usage: # Build -@docs empty, singleton, insert, update, remove, removeAll +@docs empty, singleton, insert, update, remove, removeAll, removeValues # Query -@docs isEmpty, member, get, size +@docs isEmpty, member, getAll, size # Lists @@ -153,6 +153,14 @@ remove from to (MultiSeqDict d) = 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 @@ -170,19 +178,18 @@ member from (MultiSeqDict d) = SeqDict.member from d -{-| 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. +{-| Get all values associated with a key. If the key is not found, return +an empty set. animals = fromList [ ("Tom", "cat"), ("Jerry", "mouse") ] - get "Tom" animals == SeqSet.singleton "cat" - get "Jerry" animals == SeqSet.singleton "mouse" - get "Spike" animals == SeqSet.empty + getAll "Tom" animals == SeqSet.singleton "cat" + getAll "Jerry" animals == SeqSet.singleton "mouse" + getAll "Spike" animals == SeqSet.empty -} -get : k -> MultiSeqDict k v -> SeqSet v -get from (MultiSeqDict d) = +getAll : k -> MultiSeqDict k v -> SeqSet v +getAll from (MultiSeqDict d) = SeqDict.get from d |> Maybe.withDefault SeqSet.empty diff --git a/tests/BiSeqDictTests.elm b/tests/BiSeqDictTests.elm index 684d52b..6990b7a 100644 --- a/tests/BiSeqDictTests.elm +++ b/tests/BiSeqDictTests.elm @@ -219,21 +219,21 @@ reverseTests = BiSeqDict.singleton "Tom" "cat" result = - BiSeqDict.getReverse "cat" dict + BiSeqDict.getKeys "cat" dict in Expect.equal (SeqSet.size result) 1 , test "getReverse multiple keys" <| \() -> let result = - BiSeqDict.getReverse "cat" animals + BiSeqDict.getKeys "cat" animals in Expect.equal (SeqSet.size result) 2 , test "getReverse not found" <| \() -> let result = - BiSeqDict.getReverse "dog" animals + BiSeqDict.getKeys "dog" animals in Expect.equal (SeqSet.size result) 0 , test "getReverse after insert" <| @@ -246,7 +246,7 @@ reverseTests = |> BiSeqDict.insert "k3" "v" result = - BiSeqDict.getReverse "v" dict + BiSeqDict.getKeys "v" dict in Expect.equal (SeqSet.size result) 3 , test "getReverse after remove" <| @@ -257,7 +257,7 @@ reverseTests = |> BiSeqDict.remove "Tom" result = - BiSeqDict.getReverse "cat" dict + BiSeqDict.getKeys "cat" dict in Expect.equal (SeqSet.size result) 1 , test "reverse index consistency after replace" <| @@ -268,10 +268,10 @@ reverseTests = |> BiSeqDict.insert "k" "new" oldResult = - BiSeqDict.getReverse "old" dict + BiSeqDict.getKeys "old" dict newResult = - BiSeqDict.getReverse "new" dict + BiSeqDict.getKeys "new" dict in Expect.equal ( SeqSet.size oldResult, SeqSet.size newResult ) ( 0, 1 ) ] @@ -300,7 +300,7 @@ customTypeTests = |> BiSeqDict.insert Baz "hello" result = - BiSeqDict.getReverse "hello" dict + BiSeqDict.getKeys "hello" dict in Expect.equal (SeqSet.size result) 2 , test "can use custom records as values" <| @@ -316,7 +316,7 @@ customTypeTests = { name = "Alice", value = 1 } reverseKeys = - BiSeqDict.getReverse record dict + BiSeqDict.getKeys record dict in Expect.equal (SeqSet.size reverseKeys) 2 ] @@ -386,7 +386,7 @@ fuzzTests = BiSeqDict.fromList pairs checkConsistency ( k, v ) = - BiSeqDict.getReverse v dict + BiSeqDict.getKeys v dict |> SeqSet.member k in BiSeqDict.toList dict diff --git a/tests/MultiBiSeqDictTests.elm b/tests/MultiBiSeqDictTests.elm index f84c340..f60651f 100644 --- a/tests/MultiBiSeqDictTests.elm +++ b/tests/MultiBiSeqDictTests.elm @@ -69,10 +69,10 @@ buildTests = MultiBiSeqDict.singleton "k" "v" values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict keys = - MultiBiSeqDict.getReverse "v" dict + MultiBiSeqDict.getKeys "v" dict in Expect.equal ( SeqSet.size values, SeqSet.size keys ) ( 1, 1 ) , test "insert" <| @@ -82,7 +82,7 @@ buildTests = MultiBiSeqDict.insert "k" "v" MultiBiSeqDict.empty values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "insert multiple values same key" <| @@ -95,7 +95,7 @@ buildTests = |> MultiBiSeqDict.insert "k" "v3" values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 3 , test "insert multiple keys same value" <| @@ -108,7 +108,7 @@ buildTests = |> MultiBiSeqDict.insert "k3" "v" keys = - MultiBiSeqDict.getReverse "v" dict + MultiBiSeqDict.getKeys "v" dict in Expect.equal (SeqSet.size keys) 3 , test "insert duplicate value" <| @@ -120,7 +120,7 @@ buildTests = |> MultiBiSeqDict.insert "k" "v" values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "update" <| @@ -131,7 +131,7 @@ buildTests = |> MultiBiSeqDict.update "k" (SeqSet.insert "v2") values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 2 , test "update to empty removes key" <| @@ -150,7 +150,7 @@ buildTests = |> MultiBiSeqDict.update "k" (SeqSet.insert "v2") keysForV2 = - MultiBiSeqDict.getReverse "v2" dict + MultiBiSeqDict.getKeys "v2" dict in Expect.equal (SeqSet.member "k" keysForV2) True , test "remove single value" <| @@ -163,7 +163,7 @@ buildTests = |> MultiBiSeqDict.remove "k" "v1" values = - MultiBiSeqDict.get "k" dict + MultiBiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "remove updates reverse index" <| @@ -176,7 +176,7 @@ buildTests = |> MultiBiSeqDict.remove "k1" "v" keys = - MultiBiSeqDict.getReverse "v" dict + MultiBiSeqDict.getKeys "v" dict in Expect.equal (SeqSet.size keys) 1 , test "remove last value removes key" <| @@ -219,10 +219,10 @@ buildTests = |> MultiBiSeqDict.removeAll "k" keysForV1 = - MultiBiSeqDict.getReverse "v1" dict + MultiBiSeqDict.getKeys "v1" dict keysForV2 = - MultiBiSeqDict.getReverse "v2" dict + MultiBiSeqDict.getKeys "v2" dict in Expect.equal ( SeqSet.size keysForV1, SeqSet.size keysForV2 ) ( 1, 0 ) , test "removeAll not found" <| @@ -260,14 +260,14 @@ queryTests = \() -> let docs = - MultiBiSeqDict.get "chat1" chatDocuments + MultiBiSeqDict.getAll "chat1" chatDocuments in Expect.equal (SeqSet.size docs) 2 , test "get not found returns empty" <| \() -> let docs = - MultiBiSeqDict.get "chat99" chatDocuments + MultiBiSeqDict.getAll "chat99" chatDocuments in Expect.equal (SeqSet.isEmpty docs) True , test "size of empty dictionary" <| @@ -288,14 +288,14 @@ reverseTests = \() -> let chats = - MultiBiSeqDict.getReverse "doc1" chatDocuments + MultiBiSeqDict.getKeys "doc1" chatDocuments in Expect.equal (SeqSet.size chats) 2 , test "getReverse not found returns empty" <| \() -> let chats = - MultiBiSeqDict.getReverse "doc99" chatDocuments + MultiBiSeqDict.getKeys "doc99" chatDocuments in Expect.equal (SeqSet.isEmpty chats) True , test "getReverse after insert" <| @@ -308,7 +308,7 @@ reverseTests = |> MultiBiSeqDict.insert "k3" "v" keys = - MultiBiSeqDict.getReverse "v" dict + MultiBiSeqDict.getKeys "v" dict in Expect.equal (SeqSet.size keys) 3 , test "getReverse after remove" <| @@ -321,7 +321,7 @@ reverseTests = |> MultiBiSeqDict.remove "k1" "v" keys = - MultiBiSeqDict.getReverse "v" dict + MultiBiSeqDict.getKeys "v" dict in Expect.equal (SeqSet.size keys) 1 , test "reverse index consistency after replace" <| @@ -332,10 +332,10 @@ reverseTests = |> MultiBiSeqDict.update "k" (\_ -> SeqSet.singleton "new") oldKeys = - MultiBiSeqDict.getReverse "old" dict + MultiBiSeqDict.getKeys "old" dict newKeys = - MultiBiSeqDict.getReverse "new" dict + MultiBiSeqDict.getKeys "new" dict in Expect.equal ( SeqSet.size oldKeys, SeqSet.size newKeys ) ( 0, 1 ) , test "reverse consistency with multiple updates" <| @@ -349,10 +349,10 @@ reverseTests = |> MultiBiSeqDict.remove "k1" "v1" keysForV1 = - MultiBiSeqDict.getReverse "v1" dict + MultiBiSeqDict.getKeys "v1" dict keysForV2 = - MultiBiSeqDict.getReverse "v2" dict + MultiBiSeqDict.getKeys "v2" dict in Expect.equal ( SeqSet.size keysForV1, SeqSet.size keysForV2 ) ( 1, 1 ) , test "uniqueValues returns all unique values" <| @@ -422,7 +422,7 @@ listsTests = ] keysFor3 = - MultiBiSeqDict.getReverse 3 dict + MultiBiSeqDict.getKeys 3 dict in Expect.equal (SeqSet.size keysFor3) 1 , test "toList/fromList roundtrip" <| @@ -452,7 +452,7 @@ transformTests = MultiBiSeqDict.map (\k v -> v * 2) dict values = - MultiBiSeqDict.get "a" mapped + MultiBiSeqDict.getAll "a" mapped |> SeqSet.toList |> List.sort in @@ -469,7 +469,7 @@ transformTests = MultiBiSeqDict.map (\k v -> v * 10) dict keysFor10 = - MultiBiSeqDict.getReverse 10 mapped + MultiBiSeqDict.getKeys 10 mapped in Expect.equal (SeqSet.size keysFor10) 2 , test "foldl" <| @@ -512,7 +512,7 @@ transformTests = MultiBiSeqDict.filter (\k v -> v > 1) dict keysFor1 = - MultiBiSeqDict.getReverse 1 filtered + MultiBiSeqDict.getKeys 1 filtered in Expect.equal (SeqSet.size keysFor1) 0 , test "filter removes empty keys" <| @@ -553,10 +553,10 @@ transformTests = MultiBiSeqDict.partition (\k values -> SeqSet.size values == 2) dict keysFor1InHasTwo = - MultiBiSeqDict.getReverse 1 hasTwo + MultiBiSeqDict.getKeys 1 hasTwo keysFor1InNoTwo = - MultiBiSeqDict.getReverse 1 noTwo + MultiBiSeqDict.getKeys 1 noTwo in Expect.equal ( SeqSet.size keysFor1InHasTwo, SeqSet.size keysFor1InNoTwo ) ( 1, 1 ) ] @@ -595,7 +595,7 @@ combineTests = MultiBiSeqDict.union d1 d2 values = - MultiBiSeqDict.get "a" result + MultiBiSeqDict.getAll "a" result in Expect.equal (SeqSet.size values) 1 , test "union maintains reverse index" <| @@ -613,10 +613,10 @@ combineTests = MultiBiSeqDict.union d1 d2 keysFor1 = - MultiBiSeqDict.getReverse 1 result + MultiBiSeqDict.getKeys 1 result keysFor2 = - MultiBiSeqDict.getReverse 2 result + MultiBiSeqDict.getKeys 2 result in Expect.equal ( SeqSet.size keysFor1, SeqSet.size keysFor2 ) ( 1, 1 ) , test "intersect" <| @@ -651,7 +651,7 @@ combineTests = MultiBiSeqDict.intersect d1 d2 keysFor1 = - MultiBiSeqDict.getReverse 1 result + MultiBiSeqDict.getKeys 1 result in Expect.equal (SeqSet.size keysFor1) 1 , test "diff" <| @@ -686,10 +686,10 @@ combineTests = MultiBiSeqDict.diff d1 d2 keysFor1 = - MultiBiSeqDict.getReverse 1 result + MultiBiSeqDict.getKeys 1 result keysFor2 = - MultiBiSeqDict.getReverse 2 result + MultiBiSeqDict.getKeys 2 result in Expect.equal ( SeqSet.size keysFor1, SeqSet.size keysFor2 ) ( 0, 1 ) , test "merge" <| @@ -731,10 +731,10 @@ customTypeTests = |> MultiBiSeqDict.insert Bar 2 fooValues = - MultiBiSeqDict.get Foo dict + MultiBiSeqDict.getAll Foo dict keysFor2 = - MultiBiSeqDict.getReverse 2 dict + MultiBiSeqDict.getKeys 2 dict in Expect.equal ( SeqSet.size fooValues, SeqSet.size keysFor2 ) ( 2, 2 ) , test "getReverse works with custom types" <| @@ -747,7 +747,7 @@ customTypeTests = |> MultiBiSeqDict.insert Baz "hello" keys = - MultiBiSeqDict.getReverse "hello" dict + MultiBiSeqDict.getKeys "hello" dict in Expect.equal (SeqSet.size keys) 2 , test "can use custom records as values" <| @@ -763,7 +763,7 @@ customTypeTests = { name = "Alice", value = 1 } keys = - MultiBiSeqDict.getReverse record dict + MultiBiSeqDict.getKeys record dict in Expect.equal (SeqSet.size keys) 2 ] @@ -779,7 +779,7 @@ fuzzTests = List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs result = - MultiBiSeqDict.get num dict + MultiBiSeqDict.getAll num dict expected = pairs @@ -795,7 +795,7 @@ fuzzTests = List.foldl (\( k, v ) acc -> MultiBiSeqDict.insert k v acc) MultiBiSeqDict.empty pairs result = - MultiBiSeqDict.getReverse num dict + MultiBiSeqDict.getKeys num dict expected = pairs @@ -813,7 +813,7 @@ fuzzTests = checkConsistency ( k, values ) = SeqSet.foldl (\v acc -> - acc && SeqSet.member k (MultiBiSeqDict.getReverse v dict) + acc && SeqSet.member k (MultiBiSeqDict.getKeys v dict) ) True values @@ -829,7 +829,7 @@ fuzzTests = |> MultiBiSeqDict.insert num num values = - MultiBiSeqDict.get num dict + MultiBiSeqDict.getAll num dict in Expect.equal (SeqSet.member num values) True , fuzz2 fuzzPairs int "remove works" <| @@ -840,7 +840,7 @@ fuzzTests = |> MultiBiSeqDict.remove num num values = - MultiBiSeqDict.get num dict + MultiBiSeqDict.getAll num dict in Expect.equal (SeqSet.member num values) False , fuzz2 fuzzPairs int "remove maintains reverse index" <| @@ -851,7 +851,7 @@ fuzzTests = |> MultiBiSeqDict.remove num num keys = - MultiBiSeqDict.getReverse num dict + MultiBiSeqDict.getKeys num dict in Expect.equal (SeqSet.member num keys) False , fuzz2 fuzzPairs int "removeAll works" <| diff --git a/tests/MultiSeqDictTests.elm b/tests/MultiSeqDictTests.elm index 1965663..abe926b 100644 --- a/tests/MultiSeqDictTests.elm +++ b/tests/MultiSeqDictTests.elm @@ -63,7 +63,7 @@ buildTests = MultiSeqDict.singleton "k" "v" values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "insert" <| @@ -73,7 +73,7 @@ buildTests = MultiSeqDict.insert "k" "v" MultiSeqDict.empty values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "insert multiple values same key" <| @@ -86,7 +86,7 @@ buildTests = |> MultiSeqDict.insert "k" "v3" values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 3 , test "insert duplicate value" <| @@ -98,7 +98,7 @@ buildTests = |> MultiSeqDict.insert "k" "v" values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "update" <| @@ -109,7 +109,7 @@ buildTests = |> MultiSeqDict.update "k" (SeqSet.insert "v2") values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 2 , test "update to empty removes key" <| @@ -130,7 +130,7 @@ buildTests = |> MultiSeqDict.remove "k" "v1" values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "remove last value removes key" <| @@ -197,14 +197,14 @@ queryTests = \() -> let colors = - MultiSeqDict.get "colors" properties + MultiSeqDict.getAll "colors" properties in Expect.equal (SeqSet.size colors) 2 , test "get not found returns empty" <| \() -> let shapes = - MultiSeqDict.get "shapes" properties + MultiSeqDict.getAll "shapes" properties in Expect.equal (SeqSet.isEmpty shapes) True , test "size of empty dictionary" <| @@ -267,7 +267,7 @@ listsTests = ] values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 2 , test "fromFlatList excludes duplicates" <| @@ -280,7 +280,7 @@ listsTests = ] values = - MultiSeqDict.get "k" dict + MultiSeqDict.getAll "k" dict in Expect.equal (SeqSet.size values) 1 , test "toList/fromList roundtrip" <| @@ -315,7 +315,7 @@ transformTests = MultiSeqDict.map (\k v -> v * 2) dict values = - MultiSeqDict.get "a" mapped + MultiSeqDict.getAll "a" mapped |> SeqSet.toList |> List.sort in @@ -402,7 +402,7 @@ combineTests = MultiSeqDict.union d1 d2 values = - MultiSeqDict.get "a" result + MultiSeqDict.getAll "a" result in Expect.equal (SeqSet.size values) 1 , test "intersect" <| @@ -466,7 +466,7 @@ customTypeTests = |> MultiSeqDict.insert Bar 3 fooValues = - MultiSeqDict.get Foo dict + MultiSeqDict.getAll Foo dict in Expect.equal (SeqSet.size fooValues) 2 , test "multiple custom type keys" <| @@ -489,7 +489,7 @@ customTypeTests = |> MultiSeqDict.insert "bob" { name = "Bob", value = 1 } aliceValues = - MultiSeqDict.get "alice" dict + MultiSeqDict.getAll "alice" dict in Expect.equal (SeqSet.size aliceValues) 2 ] @@ -505,7 +505,7 @@ fuzzTests = MultiSeqDict.fromFlatList pairs result = - MultiSeqDict.get num dict + MultiSeqDict.getAll num dict expected = pairs @@ -538,7 +538,7 @@ fuzzTests = |> MultiSeqDict.insert num num values = - MultiSeqDict.get num dict + MultiSeqDict.getAll num dict in Expect.equal (SeqSet.member num values) True , fuzz2 fuzzPairs int "remove works" <| @@ -549,7 +549,7 @@ fuzzTests = |> MultiSeqDict.remove num num values = - MultiSeqDict.get num dict + MultiSeqDict.getAll num dict in Expect.equal (SeqSet.member num values) False , fuzz2 fuzzPairs int "removeAll works" <| From eb5ad258b4160b694643cce8af9769558c165792 Mon Sep 17 00:00:00 2001 From: Charlon Date: Thu, 4 Dec 2025 15:23:00 +0700 Subject: [PATCH 8/8] Fix foldl/foldr docs: insertion order, not key order --- src/BiSeqDict.elm | 24 ++---------------------- src/MultiBiSeqDict.elm | 24 ++---------------------- src/MultiSeqDict.elm | 24 ++---------------------- 3 files changed, 6 insertions(+), 66 deletions(-) diff --git a/src/BiSeqDict.elm b/src/BiSeqDict.elm index f1b7167..44cdd08 100644 --- a/src/BiSeqDict.elm +++ b/src/BiSeqDict.elm @@ -317,34 +317,14 @@ fromDict forward = } -{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. - - - getAges users = - SeqDict.foldl addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [33,19,28] - +{-| 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 from highest key to lowest key. - - - getAges users = - SeqDict.foldr addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [28,19,33] - +{-| 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) = diff --git a/src/MultiBiSeqDict.elm b/src/MultiBiSeqDict.elm index e02e65d..e6c6948 100644 --- a/src/MultiBiSeqDict.elm +++ b/src/MultiBiSeqDict.elm @@ -333,34 +333,14 @@ fromDict forward = } -{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. - - - getAges users = - SeqDict.foldl addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [33,19,28] - +{-| 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 from highest key to lowest key. - - - getAges users = - SeqDict.foldr addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [28,19,33] - +{-| 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) = diff --git a/src/MultiSeqDict.elm b/src/MultiSeqDict.elm index b50c8d1..72f7d20 100644 --- a/src/MultiSeqDict.elm +++ b/src/MultiSeqDict.elm @@ -288,34 +288,14 @@ fromDict dict = MultiSeqDict dict -{-| Fold over the key-value pairs in a dictionary from lowest key to highest key. - - - getAges users = - SeqDict.foldl addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [33,19,28] - +{-| 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 from highest key to lowest key. - - - getAges users = - SeqDict.foldr addAge [] users - - addAge _ user ages = - user.age :: ages - - -- getAges users == [28,19,33] - +{-| 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) =