# `AnchoFijo.Transcodificacion`
[🔗](https://github.com/galenzo17/ancho_fijo/blob/v0.1.0/lib/ancho_fijo/transcodificacion.ex#L1)

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.

# `detalle`

```elixir
@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`

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

# `ubicacion`

```elixir
@type ubicacion() :: %{
  :posicion =&gt; non_neg_integer(),
  :byte =&gt; non_neg_integer(),
  optional(atom()) =&gt; term()
}
```

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

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

# `a_utf8`

```elixir
@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?`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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`

```elixir
@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?`

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

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

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
