defmodule HexSolver.Constraints.Union do @moduledoc false use HexSolver.Constraints.Impl alias HexSolver.Constraint alias HexSolver.Constraints.{Empty, Range, Union, Util, Version} # We will always work under the following assumptions for unions: # * Minimum of two elements # * Only %Range{} and %Version{}, no %Empty{} # * No "any range" (%Range{min: nil, max: nil}) # * Elements sorted by their minimum version # * Ranges do not overlap defstruct ranges: [] def empty?(%Union{}), do: false def any?(%Union{}), do: false def allows?(%Union{ranges: ranges}, %Elixir.Version{} = version) do Enum.any?(ranges, &Constraint.allows?(&1, version)) end def allows_all?(%Union{ranges: left}, %Union{ranges: right}) do do_allows_all?(left, right) end def allows_all?(%Union{ranges: left}, right) do do_allows_all?(left, [right]) end # We can recurse left and right together since they are # sorted on minimum version defp do_allows_all?([left | lefts], [right | rights]) do if Constraint.allows_all?(left, right) do do_allows_all?([left | lefts], rights) else do_allows_all?(lefts, [right | rights]) end end defp do_allows_all?(_lefts, []), do: true defp do_allows_all?([], _rights), do: false def allows_any?(%Union{}, %Empty{}), do: true def allows_any?(%Union{} = left, right) do do_allows_any?(to_ranges(left), to_ranges(right)) end # We can recurse left and right together since they are # sorted on minimum version defp do_allows_any?([left | lefts], [right | rights]) do cond do Range.allows_any?(left, right) -> true # Move forward with the range with the lower max value Range.allows_higher?(right, left) -> do_allows_any?(lefts, [right | rights]) true -> do_allows_any?([left | lefts], rights) end end defp do_allows_any?(_lefts, _rights), do: false def difference(%Union{} = left, right) do do_difference(to_ranges(left), to_ranges(right), []) end defp do_difference(lefts, [], acc) do # If there are no more "right" ranges, none of the rest needs to # be subtracted and can be added as-is Util.from_list(Enum.reverse(acc) ++ lefts) end defp do_difference([], _rights, acc) do Util.from_list(Enum.reverse(acc)) end defp do_difference([left | lefts], [right | rights], acc) do cond do Range.strictly_lower?(right, left) -> do_difference([left | lefts], rights, acc) Range.strictly_higher?(right, left) -> do_difference(lefts, [right | rights], [left | acc]) true -> # Left and right overlaps case maybe_to_range(Range.difference(left, right)) do %Union{ranges: [first, last]} -> # If right splits left in half, we only need to check future ranges # against the latter half do_difference([last | lefts], rights, [first | acc]) %Range{} = range -> # Move the constraint with the lower max value forward. Ensures # we keep both lists in sync as much as possible and that large # ranges have a chance to subtract or be subtracted of multiple # small ranges if Range.allows_higher?(range, right) do do_difference([range | lefts], rights, acc) else do_difference(lefts, [right | rights], [range | acc]) end %Empty{} -> do_difference(lefts, [right | rights], acc) end end end def intersect(%Union{} = left, right) do do_intersect(to_ranges(left), to_ranges(right), []) end defp do_intersect([left | lefts], [right | rights], acc) do acc = case Constraint.intersect(left, right) do %Empty{} -> acc intersection -> [intersection | acc] end if Range.allows_higher?(right, left) do do_intersect(lefts, [right | rights], acc) else do_intersect([left | lefts], rights, acc) end end defp do_intersect(_lefts, [], acc), do: Util.from_list(Enum.reverse(acc)) defp do_intersect([], _rights, acc), do: Util.from_list(Enum.reverse(acc)) def union(%Union{} = left, right) do Util.union([left, right]) end def compare(%Union{ranges: [range | _]}, right) do Constraint.compare(range, right) end defp to_ranges(%Empty{}), do: [] defp to_ranges(%Elixir.Version{} = version), do: [Version.to_range(version)] defp to_ranges(%Range{} = range), do: [range] defp to_ranges(%Union{ranges: ranges}), do: Enum.map(ranges, &Version.to_range/1) defp maybe_to_range(%Elixir.Version{} = version), do: Version.to_range(version) defp maybe_to_range(other), do: other def to_string(%Union{ranges: ranges}) do Enum.map_join(ranges, " or ", &Kernel.to_string/1) end defimpl String.Chars do defdelegate to_string(union), to: HexSolver.Constraints.Union end defimpl Inspect do def inspect(union, _opts) do "#Union<#{union}>" end end end