1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
defmodule Cepis.Geo do
@moduledoc """
The CEP's coordinate, when the database has one.
The point is the **median** of the coordinates of that CEP's addresses, not
the mean: one badly geocoded address 40 km away shifts a mean and does not
move a median.
Always check `precisao` before using `lat`/`lon` for distance:
- `"logradouro"`: the point is a stretch of street. `raio_m` lands around
100–150 m. Good for maps and for distance.
- `"localidade"`: the CEP *is* the whole municipality (a city-wide general
CEP), and the point is its centroid. `raio_m` runs from 7 km to 40 km.
Computing shipping from this is wrong by tens of kilometres.
`raio_m` is the radius containing 90% of that CEP's addresses. It exists
precisely so that error cannot pass unnoticed.
"""
@enforce_keys [:lat, :lon, :raio_m, :precisao]
defstruct [:lat, :lon, :raio_m, :precisao]
@type t :: %__MODULE__{
lat: float(),
lon: float(),
raio_m: integer(),
precisao: String.t()
}
@doc """
`true` when the coordinate points at a street, not a municipality.
Shorthand for `precisao == "logradouro"`, which is the check to make before
using the point to measure distance.
if geo && Cepis.Geo.precise?(geo) do
shipping_from(geo.lat, geo.lon)
else
shipping_from_municipality(address.ibge)
end
"""
@spec precise?(t()) :: boolean()
def precise?(%__MODULE__{precisao: precisao}), do: precisao == "logradouro"
@doc false
@spec from_map(term()) :: t() | nil
def from_map(%{"lat" => lat, "lon" => lon, "raio_m" => raio_m, "precisao" => precisao})
when is_binary(precisao) do
%__MODULE__{
lat: to_float(lat),
lon: to_float(lon),
raio_m: to_integer(raio_m),
precisao: precisao
}
rescue
ArgumentError -> nil
end
def from_map(_), do: nil
defp to_float(value) when is_float(value), do: value
defp to_float(value) when is_integer(value), do: value * 1.0
defp to_float(value) when is_binary(value) do
case Float.parse(value) do
{number, ""} -> number
_ -> raise ArgumentError, "not a number"
end
end
defp to_float(_), do: raise(ArgumentError, "not a number")
defp to_integer(value) when is_integer(value), do: value
defp to_integer(value) when is_float(value), do: trunc(value)
defp to_integer(value) when is_binary(value) do
case Integer.parse(value) do
{number, ""} -> number
_ -> raise ArgumentError, "not an integer"
end
end
defp to_integer(_), do: raise(ArgumentError, "not an integer")
end
|