AnchoFijo.Transcodificacion (ancho_fijo v0.1.0)

Copy Markdown View Source

Decodifica bytes a UTF-8 y, cuando no puede, explica por qué.

El encoding es el primer sospechoso de cualquier integración bancaria: los mainframes emiten latin-1, los ERP modernos UTF-8, y nadie lo declara en el archivo. Este módulo convierte y diagnostica; nunca levanta una excepción por un byte raro.

iex> AnchoFijo.Transcodificacion.a_utf8(<<74, 79, 83, 201>>, :latin1)
{:ok, "JOSÉ"}

iex> {:error, detalle} = AnchoFijo.Transcodificacion.a_utf8(<<74, 79, 83, 201, 32>>, :utf8)
iex> detalle.motivo
:bytes_invalidos
iex> AnchoFijo.Transcodificacion.causa_probable(detalle, :utf8)
"el byte 0xC9 no es UTF-8 válido pero sí es un carácter latin-1; declare encoding: :latin1 en el layout"

Caracteres de control

Ambos encodings rechazan los caracteres de control (0x00–0x1F menos el tabulador, y 0x7F–0x9F). No es purismo: el rango 0x80–0x9F es control en latin-1 pero contiene comillas y guiones en Windows-1252, así que encontrarlo ahí casi siempre significa que el archivo es cp1252 y no latin-1. Preferimos decirlo a devolver basura silenciosa.

Summary

Types

Detalle de una falla de decodificación.

Lo mínimo que necesita describir_byte/2: un offset y el byte que lo ocupa.

Functions

Convierte un binario a UTF-8 según el encoding declarado.

true si todos los bytes están bajo 128.

Hipótesis de por qué falló la decodificación, según el byte y el encoding declarado.

Redacta el byte problemático con su posición absoluta en la línea.

Encoding más probable de un binario.

Ubica el primer byte que rompe UTF-8, o nil si el binario está sano.

true si el binario es UTF-8 válido.

Types

detalle()

@type detalle() :: %{
  motivo: :bytes_invalidos | :secuencia_incompleta | :caracter_de_control,
  posicion: non_neg_integer(),
  byte: non_neg_integer()
}

Detalle de una falla de decodificación.

:posicion es el offset 0-based en bytes dentro del binario inspeccionado, y :byte es el byte —o el codepoint, si el problema es un control en UTF-8— que la provocó.

encoding()

@type encoding() :: :utf8 | :latin1

ubicacion()

@type ubicacion() :: %{
  :posicion => non_neg_integer(),
  :byte => non_neg_integer(),
  optional(atom()) => term()
}

Lo mínimo que necesita describir_byte/2: un offset y el byte que lo ocupa.

Todo detalle/0 sirve, pero también un mapa armado a mano por quien ya guardó esos dos datos en otra parte.

Functions

a_utf8(binario, atom)

@spec a_utf8(binary(), encoding()) :: {:ok, String.t()} | {:error, detalle()}

Convierte un binario a UTF-8 según el encoding declarado.

iex> AnchoFijo.Transcodificacion.a_utf8("MARIA", :utf8)
{:ok, "MARIA"}

iex> AnchoFijo.Transcodificacion.a_utf8("MARÍA", :utf8)
{:ok, "MARÍA"}

iex> {:error, detalle} = AnchoFijo.Transcodificacion.a_utf8(<<77, 65, 0, 65>>, :latin1)
iex> {detalle.motivo, detalle.posicion, detalle.byte}
{:caracter_de_control, 2, 0}

ascii?(binario)

@spec ascii?(binary()) :: boolean()

true si todos los bytes están bajo 128.

iex> AnchoFijo.Transcodificacion.ascii?("ABC")
true
iex> AnchoFijo.Transcodificacion.ascii?("ABÇ")
false

causa_probable(map, encoding)

@spec causa_probable(detalle(), encoding()) :: String.t()

Hipótesis de por qué falló la decodificación, según el byte y el encoding declarado.

iex> detalle = %{motivo: :caracter_de_control, posicion: 4, byte: 0x93}
iex> AnchoFijo.Transcodificacion.causa_probable(detalle, :latin1)
"0x93 es un carácter de control en latin-1 pero una comilla en Windows-1252; el archivo probablemente es cp1252"

describir_byte(map, base)

@spec describir_byte(ubicacion(), pos_integer()) :: String.t()

Redacta el byte problemático con su posición absoluta en la línea.

base es la posición 1-based donde empieza el binario inspeccionado, de modo que el número que sale en el diagnóstico sea el que el usuario puede contar en su editor.

iex> detalle = %{motivo: :bytes_invalidos, posicion: 2, byte: 241}
iex> AnchoFijo.Transcodificacion.describir_byte(detalle, 11)
"el byte 0xF1 en la posición 13"

detectar(binario)

@spec detectar(binary()) :: :ascii | :utf8 | :latin1

Encoding más probable de un binario.

:ascii significa que ambos encodings dan el mismo resultado, así que la pregunta no importa para ese archivo.

iex> AnchoFijo.Transcodificacion.detectar("SOLO ASCII")
:ascii
iex> AnchoFijo.Transcodificacion.detectar("JOSÉ")
:utf8
iex> AnchoFijo.Transcodificacion.detectar(<<74, 79, 83, 201>>)
:latin1

primer_byte_invalido(binario)

@spec primer_byte_invalido(binary()) :: detalle() | nil

Ubica el primer byte que rompe UTF-8, o nil si el binario está sano.

iex> AnchoFijo.Transcodificacion.primer_byte_invalido(<<65, 66, 241, 67>>)
%{motivo: :bytes_invalidos, posicion: 2, byte: 241}

iex> AnchoFijo.Transcodificacion.primer_byte_invalido("ABC")
nil

utf8?(binario)

@spec utf8?(binary()) :: boolean()

true si el binario es UTF-8 válido.

iex> AnchoFijo.Transcodificacion.utf8?(<<74, 79, 83, 201>>)
false