defmodule HexSolver.Constraints.Util do
@moduledoc false
alias HexSolver.{Constraint, Util}
alias HexSolver.Constraints.{Empty, Range, Union, Version}
def any(), do: %Range{}
def from_list([]), do: %Empty{}
def from_list([single]), do: Range.normalize(single)
def from_list(acc), do: %Union{ranges: Enum.map(acc, &Range.normalize/1)}
def union(list) do
list = flatten(list)
cond do
list == [] ->
%Empty{}
Enum.any?(list, &Constraint.any?/1) ->
%Range{}
true ->
list
|> Enum.sort(Util.compare(Constraint))
|> Enum.reduce([], fn constraint, acc ->
previous = List.first(acc)
cond do
acc == [] ->
[constraint]
not Constraint.allows_any?(previous, constraint) and
not adjacent?(previous, constraint) ->
[constraint | acc]
true ->
# Merge this constraint with previous, but only if they touch
List.replace_at(acc, 0, Constraint.union(previous, constraint))
end
end)
|> Enum.reverse()
|> from_list()
end
end
defp flatten(list) do
Enum.flat_map(list, fn
%Union{ranges: ranges} -> ranges
%Empty{} -> []
other -> [other]
end)
end
@doc """
Returns `true` if `left` is immediately next to, but not overlapping, `right`.
Assumes `left` is lower than `right`.
"""
def adjacent?(left, right) do
left = Version.to_range(left)
right = Version.to_range(right)
left.max == right.min and
((left.include_max and not right.include_min) or
(not left.include_max and right.include_min))
end
def from_bounds(%Elixir.Version{} = lower, %Elixir.Version{} = upper) do
case Version.compare(lower, upper) do
:eq -> lower
:lt -> %Range{min: lower, max: upper, include_min: true, include_max: true}
end
end
def from_bounds(%Elixir.Version{} = lower, nil), do: %Range{min: lower, include_min: true}
def from_bounds(nil, %Elixir.Version{} = upper), do: %Range{max: upper, include_max: true}
def from_bounds(nil, nil), do: %Range{}
end