Add Mashdict
parent
cee2b3a5bb
commit
277c15c7e1
1
elm.json
1
elm.json
|
@ -15,6 +15,7 @@
|
||||||
"Internal.Tools.Encode",
|
"Internal.Tools.Encode",
|
||||||
"Internal.Tools.Hashdict",
|
"Internal.Tools.Hashdict",
|
||||||
"Internal.Tools.Iddict",
|
"Internal.Tools.Iddict",
|
||||||
|
"Internal.Tools.Mashdict",
|
||||||
"Internal.Tools.Timestamp",
|
"Internal.Tools.Timestamp",
|
||||||
"Internal.Tools.VersionControl",
|
"Internal.Tools.VersionControl",
|
||||||
"Internal.Values.Context",
|
"Internal.Values.Context",
|
||||||
|
|
|
@ -0,0 +1,300 @@
|
||||||
|
module Internal.Tools.Mashdict exposing
|
||||||
|
( Mashdict
|
||||||
|
, empty, singleton, insert, remove, removeKey
|
||||||
|
, isEmpty, member, memberKey, get, size, isEqual
|
||||||
|
, keys, values, toList, fromList
|
||||||
|
, rehash, union
|
||||||
|
, encode, decoder, softDecoder
|
||||||
|
)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
|
||||||
|
|
||||||
|
# Mashdict
|
||||||
|
|
||||||
|
A **mashdict**, (short for "maybe mashdict") is a hashdict that uses a hash
|
||||||
|
function that _maybe_ returns a value. In this case, the mashdict exclusively
|
||||||
|
stores values for which the hashing algorithm returns a value, and it ignores
|
||||||
|
the outcome for all other scenarios.
|
||||||
|
|
||||||
|
In general, you are advised to learn more about the
|
||||||
|
[Hashdict](Internal-Tools-Hashdict) before delving into the Mashdict.
|
||||||
|
|
||||||
|
|
||||||
|
## Dictionaries
|
||||||
|
|
||||||
|
@docs Mashdict
|
||||||
|
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
@docs empty, singleton, insert, remove, removeKey
|
||||||
|
|
||||||
|
|
||||||
|
## Query
|
||||||
|
|
||||||
|
@docs isEmpty, member, memberKey, get, size, isEqual
|
||||||
|
|
||||||
|
|
||||||
|
## Lists
|
||||||
|
|
||||||
|
@docs keys, values, toList, fromList
|
||||||
|
|
||||||
|
|
||||||
|
## Transform
|
||||||
|
|
||||||
|
@docs rehash, union
|
||||||
|
|
||||||
|
|
||||||
|
## JSON coders
|
||||||
|
|
||||||
|
@docs encode, decoder, softDecoder
|
||||||
|
|
||||||
|
-}
|
||||||
|
|
||||||
|
import FastDict as Dict exposing (Dict)
|
||||||
|
import Json.Decode as D
|
||||||
|
import Json.Encode as E
|
||||||
|
|
||||||
|
|
||||||
|
{-| A dictionary of keys and values where each key is defined by its value, but
|
||||||
|
a value is not always given. For example, this can be relevant when not all
|
||||||
|
inserted values are relevant:
|
||||||
|
|
||||||
|
import Mashdict exposing (Mashdict)
|
||||||
|
|
||||||
|
users : Mashdict Event
|
||||||
|
users =
|
||||||
|
Mashdict.fromList .location
|
||||||
|
[ Event "Graduation party" 8 (Just "park")
|
||||||
|
, Event "National holiday" 17 Nothing
|
||||||
|
, Event "Local fair" 11 (Just "town square")
|
||||||
|
]
|
||||||
|
|
||||||
|
-- National holiday will be ignored
|
||||||
|
-- because it does not hash
|
||||||
|
type alias Event =
|
||||||
|
{ name : String
|
||||||
|
, participants : Int
|
||||||
|
, location : Maybe String
|
||||||
|
}
|
||||||
|
|
||||||
|
In the example listed above, all events are stored by their specified location,
|
||||||
|
which means that all you need to know is the value "park" to retrieve all the
|
||||||
|
information about the event at the park. As a result of optimization, this means
|
||||||
|
all values without a hash, are filtered out, as we can never query them.
|
||||||
|
|
||||||
|
-}
|
||||||
|
type Mashdict a
|
||||||
|
= Mashdict
|
||||||
|
{ hash : a -> Maybe String
|
||||||
|
, values : Dict String a
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a mashdict from a JSON value. To create a mashdict, you are expected
|
||||||
|
to insert a hash function. If the hash function doesn't properly hash the values
|
||||||
|
as expected, the decoder will fail to decode the mashdict.
|
||||||
|
-}
|
||||||
|
decoder : (a -> Maybe String) -> D.Decoder a -> D.Decoder (Mashdict a)
|
||||||
|
decoder f xDecoder =
|
||||||
|
D.keyValuePairs xDecoder
|
||||||
|
|> D.andThen
|
||||||
|
(\items ->
|
||||||
|
if List.all (\( hash, value ) -> f value == Just hash) items then
|
||||||
|
items
|
||||||
|
|> Dict.fromList
|
||||||
|
|> (\d -> { hash = f, values = d })
|
||||||
|
|> Mashdict
|
||||||
|
|> D.succeed
|
||||||
|
|
||||||
|
else
|
||||||
|
D.fail "Hash function fails to properly hash all values"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create an empty mashdict.
|
||||||
|
-}
|
||||||
|
empty : (a -> Maybe String) -> Mashdict a
|
||||||
|
empty hash =
|
||||||
|
Mashdict { hash = hash, values = Dict.empty }
|
||||||
|
|
||||||
|
|
||||||
|
{-| Encode a Mashdict into a JSON value. Keep in mind that an Elm function
|
||||||
|
cannot be universally converted to JSON, so it is up to you to preserve that
|
||||||
|
hash function!
|
||||||
|
-}
|
||||||
|
encode : (a -> E.Value) -> Mashdict a -> E.Value
|
||||||
|
encode encodeX (Mashdict h) =
|
||||||
|
h.values
|
||||||
|
|> Dict.toList
|
||||||
|
|> List.map (Tuple.mapSecond encodeX)
|
||||||
|
|> E.object
|
||||||
|
|
||||||
|
|
||||||
|
{-| Convert an association list into a mashdict.
|
||||||
|
-}
|
||||||
|
fromList : (a -> Maybe String) -> List a -> Mashdict a
|
||||||
|
fromList hash xs =
|
||||||
|
Mashdict
|
||||||
|
{ hash = hash
|
||||||
|
, values =
|
||||||
|
xs
|
||||||
|
|> List.filterMap (\x -> hash x |> Maybe.map (\hx -> ( hx, x )))
|
||||||
|
|> Dict.fromList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Get the value associated with a hash. If the hash is not found, return
|
||||||
|
`Nothing`. This is useful when you are not sure if a hash will be in the
|
||||||
|
mashdict.
|
||||||
|
-}
|
||||||
|
get : String -> Mashdict a -> Maybe a
|
||||||
|
get k (Mashdict h) =
|
||||||
|
Dict.get k h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Insert a value into a mashdict. The key is automatically generated by the
|
||||||
|
hash function. If the function generates a collision, it replaces the existing
|
||||||
|
value in the mashdict. If the function returns `Nothing`, the value isn't
|
||||||
|
inserted and the original Mashdict is returned.
|
||||||
|
-}
|
||||||
|
insert : a -> Mashdict a -> Mashdict a
|
||||||
|
insert v (Mashdict h) =
|
||||||
|
case h.hash v of
|
||||||
|
Just hash ->
|
||||||
|
Mashdict { h | values = Dict.insert hash v h.values }
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Mashdict h
|
||||||
|
|
||||||
|
|
||||||
|
{-| Determine if a mashdict is empty.
|
||||||
|
-}
|
||||||
|
isEmpty : Mashdict a -> Bool
|
||||||
|
isEmpty (Mashdict h) =
|
||||||
|
Dict.isEmpty h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Since the Hashdict contains a hash function, the == operator does not work
|
||||||
|
simply. Instead, you should use the isEqual operator.
|
||||||
|
-}
|
||||||
|
isEqual : Mashdict a -> Mashdict a -> Bool
|
||||||
|
isEqual h1 h2 =
|
||||||
|
toList h1 == toList h2
|
||||||
|
|
||||||
|
|
||||||
|
{-| Get all of the hashes in a mashdict, sorted from lowest to highest.
|
||||||
|
-}
|
||||||
|
keys : Mashdict a -> List String
|
||||||
|
keys (Mashdict h) =
|
||||||
|
Dict.keys h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Determine if a value's hash is in a mashdict.
|
||||||
|
-}
|
||||||
|
member : a -> Mashdict a -> Bool
|
||||||
|
member value (Mashdict h) =
|
||||||
|
h.hash value
|
||||||
|
|> Maybe.map (\key -> Dict.member key h.values)
|
||||||
|
|> Maybe.withDefault False
|
||||||
|
|
||||||
|
|
||||||
|
{-| Determine if a hash is in a mashdict.
|
||||||
|
-}
|
||||||
|
memberKey : String -> Mashdict a -> Bool
|
||||||
|
memberKey key (Mashdict h) =
|
||||||
|
Dict.member key h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Remap a mashdict using a new hashing algorithm.
|
||||||
|
-}
|
||||||
|
rehash : (a -> Maybe String) -> Mashdict a -> Mashdict a
|
||||||
|
rehash f (Mashdict h) =
|
||||||
|
Mashdict
|
||||||
|
{ hash = f
|
||||||
|
, values =
|
||||||
|
h.values
|
||||||
|
|> Dict.values
|
||||||
|
|> List.filterMap
|
||||||
|
(\v -> Maybe.map (\hash -> ( hash, v )) (f v))
|
||||||
|
|> Dict.fromList
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Remove a value from a mashdict. If the value's hash is found, the key-value
|
||||||
|
pair is removed. If the value's hash is not found, no changes are made.
|
||||||
|
|
||||||
|
hdict |> Mashdict.remove (Event "Graduation party" 8 (Just "park"))
|
||||||
|
|
||||||
|
-}
|
||||||
|
remove : a -> Mashdict a -> Mashdict a
|
||||||
|
remove v (Mashdict h) =
|
||||||
|
case h.hash v of
|
||||||
|
Just hash ->
|
||||||
|
Mashdict { h | values = Dict.remove hash h.values }
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Mashdict h
|
||||||
|
|
||||||
|
|
||||||
|
{-| Remove a key from a mashdict. If the key is not found, no changes are made.
|
||||||
|
|
||||||
|
hdict |> Mashdict.removeKey "park"
|
||||||
|
|
||||||
|
-}
|
||||||
|
removeKey : String -> Mashdict a -> Mashdict a
|
||||||
|
removeKey k (Mashdict h) =
|
||||||
|
Mashdict { h | values = Dict.remove k h.values }
|
||||||
|
|
||||||
|
|
||||||
|
{-| Create a mashdict with a single key-value pair.
|
||||||
|
-}
|
||||||
|
singleton : (a -> Maybe String) -> a -> Mashdict a
|
||||||
|
singleton f v =
|
||||||
|
empty f |> insert v
|
||||||
|
|
||||||
|
|
||||||
|
{-| Determine the number of values in a mashdict.
|
||||||
|
-}
|
||||||
|
size : Mashdict a -> Int
|
||||||
|
size (Mashdict h) =
|
||||||
|
Dict.size h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Decode a mashdict from a JSON value. If you cannot deduce the originally
|
||||||
|
used hash function, (or if you simply do not care) you can use this function to
|
||||||
|
decode and rehash the Mashdict using your new hash function.
|
||||||
|
-}
|
||||||
|
softDecoder : (a -> Maybe String) -> D.Decoder a -> D.Decoder (Mashdict a)
|
||||||
|
softDecoder f xDecoder =
|
||||||
|
D.keyValuePairs xDecoder
|
||||||
|
|> D.map (List.map Tuple.second >> fromList f)
|
||||||
|
|
||||||
|
|
||||||
|
{-| Convert a mashdict into an association list of key-value pairs, sorted by
|
||||||
|
keys.
|
||||||
|
-}
|
||||||
|
toList : Mashdict a -> List ( String, a )
|
||||||
|
toList (Mashdict h) =
|
||||||
|
Dict.toList h.values
|
||||||
|
|
||||||
|
|
||||||
|
{-| Combine two mashdicts under the hash function of the first. If there is a
|
||||||
|
collision, preference is given to the first mashdict.
|
||||||
|
-}
|
||||||
|
union : Mashdict a -> Mashdict a -> Mashdict a
|
||||||
|
union (Mashdict h1) hd2 =
|
||||||
|
case rehash h1.hash hd2 of
|
||||||
|
Mashdict h2 ->
|
||||||
|
Mashdict
|
||||||
|
{ hash = h1.hash
|
||||||
|
, values = Dict.union h1.values h2.values
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
{-| Get all values stored in the mashdict, in the order of their keys.
|
||||||
|
-}
|
||||||
|
values : Mashdict a -> List a
|
||||||
|
values (Mashdict h) =
|
||||||
|
Dict.values h.values
|
|
@ -0,0 +1,204 @@
|
||||||
|
module Test.Tools.Mashdict exposing (..)
|
||||||
|
|
||||||
|
import Expect
|
||||||
|
import Fuzz exposing (Fuzzer)
|
||||||
|
import Internal.Tools.Mashdict as Mashdict exposing (Mashdict)
|
||||||
|
import Internal.Values.Event as Event
|
||||||
|
import Json.Decode as D
|
||||||
|
import Json.Encode as E
|
||||||
|
import Test exposing (..)
|
||||||
|
import Test.Values.Event as TestEvent
|
||||||
|
|
||||||
|
|
||||||
|
fuzzer : (a -> Maybe String) -> Fuzzer a -> Fuzzer (Mashdict a)
|
||||||
|
fuzzer toHash fuz =
|
||||||
|
Fuzz.map (Mashdict.fromList toHash) (Fuzz.list fuz)
|
||||||
|
|
||||||
|
|
||||||
|
eventFuzzer : Fuzzer (Mashdict Event.Event)
|
||||||
|
eventFuzzer =
|
||||||
|
fuzzer .stateKey TestEvent.fuzzer
|
||||||
|
|
||||||
|
|
||||||
|
suite : Test
|
||||||
|
suite =
|
||||||
|
describe "Mashdict"
|
||||||
|
[ describe "empty"
|
||||||
|
[ test "empty isEmpty"
|
||||||
|
(Mashdict.empty identity
|
||||||
|
|> Mashdict.isEmpty
|
||||||
|
|> Expect.equal True
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"Nothing is member"
|
||||||
|
(\event ->
|
||||||
|
Mashdict.empty .stateKey
|
||||||
|
|> Mashdict.member event
|
||||||
|
|> Expect.equal False
|
||||||
|
)
|
||||||
|
, fuzz Fuzz.string
|
||||||
|
"No key is member"
|
||||||
|
(\key ->
|
||||||
|
Mashdict.empty identity
|
||||||
|
|> Mashdict.memberKey key
|
||||||
|
|> Expect.equal False
|
||||||
|
)
|
||||||
|
, fuzz Fuzz.string
|
||||||
|
"Get gets Nothing"
|
||||||
|
(\key ->
|
||||||
|
Mashdict.empty identity
|
||||||
|
|> Mashdict.get key
|
||||||
|
|> Expect.equal Nothing
|
||||||
|
)
|
||||||
|
, test "Size is zero"
|
||||||
|
(Mashdict.empty identity
|
||||||
|
|> Mashdict.size
|
||||||
|
|> Expect.equal 0
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, test "No keys"
|
||||||
|
(Mashdict.empty identity
|
||||||
|
|> Mashdict.keys
|
||||||
|
|> Expect.equal []
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, test "No values"
|
||||||
|
(Mashdict.empty identity
|
||||||
|
|> Mashdict.values
|
||||||
|
|> Expect.equal []
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, test "To list is []"
|
||||||
|
(Mashdict.empty identity
|
||||||
|
|> Mashdict.toList
|
||||||
|
|> Expect.equal []
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, test "From list is empty"
|
||||||
|
([]
|
||||||
|
|> Mashdict.fromList (\x -> x)
|
||||||
|
|> Mashdict.isEqual (Mashdict.empty identity)
|
||||||
|
|> Expect.equal True
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, test "Empty + empty == empty"
|
||||||
|
(Mashdict.empty Maybe.Just
|
||||||
|
|> Mashdict.union (Mashdict.empty Maybe.Just)
|
||||||
|
|> Mashdict.isEqual (Mashdict.empty Maybe.Just)
|
||||||
|
|> Expect.equal True
|
||||||
|
|> always
|
||||||
|
)
|
||||||
|
, fuzz (Fuzz.intRange 0 10)
|
||||||
|
"JSON encode -> JSON decode"
|
||||||
|
(\indent ->
|
||||||
|
Mashdict.empty Just
|
||||||
|
|> Mashdict.encode E.string
|
||||||
|
|> E.encode indent
|
||||||
|
|> D.decodeString (Mashdict.decoder Just D.string)
|
||||||
|
|> Result.map (Mashdict.isEqual (Mashdict.empty Just))
|
||||||
|
|> Expect.equal (Ok True)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, describe "singleton"
|
||||||
|
[ fuzz TestEvent.fuzzer
|
||||||
|
"singleton = empty + insert"
|
||||||
|
(\event ->
|
||||||
|
Mashdict.empty .stateKey
|
||||||
|
|> Mashdict.insert event
|
||||||
|
|> Mashdict.isEqual (Mashdict.singleton .stateKey event)
|
||||||
|
|> Expect.equal True
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"singleton - event = empty"
|
||||||
|
(\event ->
|
||||||
|
Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.remove event
|
||||||
|
|> Mashdict.isEqual (Mashdict.empty (always Nothing))
|
||||||
|
|> Expect.equal True
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"singleton - event (key) = empty"
|
||||||
|
(\event ->
|
||||||
|
case event.stateKey of
|
||||||
|
Just key ->
|
||||||
|
Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.removeKey key
|
||||||
|
|> Mashdict.isEqual (Mashdict.empty .stateKey)
|
||||||
|
|> Expect.equal True
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Expect.pass
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"Only isEmpty when not Nothing"
|
||||||
|
(\event ->
|
||||||
|
Expect.equal
|
||||||
|
(case event.stateKey of
|
||||||
|
Just _ ->
|
||||||
|
False
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
True
|
||||||
|
)
|
||||||
|
(event
|
||||||
|
|> Mashdict.singleton .stateKey
|
||||||
|
|> Mashdict.isEmpty
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"member"
|
||||||
|
(\event ->
|
||||||
|
Expect.equal
|
||||||
|
(case event.stateKey of
|
||||||
|
Just _ ->
|
||||||
|
True
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
False
|
||||||
|
)
|
||||||
|
(Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.member event
|
||||||
|
)
|
||||||
|
)
|
||||||
|
, fuzz2 TestEvent.fuzzer
|
||||||
|
Fuzz.string
|
||||||
|
"memberKey"
|
||||||
|
(\event rkey ->
|
||||||
|
case event.stateKey of
|
||||||
|
Just key ->
|
||||||
|
Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.memberKey key
|
||||||
|
|> Expect.equal True
|
||||||
|
|
||||||
|
Nothing ->
|
||||||
|
Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.memberKey rkey
|
||||||
|
|> Expect.equal False
|
||||||
|
)
|
||||||
|
, fuzz TestEvent.fuzzer
|
||||||
|
"False memberKey"
|
||||||
|
(\event ->
|
||||||
|
if event.stateKey == Just event.roomId then
|
||||||
|
Expect.pass
|
||||||
|
|
||||||
|
else
|
||||||
|
Mashdict.singleton .stateKey event
|
||||||
|
|> Mashdict.memberKey event.roomId
|
||||||
|
|> Expect.equal False
|
||||||
|
)
|
||||||
|
]
|
||||||
|
, describe "JSON"
|
||||||
|
[ fuzz2 eventFuzzer
|
||||||
|
(Fuzz.intRange 0 10)
|
||||||
|
"JSON encode -> JSON decode"
|
||||||
|
(\hashdict indent ->
|
||||||
|
hashdict
|
||||||
|
|> Mashdict.encode Event.encode
|
||||||
|
|> E.encode indent
|
||||||
|
|> D.decodeString (Mashdict.decoder .stateKey Event.decoder)
|
||||||
|
|> Result.map Mashdict.toList
|
||||||
|
|> Expect.equal (Ok <| Mashdict.toList hashdict)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
]
|
Loading…
Reference in New Issue