diff options
Diffstat (limited to 'lib/cepis')
| -rw-r--r-- | lib/cepis/error.ex | 12 | ||||
| -rw-r--r-- | lib/cepis/faixa.ex | 124 |
2 files changed, 134 insertions, 2 deletions
diff --git a/lib/cepis/error.ex b/lib/cepis/error.ex index bd86b65..2391c8c 100644 --- a/lib/cepis/error.ex +++ b/lib/cepis/error.ex @@ -21,11 +21,19 @@ defmodule Cepis.NotFoundError do `cep` holds the normalised 8 digits that were looked up. Retrying does not help: the answer is definitive. + + `faixa` is a `Cepis.Faixa` naming the municipality that CEP *would* have + belonged to, or `nil`. It is context for the failure and **not** an address — + this is still a not-found, and the CEP is still not in the database. It is + `nil` whenever the number falls in a band no municipality owns, which the API + signals by omitting the field rather than sending null. """ - defexception [:message, :cep] + alias Cepis.Faixa + + defexception [:message, :cep, :faixa] - @type t :: %__MODULE__{message: String.t(), cep: String.t()} + @type t :: %__MODULE__{message: String.t(), cep: String.t(), faixa: Faixa.t() | nil} end defmodule Cepis.NetworkError do diff --git a/lib/cepis/faixa.ex b/lib/cepis/faixa.ex new file mode 100644 index 0000000..ab85e8d --- /dev/null +++ b/lib/cepis/faixa.ex @@ -0,0 +1,124 @@ +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 |