summaryrefslogtreecommitdiffstats
path: root/lib/cepis/faixa.ex
diff options
context:
space:
mode:
Diffstat (limited to 'lib/cepis/faixa.ex')
-rw-r--r--lib/cepis/faixa.ex124
1 files changed, 124 insertions, 0 deletions
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