Current section

Files

Jump to
aja lib ex_range.ex
Raw

lib/ex_range.ex

defmodule A.ExRange do
@moduledoc ~S"""
Exclusive ranges are an exact copy of regular ranges (see `Range`),
except that they exclude the second parameter.
## Why/when would you need exclusive ranges?
The most typical use case would be when using `1..n` for loops based
on a parameter `n >= 0`:
iex> incorrect = fn n -> for i <- 1..n, do: "id_#{i}" end
iex> incorrect.(3) # works fine in general...
["id_1", "id_2", "id_3"]
iex> incorrect.(0) # weird bug at edge case!
["id_1", "id_0"]
To fix it, you would typically need to match the `n == 0` case and handle
it differently, which is adds noise.
With exclusive ranges like `0 ~> n`, you get to keep the compact and elegant
approach from above, while having a correct algorithm that covers the edge case:
iex> correct = fn n -> for i <- 1 ~> n + 1, do: "id_#{i}" end
iex> correct.(3) # works fine
["id_1", "id_2", "id_3"]
iex> correct.(0) # edge case works fine too
[]
Exclusive ranges can be either increasing (`start <= stop`) or
decreasing (`start > stop`). The `start` parameter is included
(except if `start == stop`), the `stop` parameter is *always* excluded.
An exclusive range is represented internally as a struct
`A.ExRange{start: start, stop: stop}` and can be used as is.
The `A.~>/2` convenience macro makes it possible to have a more compact
syntax, similar to `../2`.
It is totally optional and needs to be imported:
iex> import A
iex> import A, only: [{:~>, 2}] # more selective
## Examples:
iex> A.ExRange.new(5)
#A<0 ~> 5>
iex> range = 0 ~> 5
#A<0 ~> 5>
iex> start ~> stop = range
iex> {start, stop}
iex> {0, 4}
iex> Enum.to_list(range)
[0, 1, 2, 3, 4]
iex> Enum.count(range)
5
iex> Enum.member?(range, 5)
false
iex> Enum.member?(range, 4)
true
iex> Enum.to_list(3 ~> 0)
[3, 2, 1]
Just like `Range`s, such function calls are efficient memory-wise
no matter the size of the range. The implementation of the `Enumerable`
protocol uses logic based solely on the endpoints and does
not materialize the whole list of integers.
"""
import A, only: [{:~>, 2}]
@type t :: %__MODULE__{start: integer, stop: integer}
@enforce_keys [:start, :stop]
defstruct [:start, :stop]
@doc """
Creates a new exclusive range.
`start` defaults to 0.
## Examples
iex> A.ExRange.new(0, 100)
#A<0 ~> 100>
iex> A.ExRange.new(10)
#A<0 ~> 10>
"""
@spec new(integer, integer) :: t
def new(start \\ 0, stop)
def new(start, stop) when is_integer(start) and is_integer(stop) do
%A.ExRange{start: start, stop: stop}
end
def new(start, stop) do
raise ArgumentError,
"A.ExRange (start ~> stop) expect both sides to be integers, " <>
"got: #{inspect(start)} ~> #{inspect(stop)}"
end
@doc """
Checks if two ranges are disjoint.
## Examples
iex> A.ExRange.disjoint?(1 ~> 6, 6 ~> 9)
true
iex> A.ExRange.disjoint?(6 ~> 1, 6 ~> 9)
true
iex> A.ExRange.disjoint?(1 ~> 6, 5 ~> 9)
false
iex> A.ExRange.disjoint?(1 ~> 6, 2 ~> 7)
false
"""
@spec disjoint?(t, t) :: boolean
def disjoint?(start1 ~> stop1 = _range1, start2 ~> stop2 = _range2) do
{start1, stop1} = normalize(start1, stop1)
{start2, stop2} = normalize(start2, stop2)
stop2 < start1 + 1 or stop1 < start2 + 1
end
@compile inline: [normalize: 2]
defp normalize(start, stop) when start > stop, do: {stop, start}
defp normalize(start, stop), do: {start, stop}
defimpl Enumerable do
def reduce(start ~> stop, acc, fun) do
reduce(start, stop, acc, fun, _up? = stop >= start)
end
defp reduce(_start, _stop, {:halt, acc}, _fun, _up?) do
{:halted, acc}
end
defp reduce(start, stop, {:suspend, acc}, fun, up?) do
{:suspended, acc, &reduce(start, stop, &1, fun, up?)}
end
defp reduce(start, stop, {:cont, acc}, fun, _up? = true) when start < stop do
reduce(start + 1, stop, fun.(start, acc), fun, _up? = true)
end
defp reduce(start, stop, {:cont, acc}, fun, _up? = false) when start > stop do
reduce(start - 1, stop, fun.(start, acc), fun, _up? = false)
end
defp reduce(_, _, {:cont, acc}, _fun, _up) do
{:done, acc}
end
def member?(start ~> stop, value) when is_integer(value) do
if start <= stop do
{:ok, start <= value and value < stop}
else
{:ok, stop < value and value <= start}
end
end
def member?(_ ~> _, _value) do
{:ok, false}
end
def count(start ~> stop) do
if start <= stop do
{:ok, stop - start}
else
{:ok, start - stop}
end
end
def slice(start ~> stop) do
if start <= stop do
{:ok, stop - start, &slice_asc(start + &1, &2)}
else
{:ok, start - stop, &slice_desc(start - &1, &2)}
end
end
defp slice_asc(current, 1), do: [current]
defp slice_asc(current, remaining), do: [current | slice_asc(current + 1, remaining - 1)]
defp slice_desc(current, 1), do: [current]
defp slice_desc(current, remaining), do: [current | slice_desc(current - 1, remaining - 1)]
end
defimpl Inspect do
import Inspect.Algebra
def inspect(start ~> stop, opts) do
concat([
"#A<",
to_doc(start, opts),
" ~> ",
to_doc(stop, opts),
">"
])
end
end
end