BrianHicks/elm-csv/2.0.1

NOTE: the latest version is 4.0.1

Csv.Decode

Decode values from CSV. A crash course on constructing decoders:

  • To decode basic values in different columns, use field or column along with int, string, or float.
  • To transform values in ways that can't fail, pass them to map. To transform them in ways that can fail, pass decoders to andThen and call succeed or fail to handle the failure gracefully. If you just need to lift a Result String a into a decoder, use fromResult
  • To decode tuples, pass a constructor to map2 or map3.
  • To decode records, use into and pipeline.

All of those functions have examples in their documentation. Check 'em out!

If you run into trouble, this library intentionally sticks as close to elm/json and NoRedInk/elm-json-decode-into semantics as possible. Figuring out how you'd write an equivalent JSON decoder may help! (But if you run into something you truly can't do, please open an issue.)

Basic Decoders

type Decoder a

A way to specify what kind of thing you want to decode into. For example, if you have a Pet data type, you'd want a Decoder Pet.

string : Decoder String

Decode a string from a CSV.

decodeCsv NoFieldNames string "a" --> Ok [ "a" ]

Unless you specify otherwise (e.g. with column) this will assume there is only one column in the CSV and try to decode that.

decodeCsv NoFieldNames string "a,b"
--> Err (DecodingError { row = 0, problem = AmbiguousColumn })
int : Decoder Int

Decode an integer from a CSV.

decodeCsv NoFieldNames int "1" --> Ok [ 1 ]


decodeCsv NoFieldNames int "-1" --> Ok [ -1 ]


decodeCsv NoFieldNames int "volcano"
--> Err (DecodingError { row = 0, problem = ExpectedInt "volcano" })

Unless you specify otherwise (e.g. with column) this will assume there is only one column in the CSV and try to decode that.

decodeCsv NoFieldNames int "1,2"
--> Err (DecodingError { row = 0, problem = AmbiguousColumn })
float : Decoder Float

Decode an integer from a CSV.

decodeCsv NoFieldNames float "3" --> Ok [ 3.0 ]


decodeCsv NoFieldNames float "3.14" --> Ok [ 3.14 ]


decodeCsv NoFieldNames float "mimesis"
--> Err (DecodingError { row = 0, problem = ExpectedFloat "mimesis" })

Unless you specify otherwise (e.g. with column) this will assume there is only one column in the CSV and try to decode that.

decodeCsv NoFieldNames float "1.0,2.0"
--> Err (DecodingError { row = 0, problem = AmbiguousColumn })
blank : Decoder a -> Decoder (Maybe a)

Handle blank fields by making them into Maybes.

decodeCsv NoFieldNames (blank int) "\r\n1"
--> Ok [ Nothing, Just 1 ]


decodeCsv NoFieldNames (blank int) "not a number"
--> Err (DecodingError { row = 0, problem = ExpectedInt "not a number" })

Finding Values

column : Int -> Decoder a -> Decoder a

Parse a value at a numbered column in the CSV, starting from 0.

decodeCsv NoFieldNames (column 0 string) "Argentina" --> Ok [ "Argentina" ]


decodeCsv NoFieldNames (column 1 int) "1,2,3" --> Ok [ 2 ]


decodeCsv NoFieldNames (column 100 float) "3.14"
--> Err (DecodingError { row = 0, problem = ExpectedColumn 100 })
field : String -> Decoder a -> Decoder a

Parse a value at a named column in the CSV. There are a number of ways to provide these names, see FieldNames

decodeCsv
    FieldNamesFromFirstRow
    (field "Country" string)
    "Country\r\nArgentina"
--> Ok [ "Argentina" ]


decodeCsv
    (CustomFieldNames [ "a", "b", "c" ])
    (field "b" int)
    "1,2,3"
--> Ok [ 2 ]


decodeCsv
    (CustomFieldNames [ "Constant" ])
    (field "Nonexistent" float)
    "3.14"
--> Err (DecodingError { row = 0, problem = ExpectedField "Nonexistent" })

Running Decoders

= NoFieldNames
| CustomFieldNames (List String)
| FieldNamesFromFirstRow

Where do we get names for use with field?

  • NoFieldNames: don't get field names at all. field will always fail.
  • CustomFieldNames: use the provided field names in order (so ["Id", "Name"] will mean that "Id" is in column 0 and "Name" is in column 1.)
  • FieldNamesFromFirstRow: use the first row of the CSV as the source of field names.
decodeCsv : FieldNames -> Decoder a -> String -> Result Error (List a)

Convert a CSV string into some type you care about using the Decoders in this module!

{ fieldSeparator : Char }
-> Decoder a
-> String
-> Result Error (List a)

Convert something shaped roughly like a CSV. For example, to decode tab-separated values where the row separator is a single newline character:

decodeCustom {  fieldSeparator = '\t' }
    NoFieldNames
    (map2 Tuple.pair
        (column 0 int)
        (column 1 string)
    )
    "1\tBrian\n2\tAtlas"
    --> Ok [ ( 1, "Brian" ), ( 2, "Atlas" ) ]
type Error
= ParsingError Problem
| DecodingError
{ row : Int
, problem : Problem
}

Sometimes things go wrong. This is how we tell you what happened. If you need to present this to someone, you can get a human-readable version with errorToString

Some more detail:

  • ConfigError: decodeCustom got a bad separator character
  • ParsingError: there was a problem parsing the data (the most common issue is problems with quoted fields. Check that any quoted fields are closed and that quotes are escaped by doubling.)
  • DecodingError: we couldn't decode a value using the specified decoder. See Problem for more details.
errorToString : Error -> String

Want an human-readable version of an Error? Here we are!

type Problem
= NoFieldNamesOnFirstRow
| ExpectedColumn Int
| ExpectedField String
| AmbiguousColumn
| ExpectedInt String
| ExpectedFloat String
| Failure String
| ManyProblems Problem Problem

Things that went wrong specifically while decoding.

  • NoFieldNamesOnFirstRow: we tried to get the field names from the first row (using FieldNames) but couldn't find any, probably because the input was blank.
  • ExpectedColumn Int and ExpectedField String: we looked for a value at a specific column, but couldn't find it. The argument specifies where we tried to look.
  • AmbiguousColumn: basic decoders expect to find a single value. If there are multiple fields in a row, and you don't specify which one to use with column or field, you'll get this error.
  • ExpectedInt String and ExpectedFloat String: we tried to parse a string into a number, but couldn't. The arguments specify the strings we got.
  • Failure: messages from fail end up here.
  • ManyProblems: when there are multiple failures, messages from oneOf end up here.

Transforming Values

map : (from -> to) -> Decoder from -> Decoder to

Decode a value, then transform it before returning.

decodeCsv NoFieldNames (map (\i -> i * 2) int) "15" --> Ok [ 30 ]


decodeCsv NoFieldNames (map String.reverse string) "slap" --> Ok [ "pals" ]
map2 : (a -> b -> c) -> Decoder a -> Decoder b -> Decoder c

Combine two decoders to make something else, for example a tuple:

decodeCsv NoFieldNames
    (map2 Tuple.pair
        (column 0 int)
        (column 1 string)
    )
    "1,Atlas"
    --> Ok [ (1, "Atlas") ]
map3 : (a -> b -> c -> d) -> Decoder a -> Decoder b -> Decoder c -> Decoder d

Like map2, but with three decoders. map4 and beyond don't exist in this package. Use into to decode records instead!

decodeCsv NoFieldNames
    (map3 (\r g b -> (r, g, b))
        (column 0 int)
        (column 1 int)
        (column 2 int)
    )
    "255,255,0"
    --> Ok [ (255, 255, 0) ]
into : (a -> b) -> Decoder (a -> b)

Need to decode into an object? Use a into. The way this works: you provide a function that takes as many arguments as you need, and then send it values by providing decoders.

type alias Pet =
    { id : Int
    , name : String
    , species : String
    , weight : Float
    }


petDecoder : Decoder Pet
petDecoder =
    into Pet
        |> pipeline (column 0 int)
        |> pipeline (column 1 string)
        |> pipeline (column 2 string)
        |> pipeline (column 3 float)

Now you can decode pets like this:

decodeCsv NoFieldNames petDecoder "1,Atlas,cat,14.5"
--> Ok [ { id = 1, name = "Atlas", species = "cat", weight = 14.5 } ]
pipeline : Decoder a -> Decoder (a -> b) -> Decoder b

See into.

Fancy Decoding

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

Try several possible decoders in sequence, committing to the first one that passes.

decodeCsv NoFieldNames (oneOf (map Just int) [ succeed Nothing ]) "1"
--> Ok [ Just 1 ]


decodeCsv NoFieldNames (oneOf (map Just int) [ succeed Nothing ]) "a"
--> Ok [ Nothing ]
succeed : a -> Decoder a

Always succeed, no matter what. Mostly useful with andThen.

fail : String -> Decoder a

Always fail with the given message, no matter what. Mostly useful with andThen.

andThen : (a -> Decoder b) -> Decoder a -> Decoder b

Decode some value and then make a decoding decision based on the outcome. For example, if you wanted to reject negative numbers, you might do something like this:

positiveInt : Decoder Int
positiveInt =
    int
        |> andThen
            (\rawInt ->
                if rawInt < 0 then
                    Decode.fail "Only positive numbers allowed!"


                else
                    Decode.succeed rawInt
            )

You could then use it like this:

decodeCsv NoFieldNames positiveInt "1" -- Ok [ 1 ]


decodeCsv NoFieldNames positiveInt "-1"
-- Err { row = 0, problem = Failure "Only positive numbers allowed!" }
fromResult : Result String a -> Decoder a

Make creating custom decoders a little easier. If you already have a function that parses a string into something you care about, you can probably provide it here and avoid having to build your own with andThen.

For example, here's how you could parse a hexadecimal number with rtfeldman/elm-hex:

import Hex


hex : Decoder Int
hex =
    string
        |> andThen (fromResult << Hex.fromString)


decodeCsv NoFieldNames hex "ff"
--> Ok [ 255 ]
fromMaybe : String -> Maybe a -> Decoder a

Like fromResult but you have to specify the error message since Nothing has no further information.

For example, you could implement something like int using this:

myInt : Decoder Int
myInt =
    string
        |> andThen (fromMaybe "Expected an int" << String.toInt)


decodeCsv NoFieldNames myInt "123"
--> Ok [ 123 ]

(That said, you probably want to use int instead... it has better error messages!)