ericgj / elm-csv-decode / Csv.Decode

Basic usage

Using periodic/elm-csv (which returns a Result (List String) Csv):

Csv.parse rawData |> Csv.Decode.decode myDecoder

Using lovasoa/elm-csv (which returns a plain Csv):

Csv.parse rawData |> Csv.Decode.decodeCsv myDecoder

You can define decoders based on field position or on header name. See examples below.

Types


type alias Csv =
{ headers : List String
, records : List (List String) 
}

The raw CSV data structure.


type Decoder a b

A value that encapsulates how to decode CSV records (List String)

Running

decode : Decoder (a -> a) a -> Result (List String) Csv -> Result Errors (List a)

Decode the raw result of CSV parsing.

Typically you chain them together like this (using periodic/elm-csv):

Csv.parse rawData |> Csv.Decode.decode myDecoder

decodeCsv : Decoder (a -> a) a -> Csv -> Result Errors (List a)

Decode 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.

Defining

next : (String -> Result String a) -> Decoder (a -> b) b

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
            |> andMap (next String.toFloat)
            |> andMap (next String.toFloat)
        )

field : String -> (String -> Result String a) -> Decoder (a -> b) b

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
            |> andMap (field "calories"  String.toInt)
            |> andMap (field "protein" String.toFloat)
        )

Note that position- and header-based decoding can be combined, but it is not generally recommended.

assertNext : String -> Decoder a a

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"
                  |> andMap (next String.toFloat)
              )
        , map Parcel
              ( assertNext "PARCEL"
                  |> andMap (next String.toFloat)
                  |> andMap (next parseDimensions)
              )
        ]

(Note: If you are familiar with the url-parser library, this is structurally similar to the s function.)

assertField : String -> String -> Decoder a a

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"
                  |> andMap (field "weight" String.toFloat)
              )
        , map Parcel
              ( assertField "type" "PARCEL"
                  |> andMap (field "weight" String.toFloat)
                  |> andMap (field "dimensions"  parseDimensions)
              )
        ]

maybe : (String -> Result String a) -> String -> Result String (Maybe a)

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
            |> andMap (field "insurance"  (maybe parseCurrencyAmount))
        )

Combining

andMap : Decoder b c -> Decoder a b -> Decoder a c

Decode multiple fields.

decodeCsv
    (assertField "site" "blog"
        |> andMap (field "id" String.toInt)
    )
    data

-- { headers = [ "site", "id" ]
-- , records = [["blog","35"]]
-- }   ==>  Ok [35]

oneOf : List (Decoder a b) -> Decoder a b

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
      ]

map : a -> Decoder a b -> Decoder (b -> c) c

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 |> andMap (field "id" String.toInt)

decodeComment : Decoder (Comment -> a) a
decodeComment =
    map Comment decodeRawComment

Errors


type Errors
    = CsvErrors (List String)
    | DecodeErrors (List ( Basics.Int, String ))

Errors can either be

  1. Errors passed through from the underlying CSV parsing (CsvErrors), or
  2. Errors in decoding a list of parsed records to models (DecodeErrors)

Note that the latter reports the record index together with the error message.