summaryrefslogtreecommitdiff
path: root/lib/elixir/lib/range.ex
blob: 4d4a7cf684728fcf71c4c5d36d900ac377306d99 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
defmodule Range do
  @moduledoc """
  Defines a range.

  A range represents a discrete number of values where
  the first and last values are integers.

  Ranges can be either increasing (first <= last) or
  decresing (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

  """

  defstruct first: nil, last: nil

  @type t :: %Range{}
  @type t(first, last) :: %Range{first: first, last: last}

  @doc """
  Creates a new range.
  """
  @spec new(integer, integer) :: t
  def new(first, last) when is_integer(first) and is_integer(last) do
    %Range{first: first, last: last}
  end

  @doc """
  Returns `true` if the given `term` is a range.

  ## Examples

      iex> Range.range?(1..3)
      true

      iex> Range.range?(0)
      false

  """
  # @spec range?(term) :: boolean
  @spec range?(arg) :: false when arg: %Range{first: nil, last: nil}
  @spec range?(arg) :: true when arg: %Range{}
  @spec range?(term) :: false
  def range?(term)
  def range?(%Range{first: nil, last: nil}), do: false
  def range?(%Range{}), do: true
  def range?(_), do: false
end

defimpl Enumerable, for: Range do
  def reduce(first .. last, acc, fun) do
    validate_range!(first, last)
    reduce(first, last, acc, fun, last >= first)
  end

  defp reduce(_x, _y, {:halt, acc}, _fun, _up) do
    {:halted, acc}
  end

  defp reduce(x, y, {:suspend, acc}, fun, up) do
    {:suspended, acc, &reduce(x, y, &1, fun, up)}
  end

  defp reduce(x, y, {:cont, acc}, fun, true) when x <= y do
    reduce(x + 1, y, fun.(x, acc), fun, true)
  end

  defp reduce(x, y, {:cont, acc}, fun, false) when x >= y do
    reduce(x - 1, y, fun.(x, acc), fun, false)
  end

  defp reduce(_, _, {:cont, acc}, _fun, _up) do
    {:done, acc}
  end

  def member?(first .. last, value) do
    validate_range!(first, last)
    if first <= last do
      {:ok, first <= value and value <= last}
    else
      {:ok, last <= value and value <= first}
    end
  end

  def count(first .. last) do
    validate_range!(first, last)
    if first <= last do
      {:ok, last - first + 1}
    else
      {:ok, first - last + 1}
    end
  end

  defp validate_range!(first, last) when is_integer(first)
    and is_integer(last),
    do: :ok

  defp validate_range!(first, last) do
    raise ArgumentError,
      "ranges (first .. last) expect both sides to be integers, " <>
      "got: #{Macro.to_string({:.., [], [first, last]})}"
  end
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