AnchoFijo.Campo (ancho_fijo v0.1.0)

Copy Markdown View Source

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

tipovalor
:textoString.t() ya transcodificado a UTF-8
:enterointeger()
:decimal{unidades, precision}, p. ej. {123456, 2} para 1234.56
:fechaDate.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.

Summary

Types

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()

Functions

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

Última posición que ocupa el campo.

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

Valida y construye un campo.

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

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

Types

contexto()

@type contexto() :: %{
  optional(:unidad) => :bytes | :caracteres,
  optional(:encoding) => :utf8 | :latin1,
  optional(:linea) => 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()

@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()

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

valor()

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

Functions

extraer(campo, linea, contexto \\ %{})

@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(campo)

@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(arg1)

@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(atributos)

@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!(atributos)

@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(campo)

@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}