defmodule NPM.SemverUtil do @moduledoc """ Utility functions for working with npm semver ranges. Provides higher-level operations that build on `NPMSemver.matches?/2` for common npm workflows like finding the best matching version, filtering compatible versions, and determining update targets. """ @doc """ Find the highest version from a list that satisfies a range. Returns `{:ok, version}` or `:none`. """ @spec max_satisfying([String.t()], String.t()) :: {:ok, String.t()} | :none def max_satisfying(versions, range) do versions |> Enum.filter(&matches?(&1, range)) |> sort_desc() |> case do [best | _] -> {:ok, best} [] -> :none end end @doc """ Find the lowest version from a list that satisfies a range. """ @spec min_satisfying([String.t()], String.t()) :: {:ok, String.t()} | :none def min_satisfying(versions, range) do versions |> Enum.filter(&matches?(&1, range)) |> sort_asc() |> case do [lowest | _] -> {:ok, lowest} [] -> :none end end @doc """ Filter a list of versions to only those satisfying a range. """ @spec filter([String.t()], String.t()) :: [String.t()] def filter(versions, range) do Enum.filter(versions, &matches?(&1, range)) end @doc """ Check if any version in the list satisfies the range. """ @spec any_satisfying?([String.t()], String.t()) :: boolean() def any_satisfying?(versions, range) do Enum.any?(versions, &matches?(&1, range)) end @doc """ Determine the type of update between two versions. Returns `:major`, `:minor`, `:patch`, or `:prerelease`. """ @spec update_type(String.t(), String.t()) :: :major | :minor | :patch | :prerelease | :none def update_type(from, to) do with {:ok, {fmaj, fmin, fpatch}} <- parse(from), {:ok, {tmaj, tmin, tpatch}} <- parse(to) do cond do fmaj != tmaj -> :major fmin != tmin -> :minor fpatch != tpatch -> :patch true -> :none end else _ -> :none end end defp matches?(version, range) do NPMSemver.matches?(version, range) rescue ArgumentError -> false end defp sort_desc(versions) do Enum.sort(versions, fn a, b -> case {Version.parse(a), Version.parse(b)} do {{:ok, va}, {:ok, vb}} -> Version.compare(va, vb) == :gt _ -> a >= b end end) end defp sort_asc(versions) do Enum.sort(versions, fn a, b -> case {Version.parse(a), Version.parse(b)} do {{:ok, va}, {:ok, vb}} -> Version.compare(va, vb) == :lt _ -> a <= b end end) end defp parse(version) do case String.split(version, ".") do [maj, min, patch_str] -> patch = patch_str |> String.split("-") |> List.first() {:ok, {String.to_integer(maj), String.to_integer(min), String.to_integer(patch)}} _ -> :error end rescue _ -> :error end end