fifth-postulate / elm-csv-decode / Csv.Decode

Turn CSV Values into Elm values. Inspired by elm/json, so make sure to check out this intro to JSON decoders to get a feel for how this library works!

Note this library does not include an underlying CSV parser. It assumes you are using something like periodic/elm-csv 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.

In the examples we make use of a decodeString function. Which is defined as

decodeString : Decoder a -> String -> Result Error (List a)
decodeString decoder input =
    input
        |> (++) "\n"
        |> Csv.parse
        |> Result.mapError (\_ -> CsvParseError)
        |> Result.andThen (decode decoder)

Types


type Decoder a

A value that knows how to decode CSV values.


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

The raw CSV data structure.


type Error
    = CsvParseError
    | Not Kind
    | FailWithReason String
    | NonApply
    | MultipleErrors (List ( Basics.Int, Error ))

A structured error describing exactly how the decoder failed. You can use this to create more elaborate visualizations of a decoder problem. For example, you could show the entire CSV record and show the part causing the failure in red.


type Kind
    = AString
    | ABool
    | AInt
    | AFloat

Kind determines what type a decoder expects. Used in combination with the Not error.

Primitives

string : Decoder String

Decode a CSV string into an Elm String.

decodeString string "true" == Ok [ "true" ]

decodeString string "42" == Ok [ "42" ]

decodeString string "3.14" == Ok [ "3.14" ]

decodeString string "hello" == Ok [ "hello" ]

int : Decoder Basics.Int

Decode a CSV number into an Elm Int.

decodeString int "true"  == Err ...
decodeString int "42"    == Ok [ 42 ]
decodeString int "3.14"  == Err ...
decodeString int "hello" == Err ...

float : Decoder Basics.Float

Decode a CSV number into an Elm Float.

decodeString float "true"  == Err ..
decodeString float "42"    == Ok [ 42 ]
decodeString float "3.14"  == Ok [ 3.14 ]
decodeString float "hello" == Err ...

bool : Decoder Basics.Bool

Decode a CSV boolean into an Elm Bool.

decodeString bool "true"  == Ok [ True ]
decodeString bool "42"    == Err ...
decodeString bool "3.14"  == Err ...
decodeString bool "hello" == Err ...

Run Decoders

decode : Decoder a -> Csv -> Result Error (List a)

Decode the given Csv into a list of custom value by running Decoder on it. This will fail if any of the records can not be decoded by the Decoder for some reason.

Mapping

map : (a -> value) -> Decoder a -> Decoder value

Transform a decoder. Maybe you just want to know the length of a string:

import String

stringLength : Decoder Int
stringLength =
    map String.length string

It is often helpful to use map with oneOf, like when defining maybe:

maybe : Decoder a -> Decoder (Maybe a)
maybe decoder =
    oneOf
        [ map Just decoder
        , succeed Nothing
        ]

map2 : (a -> b -> value) -> Decoder a -> Decoder b -> Decoder value

Try two decoders and then combine the result. We can use this to decode objects with many fields:

type alias Point =
    { x : Float, y : Float }

point : Decoder Point
point =
    map2 Point
        float
        float

-- decodeString point "3,4" == Ok { x = 3, y = 4 }

It tries each individual decoder and puts the result together with the Point constructor.

map3 : (a -> b -> c -> value) -> Decoder a -> Decoder b -> Decoder c -> Decoder value

Try three decoders and then combine the result. We can use this to decode objects with many fields:

type alias Person =
    { name : String, age : Int, height : Float }

person : Decoder Person
person =
    map3 Person
        string
        int
        float

-- csv = "tom,42,1.8"
-- decodeString person csv == Ok { name = "tom", age = 42, height = 1.8 }

Like map2 it tries each decoder in order and then give the results to the Person constructor. That can be any function though!

Fancy Decoding

succeed : a -> Decoder a

Ignore the CSV and produce a certain Elm value.

decodeString (succeed 42) "true"  == Ok [ 42 ]
decodeString (succeed 42) "1,2,3" == Ok [ 42 ]
decodeString (succeed 42) "hello" == Ok [ 42 ]

This is handy when used with oneOf.

fail : String -> Decoder a

Ignore the CSV and make the decoder fail. This is handy when used with oneOf where you want to give a custom error message in some case.

maybe : Decoder a -> Decoder (Maybe a)

Helpful for dealing with optional fields. Here are a few slightly different examples:

decodeString (maybe int) "42" == Ok [ Just 42 ]

decodeString (maybe int) ",42" == Ok [ Nothing ]

oneOf : List (Decoder a) -> Decoder a

Try a bunch of different decoders. This can be useful if the CSV may come in a couple different formats. For example, say you want to read an array of numbers, but some of them are null.

import String

badInt : Decoder Int
badInt =
    oneOf [ int, null 0 ]

-- decodeString (list badInt) "[1,2,null,4]" == Ok [1,2,0,4]

Why would someone generate CSV like this? Questions like this are not good for your health. The point is that you can use oneOf to handle situations like this!