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
@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ó.
@type encoding() :: :utf8 | :latin1
@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
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}
true si todos los bytes están bajo 128.
iex> AnchoFijo.Transcodificacion.ascii?("ABC")
true
iex> AnchoFijo.Transcodificacion.ascii?("ABÇ")
false
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"
@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"
@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
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
true si el binario es UTF-8 válido.
iex> AnchoFijo.Transcodificacion.utf8?(<<74, 79, 83, 201>>)
false