What would an Elm function definition look like which takes a list of records, and perform a groupby operation (see example below)? Using a SQL analogy, I'm basically trying to achieve select last, sum(age) from table group by lastin Elm.
[{name= "John", last= "Smith", age= 10}
,{name="Jason", last= "Smith", age= 9}
, {name="Jane", last ="White", age =5}]
return [{last="Smith", age =19},
{last ="White", age =5}]
You could use Dict.Extra.groupBy to perform the initial grouping, then sum the ages by mapping over the list and summing the ages:
import Dict.Extra exposing (groupBy)
familySumAges : List Person -> List Family
familySumAges =
groupBy .last
>> Dict.map (\_ -> List.map .age >> List.sum)
>> Dict.toList
>> List.map (uncurry Family)
The following answer courtesy of #holy-meekrob and #ilias from the Elm Slack channel. https://ellie-app.com/7tqY9w6gNa1/1
module Main exposing (..)
import Dict exposing (..)
import Html exposing (text)
type alias Person =
{ name : String
, last : String
, age : Int
}
type alias Family =
{ last : String
, age : Int
}
people : List Person
people =
[ { name = "John"
, last = "Smith"
, age = 10
}
, { name = "Jason"
, last = "Smith"
, age = 9
}
, { name = "Jane"
, last = "White"
, age = 5
}
]
sumAges : Person -> Dict String Family -> Dict String Family
sumAges person families =
Dict.update
person.last
(\family ->
case family of
Nothing ->
Just { last = person.last, age = person.age }
Just fam ->
Just { last = person.last, age = fam.age + person.age }
)
families
main =
text (toString (List.foldl sumAges Dict.empty people |> Dict.values))
My approach is different than the solution you accepted.
If the list of records is sorted, then we could simply aggregate the age as long as the last name remains unchanged.
Otherwise, we'll need to create a new record for the unseen last name.
import List
SumAgeByLast data = let
sorted = List.sortBy .last data
fst x = List.head x |> Maybe.withDefault {name= "_", last= "_", age= 0}
agg lst p =if (fst lst).last==p.last then {name = "_", last = p.last ,age = (fst lst).age+p.age} else p::lst
in
List.foldl agg [] sorted
We basically iterate over all people p and add them to the first element of the list lst unless we encounter that last name for the first time, then we prepend it p::lst
Test it with
data = [{name= "John", last= "Smith", age= 10},{name="Jason", last= "Smith", age= 9}, {name="Jane", last ="White", age =5}]
SumByLast data
Obviously, this function could be made generic, if we pass .last and .age as parameters
Related
I have a model containing a list of items that are rendered in a select as options.
The user can select an item, enter a number and click add to add the selected item and a "quantity" to a list.
My model looks like this:
type alias Drink =
{ id: String
, name: String
}
type alias Item =
{ id: String
, quantity: Int
}
type alias Model =
{ drinks: List Drink
, selected: List Item
, inputDrink: String
, inputQuantity: Int
}
I then want to render the selected list in a table. My main struggle right now is figuring out how I map over the array of selected items, based on the id of the current item find the name of the Drink to render in the table.
I've made this itemRow view:
itemRow : (Item, Drink) -> Html Msg
itemRow tuple =
-- This bit not updated to work with a Tuple yet.
tr [ id item.id ]
[ td []
[ button [] [ text "x" ]
]
, td [] [ text drink.name ]
, td []
[ input [ type_ "number", value (String.fromInt item.quantity) ] []
]
]
So what I'd like is to do something like:
model.selected
|> List.map (\selected -> (selected, List.Extra.find (\drink -> drink.id == selected.id)) )
|> List.map itemRow
But to do this I need to get rid of the Maybe I get from List.Extra.find and I don't know how… 😅
Any other tips or tricks on how I might better solve this by modelling the data differently very welcome. New to Elm :)
Here's how you remove the Nothings. Although you know that the find must always succeed, Elm requires you to handle the case where it does not. Here I just ignore those cases.
model.selected
|> List.filterMap (\selected ->
case List.Extra.find (\drink -> drink.id == selected.id) of
Just x -> Just (selected, x)
Nothing -> Nothing
)
|> List.map itemRow
I am new to Elm. I am not able to call the update function once the dropdown value changes.
Scenario:
I have two dropdowns Grade and Environment. What I want is when Grade dropdown values change, the options of Environment dropdown will dependently change.
For example, if Grade dropdown value is 3 then the options of Environment should change to Imagine Math
gradeDropdown : String -> List String -> Html Msg
gradeDropdown grade grades =
let
buildOption =
gradeOption grade
in
select [ class "importWizard--gradeSelection", name "gradeSelection", onChange (UpdateStudent Grade) ]
(map buildOption grades)
gradeOption : String -> String -> Html Msg
gradeOption optSelectedVal temp =
let
optSelected =
temp == optSelectedVal
in
option [ value temp, selected optSelected ] [ text temp ]
environmentDropdown : Model -> String -> List String -> String -> Html Msg
environmentDropdown model learningEnvironment learningEnvironments selectedGrade =
let
buildOption =
environmentOption model learningEnvironment
blueprint_grades = ["PreKindergarten", "Kindergarten", "1"]
environmentDropdownOption =
if (selectedGrade == "" || (List.member selectedGrade blueprint_grades)) then
["Blueprint"]
else if (selectedGrade == "2") then
learningEnvironments
else
["Imagine Math"]
in
select [
class "importWizard--learningEnvironmentSelection"
, name "learningEnvironmentSelection"
, onChange (UpdateStudent LearningEnvironments)
]
(map buildOption environmentDropdownOption)
environmentOption : Model -> String -> String -> Html Msg
environmentOption model optSelectedVal temp =
let
optSelected =
temp == optSelectedVal
in
option [ value temp, selected optSelected ] [ text temp ]
And in Update
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
update flags message model =
case message of
UpdateStudent updateType newValue ->
let
validate =
validateStudent flags validatableFieldsForScreen
in
case updateType of
LastName ->
( validate { model | lastName = newValue } <| Just LastNameField, Cmd.none )
FirstName ->
( validate { model | firstName = newValue } <| Just FirstNameField, Cmd.none )
Sin ->
( validate { model | sin = newValue } <| Just SinField, Cmd.none )
Grade ->
( validate { model | grade = newValue, selectedGrade = newValue } Nothing, Cmd.none )
LearningEnvironments ->
( validate { model | learningEnvironments = newValue } Nothing, Cmd.none )
View:
, td [ class wizardTableInput ] [ gradeDropdown model.grade flags.grades ]
, td [ class wizardTableInput ] [ environmentDropdown model model.learningEnvironments flags.learningEnvironments model.selectedGrade ]
In this code, the environment dropdown's value is changing, however the model's value is not updated. From what I understand, I can see is environment dropdown's id re-rendered, but it is not updating the model's value of learningEnvironments. This means it is not executing the update function matching LearningEnvironments.
select widgets where the options change is one of the use cases for Html.Keyed.node
Use a helper function like the one bellow:
keyedSelect : (String -> a) -> String -> List ( String, String ) -> Html a
keyedSelect message selectedValue kvs =
let
toOption ( k, v ) =
( k
, option
[ value k
, selected (k == selectedValue)
, disabled (k == "-1")
]
[ text v ]
)
in
Keyed.node "select"
[ class "form-control", onChange message ]
(List.map toOption kvs)
I usually have a "Please select Foo" first option with the value -1 if the user never selected any of the options. This is why the code checks for -1 and disables the option. You can remove the disabled attribute if you don't need it.
I have this code in Elm-lang:
import Json.Decode exposing (..)
import Html exposing (..)
json = -- List that contains and will have many users
"""
[{\"ssn\":\"111.111.111-11\",\"name\":\"People Silva\",\"email\":\"people#eat.com\"}, {\"ssn\":\"000.000.000-00\",\"name\":\"Pet Silva\",\"email\":\"pet#eat.com\"}]
"""
type alias User =
{ name : String
, email: String
, ssn: String
}
userDecoder : Json.Decode.Decoder User
userDecoder =
Json.Decode.map3 User
(field "name" string)
(field "email" string)
(field "ssn" string)
userListDecoder : Json.Decode.Decoder (List User)
userListDecoder =
Json.Decode.list userDecoder
main =
let
decoded = (decodeString userListDecoder json)
in
case decoded of
Ok u ->
span [] [text (toString u)]
Err e ->
span [] [text (toString e)]
This code work very well, and output this(how expected):
[{ name = "People Silva", email = "people#eat.com", ssn = "111.111.111-11" },{ name = "Pet Silva", email = "pet#eat.com", ssn = "000.000.000-00" }]
And here begins my doubt, how get list of all users in list?(And also as a bonus, get the number of users in the list)
-- already tried, unsuccessfully
u[0].name
u[1].name
The users are in a List User value, so you can use the functions from the List package to access them.
If you want to get a list of user names from a list of users, you can call List.map .name users.
If you want to write the user names in their own divs, you can write it like this:
showUser : User -> Html msg
showUser user =
div [] [ text user.name ]
and call it from main like this:
main =
let
decoded = (decodeString userListDecoder json)
in
case decoded of
Ok users ->
div [] (List.map showUser users)
Err e ->
span [] [text (toString e)]
Obtaining the length of a list is just a matter of using the List.length function: List.length users
I've been following this tutorial: http://guide.elm-lang.org/architecture/user_input/forms.html
The text there makes sense to me, my question pertains to the exercise it lists at the bottom of the page. It asks that I:
"Add an additional field for age and check that it is a number."
I am having difficulty with this because the onInput function seems to only accept a String input. I find it odd that there is no equivalent for type="number" inputs.
Nevertheless, this is my attempt which does not work:
import Html exposing (..)
import Html.App as Html
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import String exposing (length)
main =
Html.beginnerProgram { model = model, view = view, update = update }
-- MODEL
type alias Model =
{ name : String
, password : String
, passwordAgain : String
, age : Int
}
model : Model
model =
Model "" "" "" 0
-- UPDATE
type Msg
= Name String
| Password String
| PasswordAgain String
| Age Int
update : Msg -> Model -> Model
update msg model =
case msg of
Name name ->
{ model | name = name }
Password password ->
{ model | password = password }
PasswordAgain password ->
{ model | passwordAgain = password }
Age age ->
{ model | age = age }
-- VIEW
view : Model -> Html Msg
view model =
div []
[ input [ type' "text", placeholder "Name", onInput Name ] []
, input [ type' "password", placeholder "Password", onInput Password ] []
, input [ type' "password", placeholder "Re-enter Password", onInput PasswordAgain ] []
, input [ type' "number", placeholder "Age", onInput Age ] []
, viewValidation model
]
viewValidation : Model -> Html msg
viewValidation model =
let
(color, message) =
if model.password /= model.passwordAgain then
("red", "Passwords do not match!")
else if length model.password <= 8 then
("red", "Password must be more than 8 characters!")
else
("green", "OK")
in
div [ style [("color", color)] ] [ text message ]
The error I get is the following:
-- TYPE MISMATCH ----------------------------------------------------- forms.elm
The argument to function `onInput` is causing a mismatch.
58| onInput Age
^^^
Function `onInput` is expecting the argument to be:
String -> a
But it is:
Int -> Msg
Note: I am aware that I could create the Age input as just another text input, but the exercise specifically asked me to check that it is a `number type. I assume this means I should hold it inside the model as an Int.
I am clear about what the error is. I simply want to know the idiomatic way to fix this in Elm. Thanks.
Any user-input from onInput event is a String.
Your Model expects it to be an Int
Use String.toInt to parse the integer value from a string value.
Adjust update function to convert the type to an Int and change the type signature to Age String
Age age ->
case String.toInt age of
Ok val ->
{ model | age = val }
-- Notify the user, or simply ignore the value
Err err ->
model
That way you have an option to notify the user about the error.
In case if Maybe value suits you better, the whole statement can be simplified to:
Age age ->
{ model | age = Result.toMaybe (String.toInt age) }
You'll need the equivalent of onInput but for one that operates on integers. Based on how targetValue is defined, you can do something similar with the addition of Json.Decode.int to parse it as an integer:
onIntInput : (Int -> msg) -> Attribute msg
onIntInput tagger =
Html.Events.on "input" (Json.map tagger (Json.at ["target", "value"] Json.int))
You can then use it as such:
, input [ type' "number", placeholder "Age", onIntInput Age ] []
There is the type
type User
= Anonymous
| Named {name : String, email : String}
Json.Decode.object2 doesn't fit here because its first arguments type is (a -> b -> c) but Named has { email : String, name : String } -> User type.
How to decode to User?
Another way of doing this could be to define a function that accepts a name and email and returns your Named constructor:
userDecoder : Decoder User
userDecoder =
let
named =
object2
(\n e -> Named { name = n, email = e })
("name" := string)
("email" := string)
in
oneOf [ null Anonymous, named ]
Since your Named constructor takes a record as a parameter, it might be a little cleaner to create a type alias for the name and email record.
type alias NamedUserInfo =
{ name : String
, email : String
}
You could then redefine User to use the alias:
type User
= Anonymous
| Named NamedUserInfo
While the above isn't strictly necessary, I find that aliasing record types proves useful in many ways down the road. Here it is useful because it gives us a constructor NamedUserInfo that lets us clearly define a decoder:
import Json.Decode exposing (..)
namedUserInfoDecoder : Decoder NamedUserInfo
namedUserInfoDecoder =
object2
NamedUserInfo
("name" := string)
("email" := string)
And finally, your user decoder could be constructed like this:
userDecoder : Decoder User
userDecoder =
oneOf
[ null Anonymous
, object1 Named namedUserInfoDecoder
]
You can run your example through a quick test like this:
exampleJson =
"""
[{"user":null}, {"user": {"name": "John Doe", "email": "j.doe#mailg.com"}}]
"""
main =
text <| toString <| decodeString (list ("user" := userDecoder)) exampleJson
-- Ouputs:
-- Ok ([Anonymous,Named { name = "John Doe", email = "j.doe#mailg.com" }])