Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement all the dict extra functions #7

Merged
merged 6 commits into from
Apr 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions src/DictList.elm
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ module DictList
-- Conversion
, toDict
, fromDict
-- dict extra
, groupBy
, fromListBy
, removeWhen
, removeMany
, keepOnly
, mapKeys
)

{-| Have you ever wanted a `Dict`, but you need to maintain an arbitrary
Expand Down Expand Up @@ -115,12 +122,15 @@ between an association list and a `DictList` via `toList` and `fromList`.
# JSON

@docs decodeObject, decodeArray, decodeWithKeys, decodeKeysAndValues

@docs groupBy, fromListBy, removeWhen, removeMany, keepOnly, mapKeys
-}

import Dict exposing (Dict)
import DictList.Compat exposing (customDecoder, decodeAndThen, first, maybeAndThen, second)
import Json.Decode exposing (Decoder, keyValuePairs, value, decodeValue)
import List.Extra
import Set exposing (Set)


{-| A `Dict` that maintains an arbitrary ordering of keys (rather than sorting
Expand Down Expand Up @@ -937,6 +947,77 @@ fromDict dict =
DictList dict (Dict.keys dict)


{-| Takes a key-fn and a list.
Creates a `Dict` which maps the key to a list of matching elements.
mary = {id=1, name="Mary"}
jack = {id=2, name="Jack"}
jill = {id=1, name="Jill"}
groupBy .id [mary, jack, jill] == DictList.fromList [(1, [mary, jill]), (2, [jack])]
-}
groupBy : (a -> comparable) -> List a -> DictList comparable (List a)
groupBy keyfn list =
List.foldr
(\x acc ->
update (keyfn x) (Maybe.map ((::) x) >> Maybe.withDefault [ x ] >> Just) acc
)
empty
list


{-| Create a dictionary from a list of values, by passing a function that can get a key from any such value.
If the function does not return unique keys, earlier values are discarded.
This can, for instance, be useful when constructing Dicts from a List of records with `id` fields:
mary = {id=1, name="Mary"}
jack = {id=2, name="Jack"}
jill = {id=1, name="Jill"}
fromListBy .id [mary, jack, jill] == DictList.fromList [(1, jack), (2, jill)]
-}
fromListBy : (a -> comparable) -> List a -> DictList comparable a
fromListBy keyfn xs =
List.foldl
(\x acc -> insert (keyfn x) x acc)
empty
xs


{-| Remove elements which satisfies the predicate.
removeWhen (\_ v -> v == 1) (DictList.fromList [("Mary", 1), ("Jack", 2), ("Jill", 1)]) == DictList.fromList [("Jack", 2)]
-}
removeWhen : (comparable -> v -> Bool) -> DictList comparable v -> DictList comparable v
removeWhen pred dict =
filter (\k v -> not (pred k v)) dict


{-| Remove a key-value pair if its key appears in the set.
-}
removeMany : Set comparable -> DictList comparable v -> DictList comparable v
removeMany set dict =
Set.foldl (\k acc -> remove k acc) dict set


{-| Keep a key-value pair if its key appears in the set.
-}
keepOnly : Set comparable -> DictList comparable v -> DictList comparable v
keepOnly set dict =
Set.foldl
(\k acc ->
Maybe.withDefault acc <| Maybe.map (\v -> insert k v acc) (get k dict)
)
empty
set


{-| Apply a function to all keys in a dictionary
-}
mapKeys : (comparable1 -> comparable2) -> DictList comparable1 v -> DictList comparable2 v
mapKeys keyMapper dict =
let
addKey key value d =
insert (keyMapper key) value d
in
foldl addKey empty dict



-----------
-- Internal
Expand Down
131 changes: 131 additions & 0 deletions tests/src/DictExtraPackageTests.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
module DictExtraPackageTests exposing (tests)

import Test.Runner.Node exposing (run, TestProgram)
import Test exposing (Test, describe, test, fuzz, fuzz2)
import Fuzz exposing (Fuzzer, intRange)
import Expect
import Json.Encode exposing (Value)
import Dict
import DictList exposing (..)
import Set


tests : Test
tests =
describe "Dict tests"
[ groupByTests
, fromListByTests
, removeWhenTests
, removeManyTests
, keepOnlyTests
, mapKeysTests
]



-- groupBy


groupByTests : Test
groupByTests =
describe "groupBy"
[ test "example" <|
\() ->
DictList.toList (groupBy .id [ mary, jack, jill ])
|> Expect.equal [ ( 1, [ mary, jill ] ), ( 2, [ jack ] ) ]
]


type alias GroupByData =
{ id : Int
, name : String
}


mary : GroupByData
mary =
GroupByData 1 "Mary"


jack : GroupByData
jack =
GroupByData 2 "Jack"


jill : GroupByData
jill =
GroupByData 1 "Jill"



-- fromListBy


fromListByTests : Test
fromListByTests =
describe "fromListBy"
[ test "example" <|
\() ->
fromListBy .id [ jack, jill ]
|> Expect.equal (DictList.fromList [ ( 2, jack ), ( 1, jill ) ])
, test "replacement" <|
\() ->
fromListBy .id [ jack, jill, mary ]
|> Expect.equal (DictList.fromList [ ( 2, jack ), ( 1, mary ) ])
]



-- removeWhen


removeWhenTests : Test
removeWhenTests =
describe "removeWhen"
[ test "example" <|
\() ->
removeWhen (\_ v -> v == 1) (DictList.fromList [ ( "Mary", 1 ), ( "Jack", 2 ), ( "Jill", 1 ) ])
|> Expect.equal (DictList.fromList [ ( "Jack", 2 ) ])
]



-- removeMany


removeManyTests : Test
removeManyTests =
describe "removeMany"
[ test "example" <|
\() ->
removeMany (Set.fromList [ "Mary", "Jill" ]) (DictList.fromList [ ( "Mary", 1 ), ( "Jack", 2 ), ( "Jill", 1 ) ])
|> Expect.equal (DictList.fromList [ ( "Jack", 2 ) ])
]



-- keepOnly


keepOnlyTests : Test
keepOnlyTests =
describe "keepOnly"
[ test "example" <|
\() ->
keepOnly (Set.fromList [ "Jack", "Jill" ]) (DictList.fromList [ ( "Mary", 1 ), ( "Jack", 2 ), ( "Jill", 1 ) ])
|> Expect.equal (DictList.fromList [ ( "Jack", 2 ), ( "Jill", 1 ) ])
]



-- mapKeys


mapKeysTests : Test
mapKeysTests =
describe "mapKeys"
[ test "example" <|
\() ->
mapKeys ((+) 1) (DictList.fromList [ ( 1, "Jack" ), ( 2, "Jill" ) ])
|> Expect.equal (DictList.fromList [ ( 2, "Jack" ), ( 3, "Jill" ) ])
]
98 changes: 98 additions & 0 deletions tests/src/DictExtraTests.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
module DictExtraTests exposing (tests)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks pretty good. I'd also suggest having a separate test file that copies https://github.com/elm-community/dict-extra/blob/1.3.2/tests/Main.elm and makes whatever the smallest changes are needed to match our context here. Basically, to prove that we pass Dict.Extra's own tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've done that.

import Fuzz exposing (Fuzzer)
import DictList exposing (..)
import Expect
import Test exposing (..)
import Set


{-| Fuzz a DictList, given a fuzzer for the keys and values.
-}
fuzzDictList : Fuzzer comparable -> Fuzzer value -> Fuzzer (DictList comparable value)
fuzzDictList fuzzKey fuzzValue =
Fuzz.tuple ( fuzzKey, fuzzValue )
|> Fuzz.list
|> Fuzz.map DictList.fromList


fuzzIntList : Fuzzer (List Int)
fuzzIntList =
Fuzz.list Fuzz.int


threeElementList =
(fromList [ ( 1, 1 ), ( 2, 2 ), ( 3, 3 ) ])


tests =
describe "List Tests"
[ dictExtraUnitTests
, dictExtraFuzzTests
]


dictExtraUnitTests : Test
dictExtraUnitTests =
describe "Dict Extra Unittests"
[ describe "groupBy"
[ test "empty" <| \() -> Expect.equal (groupBy identity []) empty
, test "always equal elements" <| \() -> Expect.equal (groupBy (always 1) [ 1, 2, 3 ]) (fromList [ ( 1, [ 1, 2, 3 ] ) ])
, test "map to original key" <| \() -> Expect.equal (groupBy identity [ 1, 2, 3 ]) (fromList [ ( 3, [ 3 ] ), ( 2, [ 2 ] ), ( 1, [ 1 ] ) ])
, test "odd-even" <| \() -> Expect.equal (groupBy (\v -> v % 2) [ 1, 2, 3 ]) (fromList [ ( 1, [ 1, 3 ] ), ( 0, [ 2 ] ) ])
]
, describe "fromListBy"
[ test "empty" <| \() -> Expect.equal (fromListBy identity []) empty
, test "simple list" <| \() -> Expect.equal (fromListBy (\v -> v + 1) [ 1, 2, 3 ]) (fromList [ ( 2, 1 ), ( 3, 2 ), ( 4, 3 ) ])
]
, describe "removeWhen"
[ test "empty" <| \() -> Expect.equal (removeWhen (\k v -> True) empty) empty
, test "remove all" <| \() -> Expect.equal (removeWhen (\k v -> True) (fromList [ ( 1, 1 ), ( 2, 2 ), ( 3, 3 ) ])) empty
, test "remove none" <| \() -> Expect.equal (removeWhen (\k v -> False) (fromList [ ( 1, 1 ), ( 2, 2 ), ( 3, 3 ) ])) (fromList [ ( 1, 1 ), ( 2, 2 ), ( 3, 3 ) ])
]
, describe "removeMany"
[ test "empty" <| \() -> Expect.equal (removeMany (Set.fromList [ 1, 2 ]) empty) empty
, test "remove none element" <| \() -> Expect.equal (removeMany (Set.fromList [ 4 ]) threeElementList) threeElementList
, test "remove one element" <| \() -> Expect.equal (removeMany (Set.fromList [ 1 ]) threeElementList) (DictList.filter (\k v -> k /= 1) threeElementList)
, test "remove two elements" <| \() -> Expect.equal (removeMany (Set.fromList [ 1, 2 ]) threeElementList) (DictList.filter (\k v -> k == 3) threeElementList)
, test "remove all elements" <| \() -> Expect.equal (removeMany (Set.fromList [ 1, 2, 3 ]) threeElementList) empty
]
, describe "keepOnly"
[ test "empty" <| \() -> Expect.equal (keepOnly (Set.fromList [ 1, 2 ]) empty) empty
, test "keep none element" <| \() -> Expect.equal (removeMany (Set.fromList [ 4 ]) threeElementList) threeElementList
, test "keep one element" <| \() -> Expect.equal (keepOnly (Set.fromList [ 1 ]) threeElementList) (DictList.filter (\k v -> k == 1) threeElementList)
, test "keep two elements" <| \() -> Expect.equal (keepOnly (Set.fromList [ 1, 2 ]) threeElementList) (DictList.filter (\k v -> k /= 3) threeElementList)
, test "keep all elements" <| \() -> Expect.equal (keepOnly (Set.fromList [ 1, 2, 3 ]) threeElementList) threeElementList
]
, describe "mapKeys"
[ test "empty" <| \() -> Expect.equal (mapKeys toString empty) empty
, test "toString mapping" <| \() -> Expect.equal (mapKeys toString threeElementList) (fromList [ ( "1", 1 ), ( "2", 2 ), ( "3", 3 ) ])
]
]


dictExtraFuzzTests : Test
dictExtraFuzzTests =
-- @TODO Expand the fuzz tests
describe "Dict extra fuzz tests"
[ fuzz fuzzIntList "groupBy (total length doesn't change)" <|
\subject ->
Expect.equal (List.length subject)
(groupBy (\v -> v % 2) subject
|> toList
|> List.map (\( k, v ) -> List.length v)
|> List.foldr (+) 0
)
, fuzz fuzzIntList "groupBy (no elements dissapear)" <|
\subject ->
Expect.equal
(Set.diff (Set.fromList subject)
(Set.fromList
(groupBy (\v -> v % 2) subject
|> toList
|> List.foldr (\( k, v ) agg -> List.append v agg) []
)
)
)
(Set.fromList [])
]
10 changes: 5 additions & 5 deletions tests/src/ListTests.elm
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ tests =

toDictList : List comparable -> DictList comparable comparable
toDictList =
List.map (\a -> (a, a)) >> DictList.fromList
List.map (\a -> ( a, a )) >> DictList.fromList


testListOfN : Int -> Test
Expand All @@ -40,7 +40,7 @@ testListOfN n =
-- assume foldl and (::) work
zs =
List.range 0 n
|> List.map (\a -> (a, a))
|> List.map (\a -> ( a, a ))
|> DictList.fromList

sumSeq k =
Expand Down Expand Up @@ -77,7 +77,7 @@ testListOfN n =
if n == 0 then
Expect.equal (Nothing) (head xs)
else
Expect.equal (Just (1, 1)) (head xs)
Expect.equal (Just ( 1, 1 )) (head xs)
, describe "List.filter"
[ test "none" <| \() -> Expect.equal (empty) (DictList.filter (\_ x -> x > n) xs)
, test "one" <| \() -> Expect.equal [ n ] (values <| DictList.filter (\_ z -> z == n) zs)
Expand All @@ -95,8 +95,8 @@ testListOfN n =
, test "all" <| \() -> Expect.equal (empty) (drop n xs)
, test "all+" <| \() -> Expect.equal (empty) (drop (n + 1) xs)
]
-- append works differently in `DictList` because it overwrites things with the same keys
, test "append" <| \() -> Expect.equal (xsSum {- * 2-}) (append xs xs |> foldl (always (+)) 0)
-- append works differently in `DictList` because it overwrites things with the same keys
, test "append" <| \() -> Expect.equal (xsSum {- * 2 -}) (append xs xs |> foldl (always (+)) 0)
, test "cons" <| \() -> Expect.equal (values <| append (toDictList [ -1 ]) xs) (values <| cons -1 -1 xs)
, test "List.concat" <| \() -> Expect.equal (append xs (append zs xs)) (DictList.concat [ xs, zs, xs ])
, describe "partition"
Expand Down
4 changes: 4 additions & 0 deletions tests/src/Main.elm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ port module Main exposing (..)

import DictTests
import DictListTests
import DictExtraTests
import DictExtraPackageTests
import Json.Encode exposing (Value)
import ListTests
import Test exposing (..)
Expand All @@ -20,5 +22,7 @@ all =
describe "All tests"
[ DictListTests.tests
, DictTests.tests
, DictExtraTests.tests
, DictExtraPackageTests.tests
, ListTests.tests
]