/
coord.ex
173 lines (136 loc) · 4.97 KB
/
coord.ex
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
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# ┌────────────────────────────────────────────────────────────────────┐
# │ Based on the book "Functional Web Development" by Lance Halvorsen. │
# └────────────────────────────────────────────────────────────────────┘
defmodule Islands.Coord do
@moduledoc """
A coordinates struct and functions for the _Game of Islands_.
The coordinates struct contains the fields `row` and `col` representing the
coordinates of a square in the _Game of Islands_.
##### Based on the book [Functional Web Development](https://pragprog.com/titles/lhelph/functional-web-development-with-elixir-otp-and-phoenix/) by Lance Halvorsen.
"""
alias __MODULE__
@col_range 1..10
@row_range 1..10
@square_range 1..100
@derive Jason.Encoder
@enforce_keys [:row, :col]
defstruct [:row, :col]
@typedoc "Column number"
@type col :: 1..10
@typedoc "Row number"
@type row :: 1..10
@typedoc "Square number: (row - 1) * 10 + col"
@type square :: 1..100
@typedoc "A coordinates struct for the Game of Islands"
@type t :: %Coord{row: row, col: col}
@doc """
Returns `{:ok, coord}` or `{:error, reason}` if given an invalid `row` or
`col`.
## Examples
iex> alias Islands.Coord
iex> Coord.new(10, 10)
{:ok, %Coord{col: 10, row: 10}}
"""
@spec new(row, col) :: {:ok, t} | {:error, atom}
def new(row, col) when row in @row_range and col in @col_range do
{:ok, %Coord{row: row, col: col}}
end
def new(_row, _col), do: {:error, :invalid_coordinates}
@doc """
Returns a coordinates struct or raises if given an invalid `row` or `col`.
## Examples
iex> alias Islands.Coord
iex> Coord.new!(10, 10)
%Coord{row: 10, col: 10}
iex> alias Islands.Coord
iex> Coord.new!(0, 1)
** (ArgumentError) cannot create coord, reason: :invalid_coordinates
"""
@spec new!(row, col) :: t
def new!(row, col) do
case new(row, col) do
{:ok, coord} ->
coord
{:error, reason} ->
raise ArgumentError, "cannot create coord, reason: #{inspect(reason)}"
end
end
@doc """
Returns `{:ok, coord}` or `{:error, reason}` if given an invalid `square`.
## Examples
iex> alias Islands.Coord
iex> Coord.new(99)
{:ok, %Coord{row: 10, col: 9}}
"""
@spec new(square) :: {:ok, t} | {:error, atom}
def new(square) when square in @square_range,
do: rem(square, 10) |> coord(square)
def new(_square), do: {:error, :invalid_square_number}
@doc """
Returns a coordinates struct or raises if given an invalid `square`.
## Examples
iex> alias Islands.Coord
iex> Coord.new!(99)
%Coord{row: 10, col: 9}
iex> alias Islands.Coord
iex> Coord.new!(101)
** (ArgumentError) cannot create coord, reason: :invalid_square_number
"""
@spec new!(square) :: t
def new!(square) do
case new(square) do
{:ok, coord} ->
coord
{:error, reason} ->
raise ArgumentError, "cannot create coord, reason: #{inspect(reason)}"
end
end
@doc """
Returns a square number or `{:error, reason}` if given an invalid `coord`.
## Examples
iex> alias Islands.Coord
iex> {:ok, coord} = Coord.new(2, 9)
iex> Coord.to_square(coord)
19
"""
@spec to_square(Coord.t()) :: square | {:error, atom}
def to_square(%Coord{row: row, col: col} = _coord), do: (row - 1) * 10 + col
def to_square(_coord), do: {:error, :invalid_coord_struct}
@doc """
Returns "<row> <col>" or `{:error, reason}` if given an invalid `coord`.
## Examples
iex> alias Islands.Coord
iex> {:ok, coord} = Coord.new(2, 9)
iex> Coord.to_row_col(coord)
"2 9"
"""
@spec to_row_col(Coord.t()) :: String.t() | {:error, atom}
def to_row_col(%Coord{row: row, col: col} = _coord), do: "#{row} #{col}"
def to_row_col(_coord), do: {:error, :invalid_coord_struct}
@doc """
Compares two coordinates structs based on their square numbers.
## Examples
iex> alias Islands.Coord
iex> Coord.compare(Coord.new!(4, 7), Coord.new!(5, 7))
:lt
"""
@spec compare(t, t) :: :lt | :eq | :gt
def compare(%Coord{} = coord1, %Coord{} = coord2) do
case {to_square(coord1), to_square(coord2)} do
{square1, square2} when square1 > square2 -> :gt
{square1, square2} when square1 < square2 -> :lt
_ -> :eq
end
end
## Private functions
# coord(0, 20) # => new(2, 10)
# coord(9, 19) # => new(2, 9)
@spec coord(rem :: 0..9, square) :: {:ok, t}
defp coord(0, square), do: new(div(square, 10), 10)
defp coord(rem, square), do: new(div(square, 10) + 1, rem)
## Helpers
defimpl String.Chars, for: Coord do
@spec to_string(Coord.t()) :: String.t()
def to_string(%Coord{row: row, col: col} = _coord), do: "(#{row}, #{col})"
end
end