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

Lee registros de ancho fijo según un layout.

Es la parte aburrida del paquete, y así debería ser: si `AnchoFijo.Detector`
hizo su trabajo, el parser no descubre sorpresas.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [
    ...>   [nombre: :codigo, largo: 4, tipo: :entero],
    ...>   [nombre: :monto, largo: 8, tipo: :decimal, precision: 2]
    ...> ])
    iex> AnchoFijo.Parser.parsear(layout, "001000123456\n0020no-monto\n", modo: :tolerante)
    {:ok, [%{codigo: 10, monto: {123456, 2}}],
     [
       %AnchoFijo.Diagnostico{
         tipo: :campo_invalido,
         linea: 2,
         campo: :monto,
         posicion: {5, 12},
         esperado: "8 dígitos con 2 decimales implícitos",
         recibido: "\"no-monto\"",
         causa_probable: "el monto trae un separador decimal explícito o un carácter no numérico; si el archivo usa punto o coma, declare separador: :punto o :coma",
         contenido: nil
       }
     ]}

## Los dos modos

`:estricto` (default) corta en la primera línea con problemas y devuelve
`{:error, diagnosticos}`. Sirve para archivos que son contratos: una cartola
que no cuadra no se procesa a medias.

`:tolerante` procesa todo y devuelve `{:ok, registros, diagnosticos}`. Existe
porque en producción una nómina de 10.000 filas con 3 malas debe reportar las
3, no botar el lote completo; y porque el operador que corrige el archivo
necesita la lista completa de errores, no el primero de una serie de treinta.

Nótese que el modo tolerante devuelve `:ok`, no `:error`: un lote con filas
malas separadas de las buenas es un resultado, no una falla. `{:error, _}`
queda para lo que impide procesar cualquier cosa —layout inválido, archivo
ilegible—, donde no hay nada que rescatar.

## Una línea, un diagnóstico por causa

Si una línea no mide el largo declarado, se reporta solo eso y no se intentan
leer sus campos. Un byte faltante corre todos los campos que vienen después:
reportar los 12 diagnósticos derivados esconde el único que importa.

## Opciones

  * `:modo` — `:estricto` (default) o `:tolerante`.
  * `:saltar` — cantidad de líneas iniciales a ignorar (headers). Default 0.
  * `:omitir_vacias` — ignorar líneas en blanco. Default `true`, porque el
    salto final del archivo es una línea vacía y no un registro corrupto.
  * `:desde` — `:auto` (default), `:archivo` o `:contenido`.

# `registro`

```elixir
@type registro() :: %{optional(atom()) =&gt; AnchoFijo.Campo.valor()}
```

# `resultado`

```elixir
@type resultado() ::
  {:ok, [registro()]}
  | {:ok, [registro()], [AnchoFijo.Diagnostico.t()]}
  | {:error, [AnchoFijo.Diagnostico.t()]}
```

# `parsear`

```elixir
@spec parsear(AnchoFijo.Layout.t() | Enumerable.t(), term(), keyword()) :: resultado()
```

Parsea un archivo completo en memoria.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :sigla, largo: 3], [nombre: :saldo, largo: 6, tipo: :entero]])
    iex> AnchoFijo.Parser.parsear(layout, "CLP001234\nUSD000567\n")
    {:ok, [%{sigla: "CLP", saldo: 1234}, %{sigla: "USD", saldo: 567}]}

En modo estricto, la primera línea mala corta el proceso:

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :sigla, largo: 3], [nombre: :saldo, largo: 6, tipo: :entero]])
    iex> {:error, [diagnostico]} = AnchoFijo.Parser.parsear(layout, "CLP001234\nUSD00056\n")
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "línea 2: se esperaban 9 bytes, llegaron 8; posible campo faltante o archivo delimitado"

# `parsear_linea`

```elixir
@spec parsear_linea(AnchoFijo.Layout.t(), binary(), pos_integer()) ::
  {:ok, registro()} | {:error, [AnchoFijo.Diagnostico.t()]}
```

Parsea una sola línea, ya sin terminador.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2], [nombre: :b, largo: 3, tipo: :entero]])
    iex> AnchoFijo.Parser.parsear_linea(layout, "XY007", 1)
    {:ok, %{a: "XY", b: 7}}

El caso más común: un archivo latin-1 leído con el encoding por default.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :nombre, largo: 5]])
    iex> {:error, [diagnostico]} = AnchoFijo.Parser.parsear_linea(layout, <<74, 79, 83, 201, 32>>, 3)
    iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
    "línea 3, campo :nombre (posiciones 1-5): se esperaban bytes válidos en utf8, llegaron el byte 0xC9 en la posición 4; el byte 0xC9 no es UTF-8 válido pero sí es un carácter latin-1; declare encoding: :latin1 en el layout"

# `stream`

```elixir
@spec stream(AnchoFijo.Layout.t() | Enumerable.t(), term(), keyword()) ::
  Enumerable.t()
```

Versión lazy de `parsear/3`, para archivos que no caben en memoria.

Devuelve un `Stream` de `{:ok, registro}` o `{:error, diagnosticos}`, una
entrada por línea con contenido. La decisión de qué hacer con los errores es
del consumidor, que es la única forma honesta de ser lazy: acumular todos los
diagnósticos de un archivo de 2 GB para devolverlos al final anula el punto.

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :codigo, largo: 3, tipo: :entero]])
    iex> AnchoFijo.Parser.stream(layout, "001\n002\nXYZ\n") |> Enum.count(&match?({:ok, _}, &1))
    2

Para replicar el modo estricto sobre un stream, corte usted mismo:

    iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :codigo, largo: 3, tipo: :entero]])
    iex> AnchoFijo.Parser.stream(layout, "001\n002\nXYZ\n004\n")
    ...> |> Enum.reduce_while([], fn
    ...>   {:ok, registro}, acumulado -> {:cont, [registro | acumulado]}
    ...>   {:error, _diagnosticos}, acumulado -> {:halt, Enum.reverse(acumulado)}
    ...> end)
    [%{codigo: 1}, %{codigo: 2}]

Acepta un `File.Stream` ya construido, lo que permite elegir el tamaño de
lectura o leer desde una tubería:

    AnchoFijo.Parser.stream(layout, File.stream!("cartola.txt"))

Un layout inválido levanta `AnchoFijo.Error` en vez de devolver un stream:
es un error del programador, no del archivo. Un archivo ilegible sí se
reporta como dato, en el primer elemento del stream.

Con `File.stream!/1` las líneas se cortan solo por `\n`; si el archivo usa CR
a secas (Mac clásico, que `detectar/2` identifica), use `parsear/3`.

---

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