module Internal.Tools.Hashdict exposing ( Hashdict , empty, singleton, insert, remove, removeKey , isEmpty, member, memberKey, get, size, isEqual , keys, values, toList, fromList , rehash, union, map, update , coder, encode, decoder, softDecoder ) {-| This module abstracts the `Dict` type with one function that assigns a unique identifier for each value based on a function that assigns each value. This allows you to store values based on an externally defined identifier. ## Dictionaries @docs Hashdict ## 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, map, update ## JSON coders @docs coder, encode, decoder, softDecoder -} import FastDict as Dict exposing (Dict) import Internal.Config.Log as Log import Internal.Config.Text as Text import Internal.Tools.Json as Json {-| A dictionary of keys and values where each key is defined by its value. For example, this can be useful when every user is identifiable by their username: import Hashdict exposing (Hashdict) users : Hashdict User users = Hashdict.fromList .name [ User "Alice" 28 1.65 , User "Bob" 19 1.82 , User "Chuck" 33 1.75 ] type alias User = { name : String , age : Int , height : Float } In the example listed above, the users are stored by their username, which means that all you need to know is the value "Alice" to retrieve all the information about them. Additionally, you do not need to specify a key to insert the values. -} type Hashdict a = Hashdict { hash : a -> String , values : Dict String a } {-| Define how Hashdict can be encoded to and decoded from a JSON object. -} coder : (a -> String) -> Json.Coder a -> Json.Coder (Hashdict a) coder f c1 = Json.andThen { name = Text.docs.hashdict.name , description = Text.docs.hashdict.description , forth = -- TODO: Implement fastDictWithFilter function \items -> case List.filter (\( k, v ) -> f v /= k) (Dict.toList items) of [] -> { hash = f, values = items } |> Hashdict |> Json.succeed |> (|>) [] wrongHashes -> wrongHashes |> List.map Tuple.first |> List.map ((++) "Invalid hash") |> List.map Log.log.error |> Json.fail Text.invalidHashInHashdict , back = \(Hashdict h) -> h.values , failure = Text.failures.hashdict } (Json.fastDict c1) {-| Decode a hashdict from a JSON value. To create a hashdict, 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 hashdict. -} decoder : (a -> String) -> Json.Coder a -> Json.Decoder (Hashdict a) decoder f c1 = Json.decode (coder f c1) {-| Create an empty hashdict. -} empty : (a -> String) -> Hashdict a empty hash = Hashdict { hash = hash, values = Dict.empty } {-| Encode a Hashdict 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 : Json.Coder a -> Json.Encoder (Hashdict a) encode c1 (Hashdict h) = Json.encode (coder h.hash c1) (Hashdict h) {-| Convert an association list into a hashdict. -} fromList : (a -> String) -> List a -> Hashdict a fromList hash xs = Hashdict { hash = hash , values = xs |> List.map (\x -> ( hash x, 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 hashdict. -} get : String -> Hashdict a -> Maybe a get k (Hashdict h) = Dict.get k h.values {-| Insert a value into a hashdict. The key is automatically generated by the hash function. If the function generates a collision, it replaces the existing value in the hashdict. -} insert : a -> Hashdict a -> Hashdict a insert v (Hashdict h) = Hashdict { h | values = Dict.insert (h.hash v) v h.values } {-| Since the Hashdict contains a hash function, the == operator does not work simply. Instead, you should use the isEqual operator. -} isEqual : Hashdict a -> Hashdict a -> Bool isEqual h1 h2 = toList h1 == toList h2 {-| Determine if a hashdict is empty. -} isEmpty : Hashdict a -> Bool isEmpty (Hashdict h) = Dict.isEmpty h.values {-| Get all of the hashes in a hashdict, sorted from lowest to highest. -} keys : Hashdict a -> List String keys (Hashdict h) = Dict.keys h.values {-| Map a value on a given key. If the outcome of the function changes the hash, the operation does nothing. -} map : String -> (a -> a) -> Hashdict a -> Hashdict a map key f (Hashdict h) = Hashdict { h | values = Dict.update key (Maybe.map (\value -> let newValue : a newValue = f value in if h.hash newValue == h.hash value then newValue else value ) ) h.values } {-| Determine if a value's hash is in a hashdict. -} member : a -> Hashdict a -> Bool member value (Hashdict h) = Dict.member (h.hash value) h.values {-| Determine if a hash is in a hashdict. -} memberKey : String -> Hashdict a -> Bool memberKey key (Hashdict h) = Dict.member key h.values {-| Remap a hashdict using a new hashing algorithm. -} rehash : (a -> String) -> Hashdict a -> Hashdict a rehash f (Hashdict h) = Hashdict { hash = f , values = h.values |> Dict.values |> List.map (\v -> ( f v, v )) |> Dict.fromList } {-| Remove a value from a hashdict. 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 |> Hashdict.remove (User "Alice" 19 1.82) -} remove : a -> Hashdict a -> Hashdict a remove v (Hashdict h) = Hashdict { h | values = Dict.remove (h.hash v) h.values } {-| Remove a key from a hashdict. If the key is not found, no changes are made. hdict |> Hashdict.removeKey "Alice" -} removeKey : String -> Hashdict a -> Hashdict a removeKey k (Hashdict h) = Hashdict { h | values = Dict.remove k h.values } {-| Create a hashdict with a single key-value pair. -} singleton : (a -> String) -> a -> Hashdict a singleton f v = empty f |> insert v {-| Determine the number of values in a hashdict. -} size : Hashdict a -> Int size (Hashdict h) = Dict.size h.values {-| Decode a hashdict 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 Hashdict using your new hash function. -} softDecoder : (a -> String) -> Json.Coder a -> Json.Decoder (Hashdict a) softDecoder f c1 = c1 |> Json.fastDict |> Json.map { name = Text.docs.hashdict.name , description = Text.docs.hashdict.description , forth = \items -> Hashdict { hash = f, values = items } |> rehash f , back = \(Hashdict h) -> h.values } |> Json.decode {-| Convert a hashdict into an association list of key-value pairs, sorted by keys. -} toList : Hashdict a -> List ( String, a ) toList (Hashdict h) = Dict.toList h.values {-| Combine two hashdicts under the hash function of the first. If there is a collision, preference is given to the first hashdict. -} union : Hashdict a -> Hashdict a -> Hashdict a union (Hashdict h1) hd2 = case rehash h1.hash hd2 of Hashdict h2 -> Hashdict { hash = h1.hash , values = Dict.union h1.values h2.values } {-| Update a dict to maybe contain a value (or not). If the output does not have the originally expected key, it is not updated. -} update : String -> (Maybe a -> Maybe a) -> Hashdict a -> Hashdict a update key f ((Hashdict h) as hd) = case f (get key hd) of Just v -> if h.hash v == key then insert v hd else hd Nothing -> removeKey key hd {-| Get all values stored in the hashdict, in the order of their keys. -} values : Hashdict a -> List a values (Hashdict h) = Dict.values h.values