# `AnchoFijo.Campo`
[🔗](https://github.com/galenzo17/ancho_fijo/blob/v0.1.0/lib/ancho_fijo/campo.ex#L1)

Definición de un campo del layout: dónde está, cuánto mide y cómo se lee.

Un campo es data. Se define con una keyword list y se valida al construirlo,
antes de tocar un solo archivo.

    iex> alias AnchoFijo.Campo
    iex> campo = Campo.nuevo!(nombre: :monto, posicion: 11, largo: 12, tipo: :decimal, precision: 2)
    iex> Campo.rango(campo)
    {11, 22}

## Opciones

  * `:nombre` — átomo, obligatorio. Es la clave del registro parseado.
  * `:largo` — entero positivo, obligatorio.
  * `:posicion` — entero positivo, 1-based. Si se omite, `AnchoFijo.Layout`
    la calcula encadenando el campo anterior.
  * `:tipo` — `:texto` (default), `:entero`, `:decimal` o `:fecha`.
  * `:opcional` — si es `true`, un campo en blanco (o en ceros, para fechas)
    se lee como `nil` en vez de producir un diagnóstico. Default `false`.
  * `:trim` — `:ambos` (default), `:izquierda`, `:derecha` o `false`. Solo
    aplica a `:texto`; los tipos numéricos y de fecha siempre recortan
    espacios porque el relleno no es parte del dato.
  * `:relleno` — carácter de relleno a recortar en `:texto`. Default `" "`.
  * `:precision` — obligatorio en `:decimal`. Cantidad de decimales que el
    formato declara.
  * `:separador` — en `:decimal`: `:implicito` (default), `:punto` o `:coma`.
  * `:formato` — obligatorio en `:fecha`: `:aaaammdd` o `:ddmmaaaa`.

## Tipos y valores devueltos

| tipo | valor |
| --- | --- |
| `:texto` | `String.t()` ya transcodificado a UTF-8 |
| `:entero` | `integer()` |
| `:decimal` | `{unidades, precision}`, p. ej. `{123456, 2}` para `1234.56` |
| `:fecha` | `Date.t()` |

Un `:decimal` nunca se convierte a float. Se devuelve como par
`{unidades_minimas, precision}` —centavos y escala— porque un monto que pasa
por punto flotante deja de cuadrar con la contabilidad del banco, y una
librería de lectura no debería obligar a depender de `Decimal` para evitarlo.

# `contexto`

```elixir
@type contexto() :: %{
  optional(:unidad) =&gt; :bytes | :caracteres,
  optional(:encoding) =&gt; :utf8 | :latin1,
  optional(:linea) =&gt; pos_integer() | nil
}
```

Contexto de lectura que aporta el layout: cómo medir, cómo decodificar y en
qué línea vamos, para que el diagnóstico sepa ubicarse.

# `t`

```elixir
@type t() :: %AnchoFijo.Campo{
  formato: :aaaammdd | :ddmmaaaa | nil,
  largo: pos_integer(),
  nombre: atom(),
  opcional: boolean(),
  posicion: pos_integer() | nil,
  precision: non_neg_integer() | nil,
  relleno: String.t(),
  separador: :implicito | :punto | :coma,
  tipo: tipo(),
  trim: :ambos | :izquierda | :derecha | false
}
```

# `tipo`

```elixir
@type tipo() :: :texto | :entero | :decimal | :fecha
```

# `valor`

```elixir
@type valor() ::
  String.t() | integer() | {integer(), non_neg_integer()} | Date.t() | nil
```

# `extraer`

```elixir
@spec extraer(t(), binary(), contexto() | keyword()) ::
  {:ok, valor()} | {:error, AnchoFijo.Diagnostico.t()}
```

Extrae y convierte el valor del campo desde una línea completa.

    iex> alias AnchoFijo.Campo
    iex> campo = Campo.nuevo!(nombre: :monto, posicion: 1, largo: 8, tipo: :decimal, precision: 2)
    iex> Campo.extraer(campo, "00123456")
    {:ok, {123456, 2}}

    iex> alias AnchoFijo.Campo
    iex> campo = Campo.nuevo!(nombre: :nombre, posicion: 1, largo: 6)
    iex> Campo.extraer(campo, <<74, 79, 83, 201, 32, 32>>, encoding: :latin1)
    {:ok, "JOSÉ"}

    iex> alias AnchoFijo.Campo
    iex> campo = Campo.nuevo!(nombre: :fecha, posicion: 1, largo: 8, tipo: :fecha, formato: :aaaammdd)
    iex> {:error, diagnostico} = Campo.extraer(campo, "20240230", linea: 4)
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "línea 4, campo :fecha (posiciones 1-8): se esperaban una fecha válida en formato AAAAMMDD, llegaron \"20240230\"; el día no existe en ese mes"

# `fin`

```elixir
@spec fin(t()) :: pos_integer()
```

Última posición que ocupa el campo.

    iex> AnchoFijo.Campo.fin(%AnchoFijo.Campo{nombre: :a, posicion: 11, largo: 5})
    15

# `nombre_unidad`

```elixir
@spec nombre_unidad(:bytes | :caracteres) :: String.t()
```

Nombre de la unidad de medida, para redactar diagnósticos.

    iex> AnchoFijo.Campo.nombre_unidad(:bytes)
    "bytes"

# `nuevo`

```elixir
@spec nuevo(Enumerable.t()) :: {:ok, t()} | {:error, [AnchoFijo.Diagnostico.t()]}
```

Valida y construye un campo.

Devuelve `{:ok, campo}` o `{:error, diagnosticos}` con un diagnóstico por
problema encontrado en la definición.

    iex> AnchoFijo.Campo.nuevo(nombre: :rut, largo: 10)
    {:ok, %AnchoFijo.Campo{nombre: :rut, largo: 10, tipo: :texto}}

    iex> {:error, [diagnostico]} = AnchoFijo.Campo.nuevo(nombre: :monto, largo: 12, tipo: :decimal)
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "layout, campo :monto: se esperaba :precision declarada para un campo :decimal; sin precisión no se sabe si 1234 son 12,34 o 1234,00"

# `nuevo!`

```elixir
@spec nuevo!(Enumerable.t()) :: t()
```

Igual que `nuevo/1` pero levanta `AnchoFijo.Error` si la definición es inválida.

    iex> AnchoFijo.Campo.nuevo!(nombre: :fecha, largo: 8, tipo: :fecha, formato: :ddmmaaaa).formato
    :ddmmaaaa

# `rango`

```elixir
@spec rango(t()) :: {pos_integer(), pos_integer()}
```

Rango de posiciones que ocupa el campo, 1-based e inclusivo en ambos extremos.

    iex> AnchoFijo.Campo.rango(%AnchoFijo.Campo{nombre: :a, posicion: 1, largo: 10})
    {1, 10}

---

*Consult [api-reference.md](api-reference.md) for complete listing*
