169 lines
4.3 KiB
Elixir
169 lines
4.3 KiB
Elixir
defmodule Range do
|
|
@moduledoc """
|
|
Ranges represent a sequence of one or many, ascending
|
|
or descending, consecutive integers.
|
|
|
|
Ranges can be either increasing (`first <= last`) or
|
|
decreasing (`first > last`). Ranges are also always
|
|
inclusive.
|
|
|
|
A range is represented internally as a struct. However,
|
|
the most common form of creating and matching on ranges
|
|
is via the `../2` macro, auto-imported from `Kernel`:
|
|
|
|
iex> range = 1..3
|
|
1..3
|
|
iex> first..last = range
|
|
iex> first
|
|
1
|
|
iex> last
|
|
3
|
|
|
|
A range implements the `Enumerable` protocol, which means
|
|
functions in the `Enum` module can be used to work with
|
|
ranges:
|
|
|
|
iex> range = 1..10
|
|
1..10
|
|
iex> Enum.reduce(range, 0, fn i, acc -> i * i + acc end)
|
|
385
|
|
iex> Enum.count(range)
|
|
10
|
|
iex> Enum.member?(range, 11)
|
|
false
|
|
iex> Enum.member?(range, 8)
|
|
true
|
|
|
|
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.
|
|
"""
|
|
|
|
defstruct first: nil, last: nil
|
|
|
|
@type t :: %__MODULE__{first: integer, last: integer}
|
|
@type t(first, last) :: %__MODULE__{first: first, last: last}
|
|
|
|
@doc """
|
|
Creates a new range.
|
|
|
|
## Examples
|
|
|
|
iex> Range.new(-100, 100)
|
|
-100..100
|
|
|
|
"""
|
|
@spec new(integer, integer) :: t
|
|
def new(first, last) when is_integer(first) and is_integer(last) do
|
|
%Range{first: first, last: last}
|
|
end
|
|
|
|
def new(first, last) do
|
|
raise ArgumentError,
|
|
"ranges (first..last) expect both sides to be integers, " <>
|
|
"got: #{inspect(first)}..#{inspect(last)}"
|
|
end
|
|
|
|
@doc """
|
|
Checks if two ranges are disjoint.
|
|
|
|
## Examples
|
|
|
|
iex> Range.disjoint?(1..5, 6..9)
|
|
true
|
|
iex> Range.disjoint?(5..1, 6..9)
|
|
true
|
|
iex> Range.disjoint?(1..5, 5..9)
|
|
false
|
|
iex> Range.disjoint?(1..5, 2..7)
|
|
false
|
|
|
|
"""
|
|
@doc since: "1.8.0"
|
|
@spec disjoint?(t, t) :: boolean
|
|
def disjoint?(first1..last1 = _range1, first2..last2 = _range2) do
|
|
{first1, last1} = normalize(first1, last1)
|
|
{first2, last2} = normalize(first2, last2)
|
|
last2 < first1 or last1 < first2
|
|
end
|
|
|
|
@compile inline: [normalize: 2]
|
|
defp normalize(first, last) when first > last, do: {last, first}
|
|
defp normalize(first, last), do: {first, last}
|
|
|
|
@doc false
|
|
@deprecated "Pattern match on first..last instead"
|
|
def range?(term)
|
|
def range?(first..last) when is_integer(first) and is_integer(last), do: true
|
|
def range?(_), do: false
|
|
end
|
|
|
|
defimpl Enumerable, for: Range do
|
|
def reduce(first..last, acc, fun) do
|
|
reduce(first, last, acc, fun, _up? = last >= first)
|
|
end
|
|
|
|
defp reduce(_first, _last, {:halt, acc}, _fun, _up?) do
|
|
{:halted, acc}
|
|
end
|
|
|
|
defp reduce(first, last, {:suspend, acc}, fun, up?) do
|
|
{:suspended, acc, &reduce(first, last, &1, fun, up?)}
|
|
end
|
|
|
|
defp reduce(first, last, {:cont, acc}, fun, _up? = true) when first <= last do
|
|
reduce(first + 1, last, fun.(first, acc), fun, _up? = true)
|
|
end
|
|
|
|
defp reduce(first, last, {:cont, acc}, fun, _up? = false) when first >= last do
|
|
reduce(first - 1, last, fun.(first, acc), fun, _up? = false)
|
|
end
|
|
|
|
defp reduce(_, _, {:cont, acc}, _fun, _up) do
|
|
{:done, acc}
|
|
end
|
|
|
|
def member?(first..last, value) when is_integer(value) do
|
|
if first <= last do
|
|
{:ok, first <= value and value <= last}
|
|
else
|
|
{:ok, last <= value and value <= first}
|
|
end
|
|
end
|
|
|
|
def member?(_.._, _value) do
|
|
{:ok, false}
|
|
end
|
|
|
|
def count(first..last) do
|
|
if first <= last do
|
|
{:ok, last - first + 1}
|
|
else
|
|
{:ok, first - last + 1}
|
|
end
|
|
end
|
|
|
|
def slice(first..last) do
|
|
if first <= last do
|
|
{:ok, last - first + 1, &slice_asc(first + &1, &2)}
|
|
else
|
|
{:ok, first - last + 1, &slice_desc(first - &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, for: Range do
|
|
import Inspect.Algebra
|
|
|
|
def inspect(first..last, opts) do
|
|
concat([to_doc(first, opts), "..", to_doc(last, opts)])
|
|
end
|
|
end
|