Skip to content

Commit cf1a819

Browse files
committed
Initial commit
0 parents  commit cf1a819

9 files changed

Lines changed: 288 additions & 0 deletions

File tree

.formatter.exs

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"]
4+
]

.gitignore

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# The directory Mix will write compiled artifacts to.
2+
/_build/
3+
4+
# If you run "mix test --cover", coverage assets end up here.
5+
/cover/
6+
7+
# The directory Mix downloads your dependencies sources to.
8+
/deps/
9+
10+
# Where third-party dependencies like ExDoc output generated docs.
11+
/doc/
12+
13+
# Ignore .fetch files in case you like to edit your project deps locally.
14+
/.fetch
15+
16+
# If the VM crashes, it generates a dump, let's ignore it too.
17+
erl_crash.dump
18+
19+
# Also ignore archive artifacts (built via "mix archive.build").
20+
*.ez
21+
22+
# Ignore package tarball (built via "mix hex.build").
23+
circular_buffer-*.tar
24+

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# CircularBuffer
2+
3+
CircularBuffer provides a general-purpose CircularBuffer data structure.
4+
5+
Docs: [https://hexdocs.pm/circular_buffer](https://hexdocs.pm/circular_buffer).
6+
7+
## Installation
8+
9+
```elixir
10+
def deps do
11+
[
12+
{:circular_buffer, "~> 0.1"}
13+
]
14+
end
15+
```
16+
17+
## Should I use this?
18+
19+
The entire codebase is less than 50 lines of code and has been tested using
20+
property based testing. I believe the implementation is sound but it may not
21+
be the highest performance library out there.
22+

config/config.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This file is responsible for configuring your application
2+
# and its dependencies with the aid of the Mix.Config module.
3+
use Mix.Config
4+
5+
# This configuration is loaded before any dependency and is restricted
6+
# to this project. If another project depends on this project, this
7+
# file won't be loaded nor affect the parent project. For this reason,
8+
# if you want to provide default values for your application for
9+
# third-party users, it should be done in your "mix.exs" file.
10+
11+
# You can configure your application as:
12+
#
13+
# config :circular_buffer, key: :value
14+
#
15+
# and access this configuration in your application as:
16+
#
17+
# Application.get_env(:circular_buffer, :key)
18+
#
19+
# You can also configure a third-party app:
20+
#
21+
# config :logger, level: :info
22+
#
23+
24+
# It is also possible to import configuration files, relative to this
25+
# directory. For example, you can emulate configuration per environment
26+
# by uncommenting the line below and defining dev.exs, test.exs and such.
27+
# Configuration from the imported file will override the ones defined
28+
# here (which is why it is important to import them last).
29+
#
30+
# import_config "#{Mix.env()}.exs"

lib/circular_buffer.ex

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule CircularBuffer do
2+
@moduledoc """
3+
Circular Buffer built around erlang's queue module.
4+
5+
When creating a circular buffer you must specify the max size:
6+
7+
```
8+
cb = CircularBuffer.new(10)
9+
```
10+
"""
11+
12+
def new(size) when is_integer(size) and size > 0 do
13+
%{q: :queue.new(), max_size: size, count: 0}
14+
end
15+
16+
def insert(cb, item) do
17+
if cb.count < cb.max_size do
18+
%{cb | q: :queue.cons(item, cb.q), count: cb.count + 1}
19+
else
20+
new_q =
21+
cb.q
22+
|> :queue.drop_r
23+
|> (fn q -> :queue.cons(item, q) end).()
24+
25+
%{cb | q: new_q}
26+
end
27+
end
28+
29+
def to_list(cb) do
30+
cb.q
31+
|> :queue.reverse
32+
|> :queue.to_list
33+
end
34+
35+
def newest(cb) do
36+
case :queue.peek(cb.q) do
37+
{_, val} -> val
38+
:empty -> nil
39+
end
40+
end
41+
42+
def oldest(cb) do
43+
case :queue.peek_r(cb.q) do
44+
{_, val} -> val
45+
:empty -> nil
46+
end
47+
end
48+
end

mix.exs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule CircularBuffer.MixProject do
2+
use Mix.Project
3+
4+
@version "0.1.0"
5+
6+
def project do
7+
[
8+
app: :circular_buffer,
9+
version: @version,
10+
elixir: "~> 1.8",
11+
start_permanent: Mix.env() == :prod,
12+
deps: deps(),
13+
description: description(),
14+
package: package(),
15+
name: "CircularBuffer",
16+
source_url: "https://github.com/keathley/circular_buffer",
17+
docs: docs()
18+
]
19+
end
20+
21+
# Run "mix help compile.app" to learn about applications.
22+
def application do
23+
[
24+
extra_applications: [:logger]
25+
]
26+
end
27+
28+
# Run "mix help deps" to learn about dependencies.
29+
defp deps do
30+
[
31+
{:propcheck, "~> 1.2", only: [:dev, :test]},
32+
{:ex_doc, "~> 0.19", only: [:dev, :test]}
33+
]
34+
end
35+
36+
def description do
37+
"""
38+
General purpose circular buffer.
39+
"""
40+
end
41+
42+
def package do
43+
[
44+
name: "circular_buffer",
45+
licenses: ["MIT"],
46+
links: %{"GitHub" => "https://github.com/keathley/circular_buffer"}
47+
]
48+
end
49+
50+
def docs do
51+
[
52+
source_ref: "v#{@version}",
53+
source_url: "https://github.com/keathley/circular_buffer",
54+
main: "CircularBuffer"
55+
]
56+
end
57+
end

mix.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
%{
2+
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
3+
"ex_doc": {:hex, :ex_doc, "0.21.2", "caca5bc28ed7b3bdc0b662f8afe2bee1eedb5c3cf7b322feeeb7c6ebbde089d6", [:mix], [{:earmark, "~> 1.3.3 or ~> 1.4", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
4+
"makeup": {:hex, :makeup, "1.0.0", "671df94cf5a594b739ce03b0d0316aa64312cee2574b6a44becb83cd90fb05dc", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"},
5+
"makeup_elixir": {:hex, :makeup_elixir, "0.14.0", "cf8b7c66ad1cff4c14679698d532f0b5d45a3968ffbcbfd590339cb57742f1ae", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"},
6+
"nimble_parsec": {:hex, :nimble_parsec, "0.5.2", "1d71150d5293d703a9c38d4329da57d3935faed2031d64bc19e77b654ef2d177", [:mix], [], "hexpm"},
7+
"propcheck": {:hex, :propcheck, "1.2.0", "e2b84f2f1a4c46b6b2aa22a0f6ddf97696f99d4a5c8f71d45f6519741e727eca", [:mix], [{:proper, "~> 1.3", [hex: :proper, repo: "hexpm", optional: false]}], "hexpm"},
8+
"proper": {:hex, :proper, "1.3.0", "c1acd51c51da17a2fe91d7a6fc6a0c25a6a9849d8dc77093533109d1218d8457", [:make, :mix, :rebar3], [], "hexpm"},
9+
}

test/circular_buffer_test.exs

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
defmodule CircularBufferTest do
2+
use ExUnit.Case, async: false
3+
use PropCheck
4+
5+
alias CircularBuffer, as: CB
6+
7+
property "new/1 accepts a max size" do
8+
forall i <- integer() do
9+
try do
10+
buffer = CB.new(i)
11+
buffer.max_size == i
12+
rescue
13+
FunctionClauseError ->
14+
i <= 0
15+
16+
_ ->
17+
false
18+
end
19+
end
20+
end
21+
22+
property "the count matches the number of elements in the buffer" do
23+
forall {size, is} <- {pos_integer(), list(integer())} do
24+
buffer = Enum.reduce(is, CB.new(size), fn i, cb -> CB.insert(cb, i) end)
25+
:queue.len(buffer.q) == buffer.count
26+
end
27+
end
28+
29+
property "the number of elements never exceeds the size of the buffer" do
30+
forall {size, is} <- size_and_list() do
31+
buffer = Enum.reduce(is, CB.new(size), fn i, cb -> CB.insert(cb, i) end)
32+
:queue.len(buffer.q) <= size
33+
end
34+
end
35+
36+
property "the 'oldest' elements are dropped from the buffer" do
37+
forall {size, is} <- size_and_list() do
38+
iis = Enum.with_index(is)
39+
40+
buffer =
41+
iis
42+
|> Enum.reduce(CB.new(size), fn i, cb -> CB.insert(cb, i) end)
43+
44+
slice =
45+
iis
46+
|> Enum.reverse
47+
|> Enum.take(size)
48+
49+
:queue.to_list(buffer.q) == slice
50+
end
51+
end
52+
53+
property "newest/1 returns the newest element in the buffer" do
54+
forall {size, is} <- size_and_list() do
55+
iis = Enum.with_index(is)
56+
57+
buffer = Enum.reduce(iis, CB.new(size), fn i, cb -> CB.insert(cb, i) end)
58+
59+
CB.newest(buffer) == Enum.at(Enum.reverse(iis), 0)
60+
end
61+
end
62+
63+
property "oldest/1 returns the oldest element in the buffer" do
64+
forall {size, is} <- size_and_list() do
65+
iis = Enum.with_index(is)
66+
67+
buffer = Enum.reduce(iis, CB.new(size), fn i, cb -> CB.insert(cb, i) end)
68+
69+
oldest =
70+
iis
71+
|> Enum.reverse()
72+
|> Enum.drop(size-1)
73+
|> Enum.at(0)
74+
75+
CB.oldest(buffer) == oldest
76+
end
77+
end
78+
79+
def size_and_list do
80+
let size <- pos_integer() do
81+
let is <- ints(size*2, []) do
82+
{size, is}
83+
end
84+
end
85+
end
86+
87+
defp ints(0, acc), do: acc
88+
defp ints(size, acc) do
89+
let i <- integer() do
90+
ints(size-1, [i | acc])
91+
end
92+
end
93+
end

test/test_helper.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ExUnit.start()

0 commit comments

Comments
 (0)