614 lines
16 KiB
Elixir
614 lines
16 KiB
Elixir
defmodule Range do
|
|
@moduledoc """
|
|
Ranges represent a sequence of zero, one or many, ascending
|
|
or descending integers with a common difference called step.
|
|
|
|
The most common form of creating and matching on ranges is
|
|
via the [`first..last`](`../2`) and [`first..last//step`](`..///3`)
|
|
notations, auto-imported from `Kernel`:
|
|
|
|
iex> 1 in 1..10
|
|
true
|
|
iex> 5 in 1..10
|
|
true
|
|
iex> 10 in 1..10
|
|
true
|
|
|
|
Ranges are always inclusive in Elixir. When a step is defined,
|
|
integers will only belong to the range if they match the step:
|
|
|
|
iex> 5 in 1..10//2
|
|
true
|
|
iex> 4 in 1..10//2
|
|
false
|
|
|
|
When defining a range without a step, the step will be
|
|
defined based on the first and last position of the
|
|
range, If `last >= first`, it will be an increasing range
|
|
with a step of 1. Otherwise, it is a decreasing range.
|
|
Note, however, implicit decreasing ranges are deprecated.
|
|
Therefore, if you need a decreasing range from `3` to `1`,
|
|
prefer to write `3..1//-1` instead.
|
|
|
|
`../0` can also be used as a shortcut to create the range `0..-1//1`,
|
|
also known as the full-slice range:
|
|
|
|
iex> ..
|
|
0..-1//1
|
|
|
|
## Use cases
|
|
|
|
Ranges typically have two uses in Elixir: as a collection or
|
|
to represent a slice of another data structure.
|
|
|
|
### Ranges as collections
|
|
|
|
Ranges in Elixir are enumerables and therefore can be used
|
|
with the `Enum` module:
|
|
|
|
iex> Enum.to_list(1..3)
|
|
[1, 2, 3]
|
|
iex> Enum.to_list(3..1//-1)
|
|
[3, 2, 1]
|
|
iex> Enum.to_list(1..5//2)
|
|
[1, 3, 5]
|
|
|
|
Ranges may also have a single element:
|
|
|
|
iex> Enum.to_list(1..1)
|
|
[1]
|
|
iex> Enum.to_list(1..1//2)
|
|
[1]
|
|
|
|
Or even no elements at all:
|
|
|
|
iex> Enum.to_list(10..0//1)
|
|
[]
|
|
iex> Enum.to_list(0..10//-1)
|
|
[]
|
|
|
|
The full-slice range, returned by `../0`, is an empty collection:
|
|
|
|
iex> Enum.to_list(..)
|
|
[]
|
|
|
|
### Ranges as slices
|
|
|
|
Ranges are also frequently used to slice collections.
|
|
You can slice strings or any enumerable:
|
|
|
|
iex> String.slice("elixir", 1..4)
|
|
"lixi"
|
|
iex> Enum.slice([0, 1, 2, 3, 4, 5], 1..4)
|
|
[1, 2, 3, 4]
|
|
|
|
In those cases, the first and last values of the range
|
|
are mapped to positions in the collections.
|
|
|
|
If a negative number is given, it maps to a position
|
|
from the back:
|
|
|
|
iex> String.slice("elixir", 1..-2//1)
|
|
"lixi"
|
|
iex> Enum.slice([0, 1, 2, 3, 4, 5], 1..-2//1)
|
|
[1, 2, 3, 4]
|
|
|
|
The range `0..-1//1`, returned by `../0`, returns the
|
|
collection as is, which is why it is called the full-slice
|
|
range:
|
|
|
|
iex> String.slice("elixir", ..)
|
|
"elixir"
|
|
iex> Enum.slice([0, 1, 2, 3, 4, 5], ..)
|
|
[0, 1, 2, 3, 4, 5]
|
|
|
|
## Definition
|
|
|
|
An increasing range `first..last//step` is a range from `first`
|
|
to `last` increasing by `step` where `step` must be a positive
|
|
integer and all values `v` must be `first <= v and v <= last`.
|
|
Therefore, a range `10..0//1` is an empty range because there
|
|
is no value `v` that is `10 <= v and v <= 0`.
|
|
|
|
Similarly, a decreasing range `first..last//step` is a range
|
|
from `first` to `last` decreasing by `step` where `step` must
|
|
be a negative integer and values `v` must be `first >= v and v >= last`.
|
|
Therefore, a range `0..10//-1` is an empty range because there
|
|
is no value `v` that is `0 >= v and v >= 10`.
|
|
|
|
## Representation
|
|
|
|
Internally, ranges are represented as structs:
|
|
|
|
iex> range = 1..9//2
|
|
1..9//2
|
|
iex> first..last//step = range
|
|
iex> first
|
|
1
|
|
iex> last
|
|
9
|
|
iex> step
|
|
2
|
|
iex> range.step
|
|
2
|
|
|
|
You can access the range fields (`first`, `last`, and `step`)
|
|
directly but you should not modify nor create ranges by hand.
|
|
Instead use the proper operators or `new/2` and `new/3`.
|
|
|
|
Ranges implement the `Enumerable` protocol with memory
|
|
efficient versions of all `Enumerable` callbacks:
|
|
|
|
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.
|
|
"""
|
|
|
|
@enforce_keys [:first, :last, :step]
|
|
defstruct first: nil, last: nil, step: nil
|
|
|
|
@type limit :: integer
|
|
@type step :: pos_integer | neg_integer
|
|
@type t :: %__MODULE__{first: limit, last: limit, step: step}
|
|
@type t(first, last) :: %__MODULE__{first: first, last: last, step: step}
|
|
|
|
@doc """
|
|
Creates a new range.
|
|
|
|
If `first` is less than `last`, the range will be increasing from
|
|
`first` to `last`. If `first` is equal to `last`, the range will contain
|
|
one element, which is the number itself.
|
|
|
|
If `first` is greater than `last`, the range will be decreasing from `first`
|
|
to `last`, albeit this behavior is deprecated. Therefore, it is advised to
|
|
explicitly list the step with `new/3`.
|
|
|
|
## Examples
|
|
|
|
iex> Range.new(-100, 100)
|
|
-100..100
|
|
|
|
"""
|
|
|
|
@spec new(limit, limit) :: t
|
|
def new(first, last) when is_integer(first) and is_integer(last) do
|
|
step =
|
|
if first <= last do
|
|
1
|
|
else
|
|
# TODO: Remove me on v2.0
|
|
IO.warn_once(
|
|
{__MODULE__, :new},
|
|
fn ->
|
|
"Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention"
|
|
end,
|
|
3
|
|
)
|
|
|
|
-1
|
|
end
|
|
|
|
%Range{first: first, last: last, step: step}
|
|
end
|
|
|
|
def new(first, last) do
|
|
raise ArgumentError,
|
|
"ranges (first..last) expect both sides to be integers, " <>
|
|
"got: #{inspect(first)}..#{inspect(last)}"
|
|
end
|
|
|
|
@doc """
|
|
Creates a new range with `step`.
|
|
|
|
## Examples
|
|
|
|
iex> Range.new(-100, 100, 2)
|
|
-100..100//2
|
|
|
|
"""
|
|
@doc since: "1.12.0"
|
|
@spec new(limit, limit, step) :: t
|
|
def new(first, last, step)
|
|
when is_integer(first) and is_integer(last) and is_integer(step) and step != 0 do
|
|
%Range{first: first, last: last, step: step}
|
|
end
|
|
|
|
def new(first, last, step) do
|
|
raise ArgumentError,
|
|
"ranges (first..last//step) expect both sides to be integers and the step to be a " <>
|
|
"non-zero integer, got: #{inspect(first)}..#{inspect(last)}//#{inspect(step)}"
|
|
end
|
|
|
|
@doc """
|
|
Returns the size of `range`.
|
|
|
|
## Examples
|
|
|
|
iex> Range.size(1..10)
|
|
10
|
|
iex> Range.size(1..10//2)
|
|
5
|
|
iex> Range.size(1..10//3)
|
|
4
|
|
iex> Range.size(1..10//-1)
|
|
0
|
|
|
|
iex> Range.size(10..1//-1)
|
|
10
|
|
iex> Range.size(10..1//-2)
|
|
5
|
|
iex> Range.size(10..1//-3)
|
|
4
|
|
iex> Range.size(10..1//1)
|
|
0
|
|
|
|
"""
|
|
@doc since: "1.12.0"
|
|
@spec size(t) :: non_neg_integer
|
|
def size(range)
|
|
def size(first..last//step) when step > 0 and first > last, do: 0
|
|
def size(first..last//step) when step < 0 and first < last, do: 0
|
|
def size(first..last//step), do: abs(div(last - first, step)) + 1
|
|
|
|
# TODO: Remove me on v2.0
|
|
def size(%{__struct__: Range, first: first, last: last} = range) do
|
|
step = if first <= last, do: 1, else: -1
|
|
size(Map.put(range, :step, step))
|
|
end
|
|
|
|
@doc """
|
|
Shifts a range by the given number of steps.
|
|
|
|
## Examples
|
|
|
|
iex> Range.shift(0..10, 1)
|
|
1..11
|
|
iex> Range.shift(0..10, 2)
|
|
2..12
|
|
|
|
iex> Range.shift(0..10//2, 2)
|
|
4..14//2
|
|
iex> Range.shift(10..0//-2, 2)
|
|
6..-4//-2
|
|
|
|
"""
|
|
@doc since: "1.14.0"
|
|
@spec shift(t, integer) :: t
|
|
def shift(first..last//step, steps_to_shift)
|
|
when is_integer(steps_to_shift) do
|
|
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
|
|
end
|
|
|
|
@doc """
|
|
Splits a range in two.
|
|
|
|
It returns a tuple of two elements.
|
|
|
|
If `split` is less than the number of elements in the range, the first
|
|
element in the range will have `split` entries and the second will have
|
|
all remaining entries.
|
|
|
|
If `split` is more than the number of elements in the range, the second
|
|
range in the tuple will emit zero elements.
|
|
|
|
## Examples
|
|
|
|
Increasing ranges:
|
|
|
|
iex> Range.split(1..5, 2)
|
|
{1..2, 3..5}
|
|
|
|
iex> Range.split(1..5//2, 2)
|
|
{1..3//2, 5..5//2}
|
|
|
|
iex> Range.split(1..5//2, 0)
|
|
{1..-1//2, 1..5//2}
|
|
|
|
iex> Range.split(1..5//2, 10)
|
|
{1..5//2, 7..5//2}
|
|
|
|
Decreasing ranges can also be split:
|
|
|
|
iex> Range.split(5..1//-1, 2)
|
|
{5..4//-1, 3..1//-1}
|
|
|
|
iex> Range.split(5..1//-2, 2)
|
|
{5..3//-2, 1..1//-2}
|
|
|
|
iex> Range.split(5..1//-2, 0)
|
|
{5..7//-2, 5..1//-2}
|
|
|
|
iex> Range.split(5..1//-2, 10)
|
|
{5..1//-2, -1..1//-2}
|
|
|
|
Empty ranges preserve their property but still return empty ranges:
|
|
|
|
iex> Range.split(2..5//-1, 2)
|
|
{2..3//-1, 4..5//-1}
|
|
|
|
iex> Range.split(2..5//-1, 10)
|
|
{2..3//-1, 4..5//-1}
|
|
|
|
iex> Range.split(5..2//1, 2)
|
|
{5..4//1, 3..2//1}
|
|
|
|
iex> Range.split(5..2//1, 10)
|
|
{5..4//1, 3..2//1}
|
|
|
|
If the number to split is negative, it splits from the back:
|
|
|
|
iex> Range.split(1..5, -2)
|
|
{1..3, 4..5}
|
|
|
|
iex> Range.split(5..1//-1, -2)
|
|
{5..3//-1, 2..1//-1}
|
|
|
|
If it is negative and greater than the elements in the range,
|
|
the first element of the tuple will be an empty range:
|
|
|
|
iex> Range.split(1..5, -10)
|
|
{1..0//1, 1..5}
|
|
|
|
iex> Range.split(5..1//-1, -10)
|
|
{5..6//-1, 5..1//-1}
|
|
|
|
## Properties
|
|
|
|
When a range is split, the following properties are observed.
|
|
Given `split(input)` returns `{left, right}`, we have:
|
|
|
|
assert input.first == left.first
|
|
assert input.last == right.last
|
|
assert input.step == left.step
|
|
assert input.step == right.step
|
|
assert Range.size(input) == Range.size(left) + Range.size(right)
|
|
|
|
"""
|
|
@doc since: "1.15.0"
|
|
@spec split(t, integer) :: {t, t}
|
|
def split(first..last//step = range, split) when is_integer(split) do
|
|
if split >= 0 do
|
|
split(first, last, step, split)
|
|
else
|
|
split(first, last, step, size(range) + split)
|
|
end
|
|
end
|
|
|
|
defp split(first, last, step, split) when first < last or (first == last and step > 0) do
|
|
if step > 0 do
|
|
mid = max(min(first + step * (split - 1), last), first - step)
|
|
{first..mid//step, (mid + step)..last//step}
|
|
else
|
|
{first..(first - step)//step, (last + step)..last//step}
|
|
end
|
|
end
|
|
|
|
defp split(last, first, step, split) do
|
|
if step < 0 do
|
|
mid = min(max(last + step * (split - 1), first), last - step)
|
|
{last..mid//step, (mid + step)..first//step}
|
|
else
|
|
{last..(last - step)//step, (first + step)..first//step}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Converts a range to a list.
|
|
|
|
## Examples
|
|
|
|
iex> Range.to_list(0..5)
|
|
[0, 1, 2, 3, 4, 5]
|
|
iex> Range.to_list(-3..0)
|
|
[-3, -2, -1, 0]
|
|
|
|
"""
|
|
@doc since: "1.15.0"
|
|
@spec to_list(t) :: list(integer)
|
|
def to_list(first..last//step)
|
|
when step > 0 and first <= last
|
|
when step < 0 and first >= last do
|
|
:lists.seq(first, last, step)
|
|
end
|
|
|
|
def to_list(_first.._last//_step) do
|
|
[]
|
|
end
|
|
|
|
# TODO: Remove me on v2.0
|
|
def to_list(%{__struct__: Range, first: first, last: last}) do
|
|
step = if first <= last, do: 1, else: -1
|
|
:lists.seq(first, last, step)
|
|
end
|
|
|
|
@doc """
|
|
Checks if two ranges are disjoint.
|
|
|
|
## Examples
|
|
|
|
iex> Range.disjoint?(1..5, 6..9)
|
|
true
|
|
iex> Range.disjoint?(5..1//-1, 6..9)
|
|
true
|
|
iex> Range.disjoint?(1..5, 5..9)
|
|
false
|
|
iex> Range.disjoint?(1..5, 2..7)
|
|
false
|
|
|
|
Steps are also considered when computing the ranges to be disjoint:
|
|
|
|
iex> Range.disjoint?(1..10//2, 2..10//2)
|
|
true
|
|
|
|
# First element in common is 29
|
|
iex> Range.disjoint?(1..100//14, 8..100//21)
|
|
false
|
|
iex> Range.disjoint?(57..-1//-14, 8..100//21)
|
|
false
|
|
iex> Range.disjoint?(1..100//14, 50..8//-21)
|
|
false
|
|
iex> Range.disjoint?(1..28//14, 8..28//21)
|
|
true
|
|
|
|
# First element in common is 14
|
|
iex> Range.disjoint?(2..28//3, 9..28//5)
|
|
false
|
|
iex> Range.disjoint?(26..2//-3, 29..9//-5)
|
|
false
|
|
|
|
# Starting from the back without alignment
|
|
iex> Range.disjoint?(27..11//-3, 30..0//-7)
|
|
true
|
|
|
|
"""
|
|
@doc since: "1.8.0"
|
|
@spec disjoint?(t, t) :: boolean
|
|
def disjoint?(first1..last1//step1 = range1, first2..last2//step2 = range2) do
|
|
if size(range1) == 0 or size(range2) == 0 do
|
|
true
|
|
else
|
|
{first1, last1, step1} = normalize(first1, last1, step1)
|
|
{first2, last2, step2} = normalize(first2, last2, step2)
|
|
|
|
cond do
|
|
last2 < first1 or last1 < first2 ->
|
|
true
|
|
|
|
abs(step1) == 1 and abs(step2) == 1 ->
|
|
false
|
|
|
|
true ->
|
|
# We need to find the first intersection of two arithmetic
|
|
# progressions and see if they belong within the ranges
|
|
# https://math.stackexchange.com/questions/1656120/formula-to-find-the-first-intersection-of-two-arithmetic-progressions
|
|
{gcd, u, v} = Integer.extended_gcd(-step1, step2)
|
|
|
|
if rem(first2 - first1, gcd) == 0 do
|
|
c = first1 - first2 + step2 - step1
|
|
t1 = -c / step2 * u
|
|
t2 = -c / step1 * v
|
|
t = max(floor(t1) + 1, floor(t2) + 1)
|
|
x = div(c * u + t * step2, gcd) - 1
|
|
y = div(c * v + t * step1, gcd) - 1
|
|
|
|
x < 0 or first1 + x * step1 > last1 or
|
|
y < 0 or first2 + y * step2 > last2
|
|
else
|
|
true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
@compile inline: [normalize: 3]
|
|
defp normalize(first, last, step) when first > last,
|
|
do: {first - abs(div(first - last, step) * step), first, -step}
|
|
|
|
defp normalize(first, last, step), do: {first, last, step}
|
|
|
|
@doc false
|
|
@deprecated "Pattern match on first..last//step instead"
|
|
def range?(%{__struct__: Range, first: first, last: 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//step, acc, fun) do
|
|
reduce(first, last, acc, fun, step)
|
|
end
|
|
|
|
# TODO: Remove me on v2.0
|
|
def reduce(%{__struct__: Range, first: first, last: last} = range, acc, fun) do
|
|
step = if first <= last, do: 1, else: -1
|
|
reduce(Map.put(range, :step, step), acc, fun)
|
|
end
|
|
|
|
defp reduce(_first, _last, {:halt, acc}, _fun, _step) do
|
|
{:halted, acc}
|
|
end
|
|
|
|
defp reduce(first, last, {:suspend, acc}, fun, step) do
|
|
{:suspended, acc, &reduce(first, last, &1, fun, step)}
|
|
end
|
|
|
|
defp reduce(first, last, {:cont, acc}, fun, step)
|
|
when step > 0 and first <= last
|
|
when step < 0 and first >= last do
|
|
reduce(first + step, last, fun.(first, acc), fun, step)
|
|
end
|
|
|
|
defp reduce(_, _, {:cont, acc}, _fun, _up) do
|
|
{:done, acc}
|
|
end
|
|
|
|
def member?(first..last//step, value) when is_integer(value) do
|
|
if step > 0 do
|
|
{:ok, first <= value and value <= last and rem(value - first, step) == 0}
|
|
else
|
|
{:ok, last <= value and value <= first and rem(value - first, step) == 0}
|
|
end
|
|
end
|
|
|
|
# TODO: Remove me on v2.0
|
|
def member?(%{__struct__: Range, first: first, last: last} = range, value)
|
|
when is_integer(value) do
|
|
step = if first <= last, do: 1, else: -1
|
|
member?(Map.put(range, :step, step), value)
|
|
end
|
|
|
|
def member?(_, _value) do
|
|
{:ok, false}
|
|
end
|
|
|
|
def count(range) do
|
|
{:ok, Range.size(range)}
|
|
end
|
|
|
|
def slice(first.._//step = range) do
|
|
{:ok, Range.size(range), &slice(first + &1 * step, step + &3 - 1, &2)}
|
|
end
|
|
|
|
# TODO: Remove me on v2.0
|
|
def slice(%{__struct__: Range, first: first, last: last} = range) do
|
|
step = if first <= last, do: 1, else: -1
|
|
slice(Map.put(range, :step, step))
|
|
end
|
|
|
|
defp slice(_current, _step, 0), do: []
|
|
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
|
|
end
|
|
|
|
defimpl Inspect, for: Range do
|
|
import Inspect.Algebra
|
|
import Kernel, except: [inspect: 2]
|
|
|
|
def inspect(first..last//1, opts) when last >= first do
|
|
concat([to_doc(first, opts), "..", to_doc(last, opts)])
|
|
end
|
|
|
|
def inspect(first..last//step, opts) do
|
|
concat([to_doc(first, opts), "..", to_doc(last, opts), "//", to_doc(step, opts)])
|
|
end
|
|
|
|
# TODO: Remove me on v2.0
|
|
def inspect(%{__struct__: Range, first: first, last: last} = range, opts) do
|
|
step = if first <= last, do: 1, else: -1
|
|
inspect(Map.put(range, :step, step), opts)
|
|
end
|
|
end
|