CSV decoding, heavily inspired by [url-parser][].
Note this library does not include an underlying CSV parser. It assumes you
are using something like [periodic/elm-csv][periodic] or
[lovasoa/elm-csv][lovasoa] to get from String to Csv, where
Csv is:
type alias Csv =
{ headers : List String
, records : List (List String)
}This library gets you the rest of the way, to a list of your own types. It also collects errors together with the index of the record they occurred on.
You can think of this as analogous to Json.Decode. It lets you declaratively
convert data from an intermediate representation (Json.Value or Csv) into
your own types.
Using periodic/elm-csv (which returns a Result (List String) Csv):
Csv.parse rawData |> Csv.Decode.decode myDecoderUsing lovasoa/elm-csv (which returns a plain Csv):
Csv.parse rawData |> Csv.Decode.decodeCsv myDecoderYou can define decoders based on field position or on header name. See examples below.
A value that encapsulates how to decode CSV records (List String)
Decode the raw result of CSV parsing.
Typically you chain them together like this (using periodic/elm-csv):
Csv.parse rawData |> Csv.Decode.decode myDecoderDecode raw CSV data.
This is useful if you already have a Csv structure not
wrapped in a Result, for instance lovasoa/elm-csv parses CSV strings this
way.
Decode the next field from the input: positional decoding.
Use this when you are certain of the order of the fields. It is faster than header-based decoding.
type alias Coordinates =
{ x : Float, y : Float, z : Float }
decodeCoordinates : Decoder (Coordinates -> a) a
decodeCoordinates =
map Coordinates
( next String.toFloat
</> next String.toFloat
</> next String.toFloat
)Decode the named field from the input: header-based decoding.
Use this when you do not want to rely on the order of the fields, or when your source fields map to more than one target field.
type alias Nutrition =
{ name : String, calories : Int, protein : Float }
decodeNutrition : Decoder (Nutrition -> a) a
decodeNutrition =
map Nutrition
( field "name" Ok
</> field "calories" String.toInt
</> field "protein" String.toFloat
)Note that position- and header-based decoding can be combined, but it is not generally recommended.
Decode the next field if it matches the given string.
This can be useful to decode a union type based on a field of the CSV. For example:
type Mailing
= Letter Float
| Parcel Float Dimensions
decodeMailing : Decoder (Mailing -> a) a
decodeMailing =
oneOf
[ map Letter
( assertNext "LETTER"
</> next String.toFloat
)
, map Parcel
( assertNext "PARCEL"
</> next String.toFloat
</> next parseDimensions
)
](Note: If you are familiar with the url-parser library, this is structurally
similar to the s function.)
Decode a named field if it matches the given string.
The same example above, but for header-based decoding:
type Mailing
= Letter Float
| Parcel Float Dimensions
decodeMailing : Decoder (Mailing -> a) a
decodeMailing =
oneOf
[ map Letter
( assertField "type" "LETTER"
</> field "weight" String.toFloat
)
, map Parcel
( assertField "type" "PARCEL"
</> field "weight" String.toFloat
</> field "dimensions" parseDimensions
)
]A convenience function for converting empty strings to Nothing.
Useful when you have optional fields.
type alias Letter =
{ weight = Float
, insurance = Maybe CurrencyAmount
}
decodeLetter : Decoder (Letter -> a) a
decodeLetter =
map Letter
( field "weight" String.toFloat
</> field "insurance" (maybe parseCurrencyAmount)
)Decode multiple fields.
decodeCsv
(assertField "site" "blog"
</> field "id" String.toInt
)
data
-- { headers = [ "site", "id" ]
-- , records = [["blog","35"]]
-- } ==> Ok [35]Try a bunch of different decoders, using the first one that succeeds.
type IntOrFloat
= Int_ Int
| Float_ Float
decode : Decoder (IntOrFloat -> a) a
decode =
oneOf
[ map Int_ <| next String.toInt
, map Float_ <| next String.toFloat
]Transform a decoder.
Typically used to feed a bunch of parsed state into a type constructor.
type alias Comment = { author : String, id : Int }
decodeRawComment : Decoder (String -> Int -> a) a
decodeRawComment =
field "author" Ok </> field "id" String.toInt
decodeComment : Decoder (Comment -> a) a
decodeComment =
map Comment decodeRawCommentErrors can either be
CsvErrors), orDecodeErrors)Note that the latter reports the record index together with the error message.
The raw CSV data structure.