150 lines
4.8 KiB
Elixir
150 lines
4.8 KiB
Elixir
defmodule Microwaveprop.Radio.MaidenheadPropertyTest do
|
||
use ExUnit.Case, async: true
|
||
use ExUnitProperties
|
||
|
||
alias Microwaveprop.Radio.Maidenhead
|
||
|
||
# Generators ---------------------------------------------------------
|
||
|
||
# Upper-case field chars A-R (18 divisions, the only canonical output
|
||
# from from_latlon/3).
|
||
defp field_char, do: map(integer(0..17), &<<?A + &1>>)
|
||
|
||
# Upper-case subsquare chars A-X (24 divisions). from_latlon/3 emits
|
||
# lower-case, so we hold grids canonical in lower-case when assembling.
|
||
defp subsquare_char, do: map(integer(0..23), &<<?a + &1>>)
|
||
|
||
defp digit_char, do: map(integer(0..9), &<<?0 + &1>>)
|
||
|
||
# Canonical 4-char grid: UPPER field + digits. to_latlon upcases, so
|
||
# staying canonical here gives us exact round-trip equality.
|
||
defp grid4 do
|
||
gen all(
|
||
f1 <- field_char(),
|
||
f2 <- field_char(),
|
||
d1 <- digit_char(),
|
||
d2 <- digit_char()
|
||
) do
|
||
f1 <> f2 <> d1 <> d2
|
||
end
|
||
end
|
||
|
||
defp grid6 do
|
||
gen all(
|
||
g <- grid4(),
|
||
s1 <- subsquare_char(),
|
||
s2 <- subsquare_char()
|
||
) do
|
||
g <> s1 <> s2
|
||
end
|
||
end
|
||
|
||
defp grid8 do
|
||
gen all(
|
||
g <- grid6(),
|
||
d1 <- digit_char(),
|
||
d2 <- digit_char()
|
||
) do
|
||
g <> d1 <> d2
|
||
end
|
||
end
|
||
|
||
# Lat/lon that stay inside the representable field range. The top row
|
||
# (lat = 90.0) and the wrap-around longitude (180.0) sit exactly on a
|
||
# field boundary and land outside the A-R field set, so we stop just
|
||
# short.
|
||
defp latlon do
|
||
gen all(
|
||
lat <- float(min: -90.0, max: 89.999),
|
||
lon <- float(min: -180.0, max: 179.999)
|
||
) do
|
||
{lat, lon}
|
||
end
|
||
end
|
||
|
||
# Properties ---------------------------------------------------------
|
||
|
||
describe "round-trip from_latlon/3 → to_latlon/1" do
|
||
property "precision 4: decoded center is within one grid cell of input" do
|
||
# A 4-char grid covers 2° lon × 1° lat; the center must be within
|
||
# half that distance of any point in the cell.
|
||
check all({lat, lon} <- latlon()) do
|
||
grid = Maidenhead.from_latlon(lat, lon, 4)
|
||
assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
|
||
assert abs(dlat - lat) <= 0.5 + 1.0e-9
|
||
assert abs(dlon - lon) <= 1.0 + 1.0e-9
|
||
end
|
||
end
|
||
|
||
property "precision 6: decoded center is within half a subsquare" do
|
||
# 6-char subsquare is 5'×2.5' → 5/60 lon, 2.5/60 lat.
|
||
check all({lat, lon} <- latlon()) do
|
||
grid = Maidenhead.from_latlon(lat, lon, 6)
|
||
assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
|
||
assert abs(dlat - lat) <= 2.5 / 60 / 2 + 1.0e-9
|
||
assert abs(dlon - lon) <= 5.0 / 60 / 2 + 1.0e-9
|
||
end
|
||
end
|
||
|
||
property "precision 8: decoded center is within half an extended square" do
|
||
check all({lat, lon} <- latlon()) do
|
||
grid = Maidenhead.from_latlon(lat, lon, 8)
|
||
assert {:ok, {dlat, dlon}} = Maidenhead.to_latlon(grid)
|
||
assert abs(dlat - lat) <= 2.5 / 60 / 10 / 2 + 1.0e-9
|
||
assert abs(dlon - lon) <= 5.0 / 60 / 10 / 2 + 1.0e-9
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "round-trip to_latlon/1 → from_latlon/3" do
|
||
property "precision 4 grids come back unchanged (after upcasing)" do
|
||
check all(grid <- grid4()) do
|
||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
|
||
assert Maidenhead.from_latlon(lat, lon, 4) == String.upcase(grid)
|
||
end
|
||
end
|
||
|
||
property "precision 6 grids come back unchanged" do
|
||
# from_latlon/3 emits upper-case field/square and lower-case
|
||
# subsquare, matching the canonical form our generator produces.
|
||
check all(grid <- grid6()) do
|
||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
|
||
assert Maidenhead.from_latlon(lat, lon, 6) == grid
|
||
end
|
||
end
|
||
|
||
property "precision 8 grids come back unchanged" do
|
||
check all(grid <- grid8()) do
|
||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
|
||
assert Maidenhead.from_latlon(lat, lon, 8) == grid
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "decoded bounds" do
|
||
property "to_latlon output stays inside valid geographic range" do
|
||
check all(grid <- grid8()) do
|
||
assert {:ok, {lat, lon}} = Maidenhead.to_latlon(grid)
|
||
assert lat >= -90.0 and lat <= 90.0
|
||
assert lon >= -180.0 and lon <= 180.0
|
||
end
|
||
end
|
||
end
|
||
|
||
describe "valid?/1" do
|
||
property "accepts every grid emitted by from_latlon/3" do
|
||
check all(
|
||
{lat, lon} <- latlon(),
|
||
precision <- one_of([constant(4), constant(6), constant(8)])
|
||
) do
|
||
assert Maidenhead.valid?(Maidenhead.from_latlon(lat, lon, precision))
|
||
end
|
||
end
|
||
|
||
property "never crashes on arbitrary binary input" do
|
||
check all(s <- string(:printable)) do
|
||
assert is_boolean(Maidenhead.valid?(s))
|
||
end
|
||
end
|
||
end
|
||
end
|