Spots that include subsquare (EM12kl), extended-square (EM12kl37), or extended-extended-subsquare (EM12kl37ab) precision now keep that resolution end-to-end. Previously we truncated everything to 4 chars at parse time, which collapsed distinct paths into the same row whenever two spotters shared a field. The aggregation key uses the full grid as reported, so two spotters in different subsquares of `EM12` produce two rows — correct, since they are physically different paths. Any spot that fails Maidenhead validation (odd length, illegal char at position N) is still dropped via the same error path. Storage normalizes to uppercase so the unique index doesn't split on case. Maidenhead.valid?/1 already accepts both cases on input.
127 lines
3.9 KiB
Elixir
127 lines
3.9 KiB
Elixir
defmodule Microwaveprop.PskrTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
alias Microwaveprop.Pskr
|
|
|
|
@sample %{
|
|
"sq" => 30_142_870_791,
|
|
"f" => 144_174_000,
|
|
"md" => "FT8",
|
|
"rp" => -5,
|
|
"t" => 1_662_407_712,
|
|
"t_tx" => 1_662_407_697,
|
|
"sc" => "K5ABC",
|
|
"sl" => "EM12kl",
|
|
"rc" => "K7XYZ",
|
|
"rl" => "DM43st",
|
|
"sa" => 291,
|
|
"ra" => 291,
|
|
"b" => "2m"
|
|
}
|
|
|
|
describe "parse_spot/1" do
|
|
test "preserves full subsquare precision when reported" do
|
|
json = Jason.encode!(@sample)
|
|
|
|
assert {:ok, spot} = Pskr.parse_spot(json)
|
|
|
|
assert spot.band == "2m"
|
|
assert spot.mode == "FT8"
|
|
assert spot.snr_db == -5
|
|
assert spot.sender_grid == "EM12KL"
|
|
assert spot.receiver_grid == "DM43ST"
|
|
assert spot.sender_country == 291
|
|
assert spot.receiver_country == 291
|
|
assert %DateTime{} = spot.transmit_time
|
|
assert DateTime.to_unix(spot.transmit_time) == 1_662_407_697
|
|
end
|
|
|
|
test "accepts 4-char grids verbatim" do
|
|
json = @sample |> Map.merge(%{"sl" => "EM12", "rl" => "DM43"}) |> Jason.encode!()
|
|
|
|
assert {:ok, spot} = Pskr.parse_spot(json)
|
|
assert spot.sender_grid == "EM12"
|
|
assert spot.receiver_grid == "DM43"
|
|
end
|
|
|
|
test "preserves extended-square (8-char) precision" do
|
|
json = @sample |> Map.merge(%{"sl" => "EM12kl37", "rl" => "DM43st99"}) |> Jason.encode!()
|
|
|
|
assert {:ok, spot} = Pskr.parse_spot(json)
|
|
assert spot.sender_grid == "EM12KL37"
|
|
assert spot.receiver_grid == "DM43ST99"
|
|
end
|
|
|
|
test "rejects odd-length grids (truncated 5-char)" do
|
|
json = @sample |> Map.put("sl", "EM12k") |> Jason.encode!()
|
|
assert {:error, _} = Pskr.parse_spot(json)
|
|
end
|
|
|
|
test "uses :t when :t_tx is missing" do
|
|
json = @sample |> Map.delete("t_tx") |> Jason.encode!()
|
|
|
|
assert {:ok, spot} = Pskr.parse_spot(json)
|
|
assert DateTime.to_unix(spot.transmit_time) == 1_662_407_712
|
|
end
|
|
|
|
test "rejects payloads without grids" do
|
|
json = @sample |> Map.delete("sl") |> Jason.encode!()
|
|
assert {:error, _} = Pskr.parse_spot(json)
|
|
end
|
|
|
|
test "rejects malformed JSON" do
|
|
assert {:error, _} = Pskr.parse_spot("not-json")
|
|
end
|
|
end
|
|
|
|
describe "path_key/1" do
|
|
test "buckets to the top of the hour using the full grid as reported" do
|
|
{:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
|
|
{hour, band, snd, rcv} = Pskr.path_key(spot)
|
|
|
|
assert band == "2m"
|
|
assert snd == "EM12KL"
|
|
assert rcv == "DM43ST"
|
|
# 1_662_407_697 → 2022-09-05T19:54:57Z → bucket at 19:00:00Z
|
|
assert hour == ~U[2022-09-05 19:00:00Z]
|
|
end
|
|
end
|
|
|
|
describe "merge_spot/2" do
|
|
test "initializes accumulator from a single spot" do
|
|
{:ok, spot} = Pskr.parse_spot(Jason.encode!(@sample))
|
|
|
|
acc = Pskr.merge_spot(nil, spot)
|
|
|
|
assert acc.spot_count == 1
|
|
assert acc.max_snr_db == -5
|
|
assert acc.min_snr_db == -5
|
|
assert acc.modes == ["FT8"]
|
|
assert acc.sender_grid == "EM12KL"
|
|
assert acc.receiver_grid == "DM43ST"
|
|
assert is_float(acc.distance_km)
|
|
assert acc.distance_km > 0
|
|
# Midpoint should land between the two grid centers
|
|
assert acc.midpoint_lat > min(acc.sender_lat, acc.receiver_lat)
|
|
assert acc.midpoint_lat < max(acc.sender_lat, acc.receiver_lat)
|
|
end
|
|
|
|
test "increments count and tracks SNR envelope across spots" do
|
|
{:ok, first} = Pskr.parse_spot(Jason.encode!(@sample))
|
|
{:ok, louder} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -1, "md" => "FT4"}))
|
|
{:ok, quieter} = Pskr.parse_spot(Jason.encode!(%{@sample | "rp" => -18}))
|
|
|
|
acc =
|
|
nil
|
|
|> Pskr.merge_spot(first)
|
|
|> Pskr.merge_spot(louder)
|
|
|> Pskr.merge_spot(quieter)
|
|
|
|
assert acc.spot_count == 3
|
|
assert acc.max_snr_db == -1
|
|
assert acc.min_snr_db == -18
|
|
assert Enum.sort(acc.modes) == ["FT4", "FT8"]
|
|
assert DateTime.compare(acc.first_spot_at, acc.last_spot_at) in [:lt, :eq]
|
|
end
|
|
end
|
|
end
|