AnchoFijo.Layout (ancho_fijo v0.1.0)

Copy Markdown View Source

La definición del formato, como data.

Un layout es una lista de campos más tres decisiones globales: en qué encoding viene el archivo, si las posiciones se cuentan en bytes o en caracteres, y qué largo total se espera por línea. Cambiar de formato es cambiar este mapa, no escribir código.

iex> alias AnchoFijo.Layout
iex> {:ok, layout} = Layout.nuevo(
...>   nombre: "nómina banco X",
...>   encoding: :latin1,
...>   campos: [
...>     [nombre: :rut, largo: 10],
...>     [nombre: :beneficiario, largo: 30],
...>     [nombre: :monto, largo: 12, tipo: :decimal, precision: 2]
...>   ]
...> )
iex> Layout.largo(layout)
52

Las posiciones son 1-based porque así vienen en todas las especificaciones bancarias del mundo: cuando el anexo dice "posiciones 21 a 32", el layout dice lo mismo. Si se omiten, cada campo se encadena al anterior.

Validación de la definición misma

nuevo/1 revisa el layout antes de ver un archivo. Distingue dos gravedades, y la distinción es de dominio: un solapamiento es siempre un error de transcripción del anexo, mientras que un hueco suele ser una zona reservada legítima del formato. Los huecos quedan en :advertencias y no impiden parsear.

iex> alias AnchoFijo.Layout
iex> {:error, [diagnostico]} = Layout.nuevo(campos: [
...>   [nombre: :rut, posicion: 1, largo: 10],
...>   [nombre: :nombre, posicion: 8, largo: 20]
...> ])
iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
"layout, campo :nombre (posiciones 8-27): se esperaba que empezara en la posición 11 o después, llegó 8; se solapa con el campo :rut (posiciones 1-10)"

iex> alias AnchoFijo.Layout
iex> {:ok, layout} = Layout.nuevo(campos: [
...>   [nombre: :rut, posicion: 1, largo: 10],
...>   [nombre: :nombre, posicion: 14, largo: 20]
...> ])
iex> AnchoFijo.Diagnostico.mensaje(hd(layout.advertencias))
"layout, campo :nombre (posiciones 14-33): se esperaba que empezara en la posición 11, llegó 14; quedan 3 posiciones sin declarar entre :rut y :nombre; si el formato tiene relleno ahí, ignore esta advertencia"

Opciones

  • :campos — obligatorio. Lista de definiciones de AnchoFijo.Campo (keyword lists, mapas o structs ya construidos).
  • :encoding:utf8 (default) o :latin1.
  • :unidad:bytes (default) o :caracteres. Ver más abajo.
  • :largo — largo total esperado por línea. Si se omite, se infiere del último campo.
  • :nombre — etiqueta libre para identificar el layout en logs.

Bytes o caracteres

El default es :bytes porque un formato de ancho fijo se define sobre el archivo físico: cuando el banco dice "120 posiciones", cuenta bytes. Con latin-1 da lo mismo, un byte es un carácter. Con UTF-8 no: si el archivo trae acentos y el emisor contó caracteres, hay que declarar unidad: :caracteres o cada línea con una "ñ" se corre un byte.

Summary

Functions

Busca un campo por nombre.

Acepta un layout ya construido o una definición y devuelve siempre {:ok, layout}.

Contexto de lectura que el layout entrega a cada campo.

Largo total de línea que el layout espera.

Nombres de los campos, en orden de posición.

Valida y construye un layout.

Igual que nuevo/1 pero levanta AnchoFijo.Error con el reporte completo.

Types

t()

@type t() :: %AnchoFijo.Layout{
  advertencias: [AnchoFijo.Diagnostico.t()],
  campos: [AnchoFijo.Campo.t()],
  encoding: :utf8 | :latin1,
  largo: pos_integer(),
  nombre: String.t() | nil,
  unidad: :bytes | :caracteres
}

Functions

campo(layout, nombre)

@spec campo(t(), atom()) :: AnchoFijo.Campo.t() | nil

Busca un campo por nombre.

iex> layout = AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2], [nombre: :b, largo: 3]])
iex> AnchoFijo.Layout.campo(layout, :b) |> AnchoFijo.Campo.rango()
{3, 5}
iex> AnchoFijo.Layout.campo(layout, :inexistente)
nil

coercer(layout)

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

Acepta un layout ya construido o una definición y devuelve siempre {:ok, layout}.

Es lo que usan AnchoFijo.parsear/3 y AnchoFijo.stream/3 para que el llamador pueda pasar la definición en línea sin ceremonia.

iex> {:ok, layout} = AnchoFijo.Layout.coercer(campos: [[nombre: :a, largo: 2]])
iex> AnchoFijo.Layout.coercer(layout)
{:ok, layout}

contexto(layout, linea)

@spec contexto(t(), pos_integer() | nil) :: AnchoFijo.Campo.contexto()

Contexto de lectura que el layout entrega a cada campo.

iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2]], encoding: :latin1)
...> |> AnchoFijo.Layout.contexto(9)
%{encoding: :latin1, unidad: :bytes, linea: 9}

largo(layout)

@spec largo(t()) :: pos_integer()

Largo total de línea que el layout espera.

iex> AnchoFijo.Layout.largo(AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 7]]))
7

nombres(layout)

@spec nombres(t()) :: [atom()]

Nombres de los campos, en orden de posición.

iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2], [nombre: :b, largo: 3]])
...> |> AnchoFijo.Layout.nombres()
[:a, :b]

nuevo(atributos)

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

Valida y construye un layout.

Devuelve {:ok, layout} —posiblemente con :advertencias— o {:error, diagnosticos} con todos los problemas encontrados, no solo el primero: corregir una definición de 40 campos de a un error por compilación es una forma lenta de perder el día.

iex> {:ok, layout} = AnchoFijo.Layout.nuevo(campos: [[nombre: :codigo, largo: 4, tipo: :entero]])
iex> layout.largo
4

iex> {:error, [diagnostico]} = AnchoFijo.Layout.nuevo(campos: [])
iex> AnchoFijo.Diagnostico.mensaje(diagnostico)
"layout: se esperaba al menos un campo en :campos, llegó una lista vacía; un layout sin campos no puede leer nada"

nuevo!(atributos)

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

Igual que nuevo/1 pero levanta AnchoFijo.Error con el reporte completo.

iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 2]]).unidad
:bytes

iex> AnchoFijo.Layout.nuevo!(campos: [[nombre: :a, largo: 0]])
** (AnchoFijo.Error) layout, campo :a: se esperaba un :largo entero mayor que cero, llegó 0; revise la especificación del formato