defmodule Microwaveprop.FormatPropertyTest do use ExUnit.Case, async: true use ExUnitProperties alias Microwaveprop.Format # Any finite non-negative number is legitimate input for distance_km/1. # StreamData floats default to the full representable range — we keep # that but reject negatives since negative distances are meaningless # (the algorithm callers always pass an already-absolute km value). defp non_negative_number do one_of([ integer(0..1_000_000_000), map(float(min: 0.0), fn f -> f end), map(integer(0..1_000_000), &(&1 * 1.0)) ]) end describe "distance_km/1" do property "returns a binary for nil" do # Not really a property, just pinned for discoverability alongside # the numeric properties. assert Format.distance_km(nil) == "—" end property "always returns a non-empty binary for any non-negative number" do check all(n <- non_negative_number()) do result = Format.distance_km(n) assert is_binary(result) assert result != "" end end property "output contains both `mi` and `km` units" do check all(n <- non_negative_number()) do result = Format.distance_km(n) assert result =~ "mi" assert result =~ "km" end end property "accepts Decimal input without crashing" do check all(n <- integer(0..10_000)) do decimal = Decimal.new(n) assert is_binary(Format.distance_km(decimal)) end end property "accepts negative distances without crashing" do # Not a valid input at the call site, but we don't want arbitrary # numeric data (e.g. a sign slip in a caller) to crash the view. check all(n <- float(min: -1.0e6, max: -0.01)) do assert is_binary(Format.distance_km(n)) end end end describe "number/1" do test "inserts commas every three digits from the right" do assert Format.number(0) == "0" assert Format.number(999) == "999" assert Format.number(1_000) == "1,000" assert Format.number(1_234_567) == "1,234,567" assert Format.number(-1_234_567) == "-1,234,567" end test "rounds floats to an integer before formatting" do assert Format.number(1000.4) == "1,000" assert Format.number(1000.6) == "1,001" end test "falls through to string conversion for non-numeric input" do # The scorecard table occasionally hits nil / atom cells; we'd # rather stringify than crash the LiveView. assert Format.number(nil) == "" assert Format.number(:missing) == "missing" end property "separator insertion is lossless: stripping commas round-trips to the input" do check all(n <- integer(-1_000_000_000..1_000_000_000)) do formatted = Format.number(n) assert formatted |> String.replace(",", "") |> String.to_integer() == n end end property "output has a comma every 4th position walking backward from the end (for |n| ≥ 1000)" do check all(n <- integer(1000..999_999_999)) do formatted = Format.number(n) # Walk from the end in 4-char windows (digit, digit, digit, comma). digits_only = String.replace(formatted, ",", "") expected_commas = div(String.length(digits_only) - 1, 3) assert formatted |> String.graphemes() |> Enum.filter(&(&1 == ",")) |> length() == expected_commas end end end describe "bytes/1" do test "picks the right unit at each boundary" do assert Format.bytes(0) == "0 B" assert Format.bytes(1023) == "1023 B" assert Format.bytes(1024) == "1.0 KB" assert Format.bytes(1_500_000) == "1.4 MB" assert Format.bytes(2_147_483_648) == "2.0 GB" end property "output always ends with a unit suffix (B, KB, MB, or GB)" do check all(b <- integer(0..10_000_000_000)) do s = Format.bytes(b) assert s =~ ~r/\s(B|KB|MB|GB)\z/ end end property "output is monotonic non-decreasing in the input when parsed back" do # Specifically: converting the formatted string back to bytes # preserves ordering. Edge cases around unit boundaries matter: # 1023 B < 1024 B (= 1.0 KB) < 2048 B (= 2.0 KB). check all( a <- integer(0..10_000_000_000), delta <- integer(0..10_000_000_000) ) do b = a + delta assert parse_bytes(Format.bytes(a)) <= parse_bytes(Format.bytes(b)) + 1 end end end defp parse_bytes(s) do # Reverse of Format.bytes/1 for monotonicity checking. {n, rest} = Float.parse(s) unit = rest |> String.trim() |> String.upcase() case unit do "B" -> round(n) "KB" -> round(n * 1024) "MB" -> round(n * 1_048_576) "GB" -> round(n * 1_073_741_824) end end end