defmodule AoC.Util.Range do @doc """ Given a `start` and a `length`, returns a Range of start..(start+len-1). Returns the empty range if `len` is 0. ## Examples iex> AoC.Util.Range.from_start(10, 2) 10..11 iex> AoC.Util.Range.from_start(10, 0) .. iex> AoC.Util.Range.from_start(10, 10) 10..19 """ @spec from_start(integer(), integer()) :: Range.t() def from_start(start, len) do if len == 0 do .. else start..(start + len - 1) end end @doc """ Given a `value`, z, and a range (x,y) containing that value. Return the `value`'s "place" in the `dest` range (s,t), given by s + (z - x). Note that this does not check that `source` and `dest` are the same size so it is possible the returned value is not contained within `dest`. iex> AoC.Util.Range.transpose(5, 1..10, 30..50) 34 """ def transpose(value, source, dest) do offset = value - (source |> Enum.at(0)) (dest |> Enum.at(0)) + offset end @doc """ Returns true iff r2 is completely contained within r1. ## Examples iex> AoC.Util.Range.contains?(1..10, 2..8) true """ @spec contains?(Range.t(), Range.t()) :: boolean def contains?(r1, r2) do r2.first >= r1.first and r2.last <= r1.last end @doc """ Returns the set difference r1 \\ r2. ## Examples iex> AoC.Util.Range.difference(1..10, 11..20) [1..10] iex> AoC.Util.Range.difference(1..10, 2..5) [1..1, 6..10] iex> AoC.Util.Range.difference(1..10, -1..3) [4..10] """ @spec difference(Range.t(), Range.t()) :: [Range.t()] def difference(r1, r2) do cond do Range.disjoint?(r1, r2) -> [r1] AoC.Util.Range.contains?(r2, r1) -> [] AoC.Util.Range.contains?(r1, r2) -> s1_len = r2.first - r1.first s2_len = r1.last - r2.last [ AoC.Util.Range.from_start(r1.first, s1_len), AoC.Util.Range.from_start(r2.last + 1, s2_len) ] |> Enum.reject(fn r -> r == .. end) r2.first <= r1.first and r2.last <= r1.last -> s1_len = r1.last - r2.last [AoC.Util.Range.from_start(r2.last + 1, s1_len)] r2.first >= r1.first and r2.last >= r1.last -> s1_len = r2.first - r1.first [AoC.Util.Range.from_start(r1.first, s1_len)] end end @doc """ Returns the set intersection of r1 and r2. ## Examples iex> AoC.Util.Range.intersection(1..10, 1..3) 1..3 iex> AoC.Util.Range.intersection(1..10, -1..3) 1..3 iex> AoC.Util.Range.intersection(-1..10, 1..3) 1..3 """ @spec intersection(Range.t(), Range.t()) :: Range.t() def intersection(r1, r2) do cond do Range.disjoint?(r1, r2) -> .. AoC.Util.Range.contains?(r2, r1) -> r1 AoC.Util.Range.contains?(r1, r2) -> r2 r2.first <= r1.first and r2.last <= r1.last -> Range.new(r1.first, r2.last) r2.first >= r1.first and r2.last >= r1.last -> Range.new(r2.first, r1.last) end end @spec split_at(range :: Range.t(), value :: integer(), bound :: :lb | :ub) :: [Range.t()] def split_at(range, value, bound) do if value <= range.first or value >= range.last do [range] else ls = min(range.first, value - 1) le = min(range.last, value - 1) rs = max(range.first, value + 1) re = max(range.last, value + 1) if bound == :lb do [ls..value, rs..re] else [ls..le, value..re] end end end end