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)
52Las 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 deAnchoFijo.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
@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
@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
@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}
@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}
@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 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]
@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"
@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