defmodule Cepis.Faixa do @moduledoc """ The municipality a CEP that does **not** exist would have belonged to. It arrives on `Cepis.NotFoundError`, and nowhere else. **It is context for an error, not an address**: the lookup failed, the CEP is not in the DNE, and nothing here says otherwise. Using it as a fallback address is the one wrong use. It is worth having because a failing CEP is rarely random — it is almost always someone who knows the city and guessed the number, most often the `-000`, which 957 municipalities never received. Naming the municipality turns "does not exist" into "you are in Areal, but not at that number", which is the difference between a dead end and a form the user can fix. **Field names are the API's own, in Portuguese**, as everywhere else in this library: `localidade`, `uf`, `estado`, `regiao`, `ibge`, `cep_ini`, `cep_fim`, `ceps`. > #### The range is sparse {: .warning} > > `cep_ini` and `cep_fim` are the smallest and the largest CEP that > **actually exist** in that municipality — not the bounds the Correios > publish. The DNE's own start for Areal is `25845000`, which is exactly the > CEP that just failed; echoing it back would be useless. The numbers between > the two bounds are mostly not CEPs at all: Areal's 118 are scattered across > thousands of them. That is what `ceps` is for, and what `density/1` > measures. Never expand the pair into a list of valid CEPs. The API **omits** `faixa`, never sends it as null, when the CEP falls in a numeric band no municipality owns. So `nil` is an ordinary answer, not a failure, and every caller has to handle it. """ defstruct localidade: "", uf: "", estado: "", regiao: "", ibge: "", cep_ini: "", cep_fim: "", ceps: 0 @type t :: %__MODULE__{ localidade: String.t(), uf: String.t(), estado: String.t(), regiao: String.t(), ibge: String.t(), cep_ini: String.t(), cep_fim: String.t(), ceps: non_neg_integer() } @doc """ The share of the numbers between `cep_ini` and `cep_fim` that are real CEPs. About `0.024` for Areal — 118 CEPs over the 4899 numbers between `25845001` and `25849899`. It exists so the sparsity is measurable instead of being a footnote nobody reads before iterating over the interval: if Cepis.Faixa.density(faixa) < 0.5 do # guessing another number in the range is not going to work suggest_search_by_street(faixa.localidade, faixa.uf) end Returns `0.0` when the bounds are not two readable numbers. """ @spec density(t()) :: float() def density(%__MODULE__{ceps: ceps, cep_ini: cep_ini, cep_fim: cep_fim}) do with {first, ""} <- Integer.parse(cep_ini), {last, ""} <- Integer.parse(cep_fim), true <- last >= first do ceps / (last - first + 1) else _ -> 0.0 end end @doc false @spec from_map(term()) :: t() | nil def from_map(%{"localidade" => localidade} = data) when is_binary(localidade) and localidade != "" do %__MODULE__{ localidade: localidade, uf: text(data, "uf"), estado: text(data, "estado"), regiao: text(data, "regiao"), ibge: text(data, "ibge"), cep_ini: text(data, "cep_ini"), cep_fim: text(data, "cep_fim"), ceps: count(data, "ceps") } end def from_map(_), do: nil defp text(data, key) do case Map.get(data, key) do value when is_binary(value) -> value value when is_number(value) -> to_string(value) _ -> "" end end defp count(data, key) do case Map.get(data, key) do value when is_integer(value) -> value value when is_float(value) -> trunc(value) value when is_binary(value) -> case Integer.parse(value) do {number, _} -> number :error -> 0 end _ -> 0 end end end