Skip to content

Commit 967530f

Browse files
[#766] New practice exercise circular-buffer (#775)
Co-authored-by: Angelika Tyborska <[email protected]>
1 parent f05a1e3 commit 967530f

File tree

10 files changed

+491
-0
lines changed

10 files changed

+491
-0
lines changed

config.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2550,6 +2550,33 @@
25502550
"list-comprehensions"
25512551
],
25522552
"difficulty": 8
2553+
},
2554+
{
2555+
"slug": "circular-buffer",
2556+
"name": "Circular Buffer",
2557+
"uuid": "535d64c9-95f7-4f59-b7b6-d459337b82b0",
2558+
"prerequisites": [
2559+
"atoms",
2560+
"tuples",
2561+
"lists",
2562+
"structs",
2563+
"enum",
2564+
"agent",
2565+
"processes",
2566+
"pids",
2567+
"case",
2568+
"cond",
2569+
"if",
2570+
"multiple-clause-functions",
2571+
"pattern-matching",
2572+
"guards",
2573+
"errors"
2574+
],
2575+
"practices": [
2576+
"processes",
2577+
"errors"
2578+
],
2579+
"difficulty": 8
25532580
}
25542581
],
25552582
"foregone": [
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# Description
2+
3+
A circular buffer, cyclic buffer or ring buffer is a data structure that
4+
uses a single, fixed-size buffer as if it were connected end-to-end.
5+
6+
A circular buffer first starts empty and of some predefined length. For
7+
example, this is a 7-element buffer:
8+
<!-- prettier-ignore -->
9+
[ ][ ][ ][ ][ ][ ][ ]
10+
11+
Assume that a 1 is written into the middle of the buffer (exact starting
12+
location does not matter in a circular buffer):
13+
<!-- prettier-ignore -->
14+
[ ][ ][ ][1][ ][ ][ ]
15+
16+
Then assume that two more elements are added — 2 & 3 — which get
17+
appended after the 1:
18+
<!-- prettier-ignore -->
19+
[ ][ ][ ][1][2][3][ ]
20+
21+
If two elements are then removed from the buffer, the oldest values
22+
inside the buffer are removed. The two elements removed, in this case,
23+
are 1 & 2, leaving the buffer with just a 3:
24+
<!-- prettier-ignore -->
25+
[ ][ ][ ][ ][ ][3][ ]
26+
27+
If the buffer has 7 elements then it is completely full:
28+
<!-- prettier-ignore -->
29+
[5][6][7][8][9][3][4]
30+
31+
When the buffer is full an error will be raised, alerting the client
32+
that further writes are blocked until a slot becomes free.
33+
34+
When the buffer is full, the client can opt to overwrite the oldest
35+
data with a forced write. In this case, two more elements — A & B —
36+
are added and they overwrite the 3 & 4:
37+
<!-- prettier-ignore -->
38+
[5][6][7][8][9][A][B]
39+
40+
3 & 4 have been replaced by A & B making 5 now the oldest data in the
41+
buffer. Finally, if two elements are removed then what would be
42+
returned is 5 & 6 yielding the buffer:
43+
<!-- prettier-ignore -->
44+
[ ][ ][7][8][9][A][B]
45+
46+
Because there is space available, if the client again uses overwrite
47+
to store C & D then the space where 5 & 6 were stored previously will
48+
be used not the location of 7 & 8. 7 is still the oldest element and
49+
the buffer is once again full.
50+
<!-- prettier-ignore -->
51+
[C][D][7][8][9][A][B]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Used by "mix format"
2+
[
3+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
4+
]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"authors": ["jiegillet"],
3+
"contributors": [
4+
"angelikatyborska"
5+
],
6+
"files": {
7+
"example": [
8+
".meta/example.ex"
9+
],
10+
"solution": [
11+
"lib/circular_buffer.ex"
12+
],
13+
"test": [
14+
"test/circular_buffer_test.exs"
15+
]
16+
},
17+
"blurb": "A data structure that uses a single, fixed-size buffer as if it were connected end-to-end.",
18+
"source": "Wikipedia",
19+
"source_url": "http://en.wikipedia.org/wiki/Circular_buffer"
20+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
defmodule CircularBuffer do
2+
use GenServer
3+
4+
@moduledoc """
5+
An API to a stateful process that fills and empties a circular buffer
6+
"""
7+
8+
# CLIENT API
9+
10+
@doc """
11+
Create a new buffer of a given capacity
12+
"""
13+
@spec new(capacity :: integer) :: {:ok, pid}
14+
def new(capacity) do
15+
GenServer.start_link(__MODULE__, capacity, [])
16+
end
17+
18+
@doc """
19+
Read the oldest entry in the buffer, fail if it is empty
20+
"""
21+
@spec read(buffer :: pid) :: {:ok, any} | {:error, atom}
22+
def read(buffer) do
23+
GenServer.call(buffer, :read)
24+
end
25+
26+
@doc """
27+
Write a new item in the buffer, fail if is full
28+
"""
29+
@spec write(buffer :: pid, item :: any) :: :ok | {:error, atom}
30+
def write(buffer, item) do
31+
GenServer.call(buffer, {:write, item})
32+
end
33+
34+
@doc """
35+
Write an item in the buffer, overwrite the oldest entry if it is full
36+
"""
37+
@spec overwrite(buffer :: pid, item :: any) :: :ok
38+
def overwrite(buffer, item) do
39+
GenServer.cast(buffer, {:overwrite, item})
40+
end
41+
42+
@doc """
43+
Clear the buffer
44+
"""
45+
@spec clear(buffer :: pid) :: :ok
46+
def clear(buffer) do
47+
GenServer.cast(buffer, :clear)
48+
end
49+
50+
# DATA STRUCTURE
51+
# Essentially a deque made out of two lists, one for new input (write, overwrite)
52+
# and one for output (read), and keeping track of the size and capacity.
53+
54+
defstruct [:capacity, size: 0, input: [], output: []]
55+
56+
def new_buffer(capacity), do: {:ok, %CircularBuffer{capacity: capacity}}
57+
58+
def read_buffer(%CircularBuffer{size: 0} = buffer), do: {{:error, :empty}, buffer}
59+
60+
def read_buffer(%CircularBuffer{size: size, output: [out | output]} = buffer),
61+
do: {{:ok, out}, %{buffer | size: size - 1, output: output}}
62+
63+
def read_buffer(%CircularBuffer{input: input} = buffer),
64+
do: read_buffer(%{buffer | input: [], output: Enum.reverse(input)})
65+
66+
def write_buffer(%CircularBuffer{size: capacity, capacity: capacity} = buffer, _item),
67+
do: {{:error, :full}, buffer}
68+
69+
def write_buffer(%CircularBuffer{size: size, input: input} = buffer, item),
70+
do: {:ok, %{buffer | size: size + 1, input: [item | input]}}
71+
72+
def overwrite_buffer(%CircularBuffer{size: capacity, capacity: capacity} = buffer, item) do
73+
{_, smaller_buffer} = read_buffer(buffer)
74+
write_buffer(smaller_buffer, item)
75+
end
76+
77+
def overwrite_buffer(buffer, item), do: write_buffer(buffer, item)
78+
79+
def clear_buffer(%CircularBuffer{capacity: capacity}), do: %CircularBuffer{capacity: capacity}
80+
81+
@impl true
82+
def init(capacity) do
83+
new_buffer(capacity)
84+
end
85+
86+
# SERVER API
87+
88+
@impl true
89+
def handle_call(:read, _from, buffer) do
90+
{reply, buffer} = read_buffer(buffer)
91+
{:reply, reply, buffer}
92+
end
93+
94+
@impl true
95+
def handle_call({:write, item}, _from, buffer) do
96+
{reply, buffer} = write_buffer(buffer, item)
97+
{:reply, reply, buffer}
98+
end
99+
100+
@impl true
101+
def handle_cast({:overwrite, item}, buffer) do
102+
{_reply, buffer} = overwrite_buffer(buffer, item)
103+
{:noreply, buffer}
104+
end
105+
106+
@impl true
107+
def handle_cast(:clear, buffer) do
108+
{:noreply, clear_buffer(buffer)}
109+
end
110+
end
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
# This is an auto-generated file.
2+
#
3+
# Regenerating this file via `configlet sync` will:
4+
# - Recreate every `description` key/value pair
5+
# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications
6+
# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion)
7+
# - Preserve any other key/value pair
8+
#
9+
# As user-added comments (using the # character) will be removed when this file
10+
# is regenerated, comments can be added via a `comment` key.
11+
[28268ed4-4ff3-45f3-820e-895b44d53dfa]
12+
description = "reading empty buffer should fail"
13+
14+
[2e6db04a-58a1-425d-ade8-ac30b5f318f3]
15+
description = "can read an item just written"
16+
17+
[90741fe8-a448-45ce-be2b-de009a24c144]
18+
description = "each item may only be read once"
19+
20+
[be0e62d5-da9c-47a8-b037-5db21827baa7]
21+
description = "items are read in the order they are written"
22+
23+
[2af22046-3e44-4235-bfe6-05ba60439d38]
24+
description = "full buffer can't be written to"
25+
26+
[547d192c-bbf0-4369-b8fa-fc37e71f2393]
27+
description = "a read frees up capacity for another write"
28+
29+
[04a56659-3a81-4113-816b-6ecb659b4471]
30+
description = "read position is maintained even across multiple writes"
31+
32+
[60c3a19a-81a7-43d7-bb0a-f07242b1111f]
33+
description = "items cleared out of buffer can't be read"
34+
35+
[45f3ae89-3470-49f3-b50e-362e4b330a59]
36+
description = "clear frees up capacity for another write"
37+
38+
[e1ac5170-a026-4725-bfbe-0cf332eddecd]
39+
description = "clear does nothing on empty buffer"
40+
41+
[9c2d4f26-3ec7-453f-a895-7e7ff8ae7b5b]
42+
description = "overwrite acts like write on non-full buffer"
43+
44+
[880f916b-5039-475c-bd5c-83463c36a147]
45+
description = "overwrite replaces the oldest item on full buffer"
46+
47+
[bfecab5b-aca1-4fab-a2b0-cd4af2b053c3]
48+
description = "overwrite replaces the oldest item remaining in buffer following a read"
49+
50+
[9cebe63a-c405-437b-8b62-e3fdc1ecec5a]
51+
description = "initial clear does not affect wrapping around"
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule CircularBuffer do
2+
@moduledoc """
3+
An API to a stateful process that fills and empties a circular buffer
4+
"""
5+
6+
@doc """
7+
Create a new buffer of a given capacity
8+
"""
9+
@spec new(capacity :: integer) :: {:ok, pid}
10+
def new(capacity) do
11+
end
12+
13+
@doc """
14+
Read the oldest entry in the buffer, fail if it is empty
15+
"""
16+
@spec read(buffer :: pid) :: {:ok, any} | {:error, atom}
17+
def read(buffer) do
18+
end
19+
20+
@doc """
21+
Write a new item in the buffer, fail if is full
22+
"""
23+
@spec write(buffer :: pid, item :: any) :: :ok | {:error, atom}
24+
def write(buffer, item) do
25+
end
26+
27+
@doc """
28+
Write an item in the buffer, overwrite the oldest entry if it is full
29+
"""
30+
@spec overwrite(buffer :: pid, item :: any) :: :ok
31+
def overwrite(buffer, item) do
32+
end
33+
34+
@doc """
35+
Clear the buffer
36+
"""
37+
@spec clear(buffer :: pid) :: :ok
38+
def clear(buffer) do
39+
end
40+
end
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
defmodule CircularBuffer.MixProject do
2+
use Mix.Project
3+
4+
def project do
5+
[
6+
app: :circular_buffer,
7+
version: "0.1.0",
8+
# elixir: "~> 1.8",
9+
start_permanent: Mix.env() == :prod,
10+
deps: deps()
11+
]
12+
end
13+
14+
# Run "mix help compile.app" to learn about applications.
15+
def application do
16+
[
17+
extra_applications: [:logger]
18+
]
19+
end
20+
21+
# Run "mix help deps" to learn about dependencies.
22+
defp deps do
23+
[
24+
# {:dep_from_hexpm, "~> 0.3.0"},
25+
# {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
26+
]
27+
end
28+
end

0 commit comments

Comments
 (0)