defmodule HexSolver.Constraints.Range do @moduledoc false use HexSolver.Constraints.Impl alias HexSolver.Constraint alias HexSolver.Constraints.{Empty, Range, Union, Util, Version} # %Range{min: nil, max: nil} allows any version. # %Range{min: min, max: max, include_min: true, include_max: true} # when min == max is a single version. defstruct min: nil, max: nil, include_min: false, include_max: false def valid?(%Range{min: min, max: max, include_min: include_min, include_max: include_max}) do case version_compare(min, max) do :lt -> true :eq -> include_min and include_max :gt -> false end end def any?(%Range{min: nil, max: nil}), do: true def any?(%Range{}), do: false def empty?(%Range{}), do: false def allows?(%Range{} = range, %Elixir.Version{} = version) do compare_min = version_compare(range.min, version) if compare_min == :lt or (compare_min == :eq and range.include_min) do compare_max = version_compare(version, range.max) compare_max == :lt or (compare_max == :eq and range.include_max) else false end end def allows_all?(%Range{}, %Empty{}), do: true def allows_all?(%Range{} = range, %Elixir.Version{} = version), do: allows?(range, version) def allows_all?(%Range{} = left, %Range{} = right) do not allows_lower?(right, left) and not allows_higher?(right, left) end def allows_all?(%Range{} = range, %Union{ranges: ranges}) do Enum.all?(ranges, &allows_all?(range, &1)) end def allows_any?(%Range{}, %Empty{}), do: true def allows_any?(%Range{} = range, %Elixir.Version{} = version), do: allows?(range, version) def allows_any?(%Range{} = left, %Range{} = right) do not strictly_lower?(right, left) and not strictly_higher?(right, left) end def allows_any?(%Range{} = range, %Union{ranges: ranges}) do Enum.any?(ranges, &allows_any?(range, &1)) end def allows_lower?(%Range{} = left, %Range{} = right) do cond do left.min == nil -> right.min != nil right.min == nil -> false true -> case Version.compare(left.min, right.min) do :lt -> true :gt -> false :eq -> left.include_min and not right.include_min end end end def allows_higher?(%Range{} = left, %Range{} = right) do cond do left.max == nil -> right.max != nil right.max == nil -> false true -> case Version.compare(left.max, right.max) do :lt -> false :gt -> true :eq -> left.include_max and not right.include_max end end end def strictly_lower?(%Elixir.Version{} = left, %Elixir.Version{} = right) do Version.compare(left, right) == :lt end def strictly_lower?(%Range{} = range, %Elixir.Version{} = version) do range.max != nil and Version.compare(range.max, version) == :lt end def strictly_lower?(%Elixir.Version{} = version, %Range{} = range) do range.min != nil and Version.compare(version, range.min) == :lt end def strictly_lower?(%Range{} = left, %Range{} = right) do if left.max == nil or right.min == nil do false else case Version.compare(left.max, right.min) do :lt -> true :gt -> false :eq -> not left.include_max or not right.include_min end end end def strictly_higher?(left, right) do strictly_lower?(right, left) end def difference(%Range{} = range, %Empty{}) do range end def difference(%Range{} = range, %Elixir.Version{} = version) do cond do not allows?(range, version) -> range range.min == version -> %{range | include_min: false} range.max == version -> %{range | include_max: false} true -> %Union{ ranges: [ %{range | max: version, include_max: false}, %{range | min: version, include_min: false} ] } end end def difference(%Range{} = left, %Range{} = right) do if allows_any?(left, right) do before_range = cond do not allows_lower?(left, right) -> nil left.min == right.min -> true = left.include_min and not right.include_min true = left.min != nil left.min true -> %Range{ min: left.min, max: right.min, include_min: left.include_min, include_max: not right.include_min } end after_range = cond do not allows_higher?(left, right) -> nil left.max == right.max -> true = left.include_max and not right.include_max true = left.max != nil left.max true -> %Range{ min: right.max, max: left.max, include_min: not right.include_max, include_max: left.include_max } end cond do before_range == nil and after_range == nil -> %Empty{} before_range == nil -> after_range after_range == nil -> before_range true -> %Union{ranges: [before_range, after_range]} end else left end end def difference(%Range{} = range, %Union{} = union) do {range, ranges} = Enum.reduce_while(union.ranges, {range, []}, fn union_range, {range, acc} -> cond do strictly_lower?(union_range, range) -> {:cont, {range, acc}} strictly_higher?(union_range, range) -> {:halt, {range, acc}} true -> case Constraint.difference(range, union_range) do %Empty{} -> {:halt, {range, %Empty{}}} %Union{ranges: [first, last]} -> {:cont, {last, [first | acc]}} constraint -> {:cont, {constraint, acc}} end end end) case ranges do %Empty{} -> %Empty{} [] -> range _ -> %Union{ranges: Enum.reverse([range | ranges])} end end def intersect(%Range{}, %Empty{}) do %Empty{} end def intersect(%Range{} = range, %Elixir.Version{} = version) do if allows?(range, version) do version else %Empty{} end end def intersect(%Range{} = left, %Range{} = right) do if strictly_lower?(left, right) or strictly_lower?(right, left) do %Empty{} else {intersect_min, intersect_include_min} = if allows_lower?(left, right) do {right.min, right.include_min} else {left.min, left.include_min} end {intersect_max, intersect_include_max} = if allows_higher?(left, right) do {right.max, right.include_max} else {left.max, left.include_max} end cond do intersect_min == nil and intersect_max == nil -> # Open range %Range{} intersect_min == intersect_max -> # There must be overlap since we already checked none of # the ranges are strictly lower true = intersect_include_min and intersect_include_max intersect_min true -> %Range{ min: intersect_min, max: intersect_max, include_min: intersect_include_min, include_max: intersect_include_max } end end end def intersect(%Range{} = range, %Union{} = union) do Union.intersect(union, range) end def union(%Range{} = range, %Empty{}) do range end def union(%Range{} = range, %Elixir.Version{} = version) do if allows?(range, version) do range else case range do %Range{min: ^version} -> %{range | include_min: true} %Range{max: ^version} -> %{range | include_max: true} _ -> Util.union([range, version]) end end end def union(%Range{} = left, %Range{} = right) do if not edges_touch?(left, right) and not Range.allows_any?(left, right) do Util.union([left, right]) else min = if allows_lower?(left, right), do: left, else: right max = if allows_higher?(left, right), do: left, else: right %Range{ min: min.min, max: max.max, include_min: min.include_min, include_max: max.include_max } end end def union(%Range{} = range, %Union{} = union) do Util.union([range, union]) end defp edges_touch?(left, right) do (left.max != nil and left.max == right.min and (left.include_max or right.include_min)) or (left.min != nil and left.min == right.max and (left.include_min or right.include_max)) end def compare(%Range{min: min, include_min: include_min}, %Elixir.Version{} = version) do if min == nil do :lt else case Version.compare(min, version) do :eq when include_min -> :eq :eq -> :gt :lt -> :lt :gt -> :gt end end end def compare(%Range{} = left, %Range{} = right) do cond do left.min == nil and right.min == nil -> :eq left.min == nil -> :lt right.min == nil -> :gt true -> left_include_min = left.include_min right_include_min = right.include_min case Version.compare(left.min, right.min) do :eq when left_include_min == right_include_min -> :eq :eq when left_include_min -> :lt :eq when right_include_min -> :gt :lt -> :lt :gt -> :gt end end end def compare(%Range{} = left, %Union{ranges: [right | _]}) do compare(left, right) end def single_version?(%Range{min: min, max: max, include_min: true, include_max: true}) do min == max end def single_version?(%Range{}) do false end defp version_compare(nil, _right), do: :lt defp version_compare(_left, nil), do: :lt defp version_compare(left, right), do: Version.compare(left, right) def normalize(%Range{min: version, max: version, include_min: true, include_max: true}), do: version def normalize(%Range{} = range), do: range def normalize(%Elixir.Version{} = version), do: version def to_string(%Range{min: nil, max: nil}) do "any" end def to_string(%Range{min: version, max: version, include_min: true, include_max: true}) do Kernel.to_string(version) end def to_string(%Range{ min: %Elixir.Version{major: min_major, minor: min_minor, patch: 0, pre: pre}, max: %Elixir.Version{major: max_major, minor: 0, patch: 0, pre: [0]}, include_min: true, include_max: false }) when min_major + 1 == max_major do "~> #{min_major}.#{min_minor}#{pre_to_string(pre)}" end def to_string(%Range{ min: %Elixir.Version{major: min_major, minor: min_minor, patch: min_patch, pre: pre}, max: %Elixir.Version{major: max_major, minor: max_minor, patch: 0, pre: [0]}, include_min: true, include_max: false }) when min_major == max_major and min_minor + 1 == max_minor do "~> #{min_major}.#{min_minor}.#{min_patch}#{pre_to_string(pre)}" end def to_string(%Range{min: min, max: max, include_min: include_min, include_max: include_max}) do min_string = if min, do: ">#{include(include_min)} #{min}" max_string = if max, do: "<#{include(include_max)} #{max}" if min_string && max_string do "#{min_string} and #{max_string}" else min_string || max_string end end defp pre_to_string([]), do: "" defp pre_to_string(pre), do: "-" <> Enum.join(pre, ".") defp include(true), do: "=" defp include(false), do: "" defimpl String.Chars do defdelegate to_string(range), to: HexSolver.Constraints.Range end defimpl Inspect do def inspect(range, _opts) do "#Range<#{range}>" end end end