lue-bird/elm-keysset/2.1.0

NOTE: the latest version is 5.0.2

KeysSet

type KeysSet element

🦄 Unsorted data structure that lets you specify aspects that are checked to be unique across all elements

countries : KeysSet { flag : String, code : String, name : String }
countries =
    KeysSet.promising
        [ unique .flag, unique .code ]
        |> KeysSet.insertList
            [ { flag = "🇦🇺", code = "AU", name = "Australia" }
            , { flag = "🇦🇶", code = "AQ", name = "Antarctica" }
            , { flag = "🇱🇧", code = "LB", name = "Lebanon" }
            ]


-- aspect to check for matches + key → matching element
countries |> KeysSet.element ( .flag, "🇦🇶" )
--> Just { flag = "🇦🇶", code = "AQ", name = "Antarctica" }


countries |> KeysSet.element ( .code, "LB" )
--> Just { flag = "🇱🇧", code = "LB", name = "Lebanon" }

create

promising : List (Uniqueness element) -> KeysSet element

A KeysSet with no elements inside, promising that given aspects are unique across all elements. See Uniqueness

KeysSet.promising [ unique .email ]
    |> KeysSet.insert
        { username = "ben", email = "[email protected]" }
    |> KeysSet.insert
        { username = "mai", email = "[email protected]" }
    -- not inserted
    -- There's already an element where .email is "[email protected]"

Elements that are inserted must not contain functions, json or regexes. Elm will crash trying to see if they are equal

unique or not?

type alias Uniqueness value =
value -> value -> { areUnique : Bool }

A check on whether 2 values are equal in some aspect. See unique

uniqueInCasedLetter =
    [ unique .inAlphabet
    , unique .lowercase
    , unique .uppercase
    ]


KeysSet.promising uniqueInCasedLetter
    |> KeysSet.insert
        { inAlphabet = 0, lowercase = 'a', uppercase = 'A' }
    |> KeysSet.insert
        { inAlphabet = 0, lowercase = 'b', uppercase = 'B' }
    -- not inserted
    -- There's already an element where .inAlphabet is 0
unique : (value -> aspectToCheckForStructuralEquality_) -> Uniqueness value

Check values on whether a given aspect is the same structurally

unique .name
    { name = "smile", symbol = '😊' }
    { symbol = '🙂', name = "smile" }
--> { areUnique = False }


unique .symbol
    { name = "smile", symbol = '😊' }
    { symbol = '🙂', name = "smile" }
--> { areUnique = True }


unique (\person -> ( person.firstName, person.lastName ))
    { lastName = "jimmy", firstName = "petter" }
    { lastName = "jimmy", firstName = "greg" }
--> { areUnique = True }

in KeysSet

KeysSet.promising [ unique .email ]
    |> KeysSet.insert
        { username = "ben", email = "[email protected]" }
    |> KeysSet.insert
        { username = "mai", email = "[email protected]" }
    -- not inserted
    -- There's already an element where .email is "[email protected]"

scan

isEqualTo : KeysSet element -> KeysSet element -> Bool

How can you know if each element in aKeysSet can also be found in bKeysSet?

letterCodes : KeysSet { letter : Char, code : Int }
letterCodes =
    KeysSet.insertList
        [ { letter = 'a', code = 97 }
        , { letter = 'b', code = 98 }
        ]
        (KeysSet.promising
            [ unique .letter, unique .code ]
        )


fancyCompetingLetterCodes : KeysSet { letter : Char, code : Int }
fancyCompetingLetterCodes =
    KeysSet.promising
        [ unique .code, unique .letter ]
        |> KeysSet.insert { code = 98, letter = 'b' }
        |> KeysSet.insert { code = 97, letter = 'a' }


letterCodes == fancyCompetingLetterCodes
-- elm crashes
-- because a `KeysSet`'s `Uniqueness` is defined as functions


(letterCodes |> KeysSet.toList)
    == (fancyCompetingLetterCodes |> KeysSet.toList)
-- both contain the same elements
-- but in a different order
--> False


letterCodes
    |> KeysSet.isEqualTo fancyCompetingLetterCodes
--> True

→ Don't use == to compare KeysSets

The keys can be non-comparable. There is no obvious order. → You shouldn't rely on order when using functions like fold or toList

runtime: length^2

isEmpty : KeysSet element_ -> Bool

Whether there are no elements inside

KeysSet.promising [ unique .name ]
    |> KeysSet.isEmpty
--> True


KeysSet.promising [ unique .name ]
    |> KeysSet.insertList []
    |> KeysSet.isEmpty
--> True


KeysSet.promising [ unique .name ]
    |> KeysSet.insert { name = "pete" }
    |> KeysSet.isEmpty
--> False
element : ( element -> aspect, aspect ) -> KeysSet element -> Maybe element

Try to find an element where a given aspect matches a given value

casedLetters =
    KeysSet.promising
        [ unique .lowercase, unique .uppercase ]
        |> KeysSet.insertList
            [ { lowercase = 'a', uppercase = 'A' }
            , { lowercase = 'b', uppercase = 'B' }
            ]


lowercase char =
    casedLetters
        |> KeysSet.element .uppercase char
        |> Maybe.map .lowercase


uppercase char =
    casedLetters
        |> KeysSet.element .lowercase char
        |> Maybe.map .uppercase

If the given aspect isn't promised to be unique, at will find the most recently inserted element where the given aspect matches the given value

ratedOperators : KeysSet { rating : Float, symbol : String, name : String }
ratedOperators =
    KeysSet.promising
        [ unique .symbol, unique .name ]
        |> KeysSet.insertList
            [ { rating = 0.5, symbol = "<", name = "lt" }
            , { rating = 0.5, symbol = ">", name = "gt" }
            ]


ratedOperators
    |> KeysSet.element ( .rating, 0.5 )
--> { rating = 0.5, symbol = ">", name = "gt" }
-->     |> Just

runtime: length

size : KeysSet element_ -> Int

How many elements there are

KeysSet.promising [ unique identity ]
    |> KeysSet.insertList (List.range 0 41)
    |> KeysSet.size
--> 42
isUniqueIn : KeysSet element -> element -> Bool

Whether this element is considered unique / would be inserted

letters : KeysSet { lowercase : Char, uppercase : Char }
letters =
    KeysSet.promising
        [ unique .lowercase, unique .uppercase ]
        |> KeysSet.insertList
            [ { lowercase = 'a', uppercase = 'A' }
            , { lowercase = 'b', uppercase = 'B' }
            ]


KeysSet.isUniqueIn letters
    { lowercase = 'b', uppercase = 'C' }
-- .lowercase 'b' already exists
--> False


KeysSet.isUniqueIn letters
    { lowercase = 'c', uppercase = 'A' }
-- .uppercase 'A' already exists
--> False


KeysSet.isUniqueIn letters
    { lowercase = 'c', uppercase = 'C' }
--> True

runtime: length

all : (element -> Bool) -> KeysSet element -> Bool

Whether all elements pass a given test.

KeysSet.promising
    [ unique .username, unique .email ]
    |> KeysSet.insertList
        [ { username = "fred", priority = 1, email = "[email protected]" }
        , { username = "gria", priority = 3, email = "[email protected]" }
        ]
    |> KeysSet.all (\user -> user.priority < 4)
--> True
any : (element -> Bool) -> KeysSet element -> Bool

Whether there are least some elements that pass a given test.

KeysSet.promising
    [ unique .username, unique .email ]
    |> KeysSet.insertList
        [ { username = "fred", priority = 1, email = "[email protected]" }
        , { username = "gria", priority = 3, email = "[email protected]" }
        ]
    |> KeysSet.any (\user -> user.priority > 4)
--> False


member needle =
    KeysSet.any ((==) needle)
uniqueness : KeysSet element -> List (Uniqueness element)

What Uniqueness the KeysSet is promising to check for.

KeysSet.promising [ unique .open, unique .closed ]
    |> KeysSet.uniqueness
--→ [ unique .open, unique .closed ]

This is rarely useful! Some examples:

alter change =
    \keysSet ->
        keysSet |> KeysSet.map change (keysSet |> uniqueness)


where isOk =
    \keysSet ->
        keysSet
            |> KeysSet.mapTry
                (\element ->
                    if element |> isOk then
                        element |> Just


                    else
                        Nothing
                )
                (keysSet |> uniqueness)

altering

insert : element -> KeysSet element -> KeysSet element

Put an element into the KeysSet.

If there is already an element where some aspect that is promised to be unique is equal (see Uniqueness), the KeysSet remains unchanged

KeysSet.promising
    [ unique .lowercase, unique .uppercase ]
    |> KeysSet.insert
        { lowercase = 'b', uppercase = 'B', rating = 0.5 }
        -- is inserted
    |> KeysSet.insert
        { lowercase = 'a', uppercase = 'A', rating = 0.5 }
        -- is inserted
        -- .rating is not specified as unique
    |> KeysSet.insert
        { lowercase = 'b', uppercase = 'C', rating = 0 }
        -- is ignored
        -- .lowercase 'b' already exists
    |> KeysSet.insert
        { lowercase = 'c', uppercase = 'A', rating = 0 }
        -- is ignored
        -- .uppercase 'A' already exists
    |> KeysSet.insert
        { lowercase = 'c', uppercase = 'C', rating = 0.6 }
        -- is inserted
    |> KeysSet.isEqualTo
        (KeysSet.promising
            [ unique .lowercase, unique .uppercase ]
            |> KeysSet.insertList
                [ { lowercase = 'b', uppercase = 'B', rating = 0.5 }
                , { lowercase = 'a', uppercase = 'A', rating = 0.5 }
                , { lowercase = 'c', uppercase = 'C', rating = 0.6 }
                ]
        )
--> True

runtime: length

insertList : List element -> KeysSet element -> KeysSet element

Conveniently insert a List of elements

KeysSet.promising
    [ unique .open, unique .closed ]
    |> KeysSet.insertList
        [ { open = '(', closed = ')' }
        , { open = '{', closed = '}' }
        ]
    |> KeysSet.isEqualTo
        (KeysSet.promising [ unique .open, unique .closed ]
            |> KeysSet.insert { open = '(', closed = ')' }
            |> KeysSet.insert { open = '{', closed = '}' }
        )
--> True

runtime: setLength * listLength

elementRemove : ( element -> aspect, aspect ) -> KeysSet element -> KeysSet element

Remove the element where a given aspect of the element matches a given value

openClosedBrackets : KeysSet { open : String, closed : String }
openClosedBrackets =
    KeysSet.promising
        [ unique .open, unique .closed ]
        |> KeysSet.insert
            { open = "(", closed = ")" }


openClosedBrackets
    |> KeysSet.elementRemove ( .open, ")" )
    |> KeysSet.isEmpty
--> False
-- .open is never ")"


openClosedBrackets
    |> KeysSet.elementRemove ( .closed, ")" )
    |> KeysSet.isEmpty
--> True

If the checked aspect isn't promised to be unique, elementRemove acts like a filter

KeysSet.promising
    [ unique .open, unique .closed ]
    |> KeysSet.insertList
        [ { open = "[", closed = "]", description = "List" }
        , { open = "<", closed = ">", description = "Custom" }
        , { open = "\\", closed = "/", description = "Custom" }
        ]
    |> KeysSet.elementRemove ( .description, "Custom" )
    |> KeysSet.toList
--> [ { open = "[", closed = "]", description = "List" } ]

KeysSet.mapTry to filter values

runtime: length

( element -> aspect, aspect )
-> (element -> element)
-> KeysSet element
-> KeysSet element

Change the element with the matching aspect based on its current value

KeysSet.promising
    [ unique .username, unique .email ]
    |> KeysSet.insertList
        [ { username = "fred", priority = 1, email = "[email protected]" }
        , { username = "gria", priority = 3, email = "[email protected]" }
        ]
    |> KeysSet.elementAlter ( .username, "fred" )
        (\user -> { user | priority = user.priority + 3 })
    |> KeysSet.isEqualTo
        (KeysSet.promising [ unique .email ]
            |> KeysSet.insertList
                [ { username = "fred", priority = 4, email = "[email protected]" }
                , { username = "gria", priority = 3, email = "[email protected]" }
                ]
        )
--> True

If this aspect isn't unique, all elements with the matching aspect are altered

KeysSet.promising [ unique .email ]
    |> KeysSet.insertList
        [ { username = "fred", priority = 1, email = "[email protected]" }
        , { username = "fred", priority = 3, email = "[email protected]" }
        ]
    |> KeysSet.elementAlter ( .username, "fred" )
        (\user -> { user | priority = user.priority + 3 })
    |> KeysSet.isEqualTo
        (KeysSet.promising [ unique .email ]
            |> KeysSet.insertList
                [ { username = "fred", priority = 4, email = "[email protected]" }
                , { username = "fred", priority = 6, email = "[email protected]" }
                ]
        )
--> True

To change all elements → map, mapTry or alter if the result type is the same as before

runtime: length

alter : (element -> element) -> KeysSet element -> KeysSet element

Change every element based on its current value

rankUpAllUsers =
    KeysSet.updateAll
        (\user -> { user | rank = user.rank + 1 })

If aspects that are promised to be unique become the same for 2 elements, the more recently inserted element is chosen

Your function changes the type of the element? → KeysSet.map

runtime: length^2

map :
(element -> mappedElement)
-> List (Uniqueness mappedElement)
-> KeysSet element
-> KeysSet mappedElement

Alter every element based on its current value

digitNames =
    KeysSet.promising
        [ unique .number, unique .name ]
        |> KeysSet.insertList
            [ { number = 0, name = "zero" }
            , { number = 1, name = "one" }
            ]


mathSymbolNames =
    digitNames
        |> KeysSet.map
            (\{ number, name } ->
                { symbol = String.fromInt number, name = name }
            )
            [ unique .symbol, unique .name ]
        |> KeysSet.insert { symbol = "+", name = "plus" }

Your function doesn't change the type of the element? → KeysSet.alter

runtime: length^2

(element -> Maybe mappedElementFill)
-> List (Uniqueness mappedElementFill)
-> KeysSet element
-> KeysSet mappedElementFill

Only keep elements that are transformed to Just by a given function

operators : KeysSet { symbol : String, name : String }
operators =
    KeysSet.promising
        [ unique .symbol, unique .name ]
        |> KeysSet.insertList
            [ { symbol = ">", name = "gt" }
            , { symbol = "<", name = "lt" }
            , { symbol = "==", name = "eq" }
            ]


-- singleCharOperators
operators
    |> KeysSet.mapTry
        (\operator ->
            case operator.symbol |> String.length of
                1 ->
                    operator.name |> Just
                _ ->
                    Nothing
        )
        [ unique identity ]
    |> KeysSet.toList
--> [ "gt", "lt" ]

runtime: length^2

transform

accumulationValue
-> (element -> accumulationValue -> accumulationValue)
-> KeysSet element
-> accumulationValue

Reduce the elements from most recently to least recently inserted element

The keys can be non-comparable. There is no obvious order.

→ You shouldn't rely on order when using functions like fold or toList

brackets : KeysSet { open : Char, closed : Char }
brackets =
    KeysSet.promising
        [ unique .open, unique .closed ]
        |> KeysSet.insertList
            [ { open = '(', closed = ')' }
            , { open = '{', closed = '}' }
            ]


brackets
    |> KeysSet.foldFrom []
        (\{ open, closed } ->
            (::) (String.fromList [ open, closed ])
        )
--> []
-->     |> (::) (String.fromList [ '{', '}' ])
-->     |> (::) (String.fromList [ '(', ')' ])
toList : KeysSet element -> List element

The List containing all elements from most recently (= head) to least recently inserted element

The keys can be non-comparable. There is no obvious order.

→ You shouldn't rely on order when using functions like fold or toList

mostRecentlyInserted =
    KeysSet.toList >> List.head


KeysSet.promising
    [ unique .open, unique .closed ]
    |> KeysSet.insertList
        [ { open = '(', closed = ')' }
        , { open = '{', closed = '}' }
        ]
    |> KeysSet.toList
--> [ { open = '{', closed = '}' }
--> , { open = '(', closed = ')' }
--> ]

runtime: 1