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), &<>) # 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), &<>) defp digit_char, do: map(integer(0..9), &<>) # 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