defmodule NPM.VersionUtil do
@moduledoc """
Utilities for npm version string manipulation.
Helpers for parsing, comparing, and formatting npm version strings
that go beyond what Elixir's `Version` module handles.
"""
@doc """
Parse a version string into `{major, minor, patch}` tuple.
Returns `:error` for invalid versions.
"""
@spec parse_triple(String.t()) ::
{:ok, {non_neg_integer(), non_neg_integer(), non_neg_integer()}} | :error
def parse_triple(version) do
case Version.parse(version) do
{:ok, %{major: maj, minor: min, patch: patch}} -> {:ok, {maj, min, patch}}
:error -> :error
end
end
@doc """
Compare two version strings.
Returns `:gt`, `:eq`, or `:lt`.
"""
@spec compare(String.t(), String.t()) :: :gt | :eq | :lt
def compare(a, b) do
Version.compare(a, b)
end
@doc """
Check if version `a` is greater than version `b`.
"""
@spec gt?(String.t(), String.t()) :: boolean()
def gt?(a, b), do: compare(a, b) == :gt
@doc """
Check if version `a` is less than version `b`.
"""
@spec lt?(String.t(), String.t()) :: boolean()
def lt?(a, b), do: compare(a, b) == :lt
@doc """
Get the major version number from a version string.
"""
@spec major(String.t()) :: non_neg_integer() | :error
def major(version) do
case parse_triple(version) do
{:ok, {maj, _, _}} -> maj
:error -> :error
end
end
@doc """
Get the minor version number from a version string.
"""
@spec minor(String.t()) :: non_neg_integer() | :error
def minor(version) do
case parse_triple(version) do
{:ok, {_, min, _}} -> min
:error -> :error
end
end
@doc """
Check if a version is a prerelease (has a pre tag).
"""
@spec prerelease?(String.t()) :: boolean()
def prerelease?(version) do
case Version.parse(version) do
{:ok, %{pre: pre}} -> pre != []
:error -> false
end
end
@doc """
Increment the patch version.
"""
@spec bump_patch(String.t()) :: String.t() | :error
def bump_patch(version) do
case parse_triple(version) do
{:ok, {maj, min, patch}} -> "#{maj}.#{min}.#{patch + 1}"
:error -> :error
end
end
@doc """
Increment the minor version (resets patch to 0).
"""
@spec bump_minor(String.t()) :: String.t() | :error
def bump_minor(version) do
case parse_triple(version) do
{:ok, {maj, min, _}} -> "#{maj}.#{min + 1}.0"
:error -> :error
end
end
@doc """
Increment the major version (resets minor and patch to 0).
"""
@spec bump_major(String.t()) :: String.t() | :error
def bump_major(version) do
case parse_triple(version) do
{:ok, {maj, _, _}} -> "#{maj + 1}.0.0"
:error -> :error
end
end
@doc """
Sort a list of version strings.
"""
@spec sort([String.t()]) :: [String.t()]
def sort(versions) do
versions
|> Enum.flat_map(fn v ->
case Version.parse(v) do
{:ok, ver} -> [{v, ver}]
:error -> []
end
end)
|> Enum.sort_by(&elem(&1, 1), Version)
|> Enum.map(&elem(&1, 0))
end
@doc """
Get the latest (highest) version from a list.
"""
@spec latest([String.t()]) :: String.t() | nil
def latest(versions) do
sort(versions) |> List.last()
end
end