The built-in field types and the behaviour for defining your own.
A type is the only place where the text in a file becomes an Elixir term.
Reading calls cast/2 with the cell's text; writing calls dump/2 with
the term. Neither callback ever receives nil: an empty cell becomes the
field's default without reaching the type, and a nil value is written as the
dialect's first null string.
Built-in types
| Type | Reads | Writes |
|---|---|---|
:string | the cell unchanged | the binary unchanged |
:integer | an optionally signed decimal integer | Integer.to_string/1 |
:float | an integer or decimal, with an optional exponent | Float.to_string/1 |
:boolean | true/t/yes/y/1 and false/f/no/n/0, any case | "true" or "false" |
:date | Date.from_iso8601/1 | Date.to_iso8601/1 |
:time | Time.from_iso8601/1 | Time.to_iso8601/1 |
:naive_datetime | NaiveDateTime.from_iso8601/1 | NaiveDateTime.to_iso8601/1 |
:utc_datetime | DateTime.from_iso8601/1, converted to UTC | DateTime.to_iso8601/1 |
:decimal | Decimal.parse/1, finite values only | Decimal.to_string/2 in :normal form |
{:enum, values} | one of the declared strings | the string declared for the term |
The whole cell must be consumed. "12abc" is not an integer and "1.0" is
not an integer, because a partial read is how silently wrong numbers enter a
data set.
:utc_datetime accepts any ISO 8601 offset and converts to UTC, so
2024-03-01T12:00:00+02:00 reads as 2024-03-01 10:00:00Z. Writing it back
produces the UTC form, not the original offset.
:decimal requires the optional :decimal dependency. Declaring the type
without it raises at compile time.
No built-in type parses a locale-specific date, a thousands separator, or a currency symbol. Define a type for those.
Enumerations
{:enum, [:draft, :published]} reads and writes the atom's own text.
{:enum, [draft: "D", published: "P"]} maps each atom to the text the file
uses. Any other text in the cell is a :cast_failed error, which is the point
of declaring the enumeration.
Defining a type
defmodule Postcode do
@behaviour Delimited.Type
@impl true
def cast(text, _opts) do
case Regex.run(~r/^([A-Z]{1,2}\d[A-Z\d]?) ?(\d[A-Z]{2})$/, String.upcase(text)) do
[_, outward, inward] -> {:ok, outward <> " " <> inward}
nil -> {:error, "a UK postcode"}
end
end
@impl true
def dump(postcode, _opts) when is_binary(postcode), do: {:ok, postcode}
def dump(_other, _opts), do: {:error, "a UK postcode"}
endUse it as field :postcode, Postcode. The error string completes the sentence
"cannot read value as ...", so write a noun phrase rather than a sentence.
Options that Delimited.Field does not recognise are passed to a custom type
as opts, so field :price, Money, currency: "GBP" reaches cast/2 as
[currency: "GBP"]. Built-in types accept no options and reject unknown ones,
because there a stray key is a typo rather than configuration.
Summary
Types
What the type expected, as a noun phrase completing "cannot read X as ...".
A built-in type name, an enumeration, or a module implementing this behaviour.
Functions
Returns the built-in type names.
Reads one cell's text as type.
Describes what a type accepts, as a noun phrase.
Writes a term as one cell's text.
Checks a declared type and returns it in the form the reader and writer use.
Types
@type expectation() :: String.t()
What the type expected, as a noun phrase completing "cannot read X as ...".
@type t() :: :string | :integer | :float | :boolean | :date | :time | :naive_datetime | :utc_datetime | :decimal | {:enum, [{atom(), String.t()}]} | module()
A built-in type name, an enumeration, or a module implementing this behaviour.
Callbacks
@callback cast(text :: String.t(), opts :: keyword()) :: {:ok, term()} | {:error, expectation()}
Converts one cell's text into a term.
Never called with nil. Return {:error, expectation} where the expectation
completes the sentence "cannot read value as ...".
@callback dump(value :: term(), opts :: keyword()) :: {:ok, iodata()} | {:error, expectation()}
Converts a term into the text for one cell.
Never called with nil. Returning iodata avoids a copy for composite values.
Functions
@spec builtins() :: [atom()]
Returns the built-in type names.
@spec cast(t(), String.t(), keyword()) :: {:ok, term()} | {:error, expectation()}
Reads one cell's text as type.
@spec describe(t()) :: expectation()
Describes what a type accepts, as a noun phrase.
@spec dump(t(), term(), keyword()) :: {:ok, iodata()} | {:error, expectation()}
Writes a term as one cell's text.
Checks a declared type and returns it in the form the reader and writer use.
Enumerations are normalised to a keyword list of term-to-text pairs, so
{:enum, [:a]} becomes {:enum, [a: "a"]}. Every other type is returned
unchanged.
Raises ArgumentError for an unknown type. Called at compile time by the
schema DSL, so a typo fails the build rather than the first read.