advent_of_code/elixir/lib/util/range.ex
2024-11-15 21:03:49 -05:00

141 lines
3.4 KiB
Elixir

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