prop/test/microwaveprop/radio/maidenhead_property_test.exs

150 lines
4.8 KiB
Elixir
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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