Current section

Files

Jump to
cbuf lib cbuf.ex
Raw

lib/cbuf.ex

defmodule Cbuf do
defstruct impl: :array.new(), size: 0, start: 0, current: 0, empty: true
@doc """
Create a new circular buffer of a given size.
iex> Cbuf.new(5)
#Cbuf<[]>
"""
def new(size) when size > 0 do
%__MODULE__{
impl: :array.new(size: size),
size: size,
start: 0,
current: 0,
empty: true
}
end
@doc """
Calculate the allocated size for the buffer.
This is maximum addressable size of the buffer, not how many values it currently contains. For the number of values in the current buffer, see `count/1`
iex> Cbuf.new(5) |> Cbuf.size()
5
"""
def size(buf) do
buf.size
end
@doc """
Insert a value into a circular buffer.
Values are inserted such that when the buffer is full, the oldest items are overwritten first.
iex> buf = Cbuf.new(5)
iex> buf |> Cbuf.insert("a") |> Cbuf.insert("b")
#Cbuf<["a", "b"]>
iex> buf = Cbuf.new(3)
iex> Enum.reduce(1..20, buf, fn(val, acc) -> Cbuf.insert(acc, val) end)
#Cbuf<[18, 19, 20]>
iex> buf = Cbuf.new(1)
iex> Enum.reduce(1..20, buf, fn(val, acc) -> Cbuf.insert(acc, val) end)
#Cbuf<[20]>
"""
def insert(buf, val) do
first = 0
size = buf.size
last = size - 1
{start, current, empty} =
case {buf.start, buf.current, buf.empty} do
# special case:
# handle the initial case where start and current both == 0
{s, c, true} when s == first and c == first ->
{first, first, false}
# special case:
# handle the case where start and current both == 0
# but a value has been set at current and the size
# of the buffer == 1
{s, c, false} when size == 1 and s == first and c == first ->
{first, first, false}
# special case:
# handle the case where start and current both == 0
# but a value has been set at current AND
# the buffer size is larger than 1
{s, c, false} when s == first and c == first ->
{first, 1, false}
# normal advance
{s, c, false} when c > s and c != last ->
{s, c + 1, false}
# normal advance with wraparound
{s, c, false} when s == first and c == last ->
{s + 1, first, false}
# normal advance with wraparound
{s, c, false} when s == last and c == s - 1 ->
{first, c + 1, false}
# normal advance
{s, c, false} when c == s - 1 ->
{s + 1, c + 1, false}
end
%{
buf
| impl: :array.set(current, val, buf.impl),
start: start,
current: current,
empty: empty
}
end
@doc """
See the oldest value in the buffer. Works in constant time.
iex> buf = Enum.reduce(1..20, Cbuf.new(3), fn(val, acc) -> Cbuf.insert(acc, val) end)
iex> Cbuf.peek(buf)
18
iex> buf = Cbuf.new(20) |> Cbuf.insert("ok") |> Cbuf.insert("fine")
iex> Cbuf.peek(buf)
"ok"
iex> Cbuf.new(3) |> Cbuf.peek()
nil
"""
def peek(buf) do
case :array.get(buf.start, buf.impl) do
:undefined -> nil
val -> val
end
end
@doc """
Convert a circular buffer to a list. The list is ordered by age, oldest to newest.
This operation takes linear time.
iex> buf = Cbuf.new(5)
iex> buf |> Cbuf.insert("a") |> Cbuf.insert("b") |> Cbuf.to_list()
["a", "b"]
iex> buf = Cbuf.new(3)
iex> Enum.reduce(1..20, buf, fn(val, acc) -> Cbuf.insert(acc, val) end) |> Cbuf.to_list()
[18, 19, 20]
iex> Cbuf.new(5) |> Cbuf.to_list()
[]
"""
def to_list(buf) do
do_to_list(buf, [], count(buf)) |> Enum.reverse()
end
defp do_to_list(_buf, list, 0), do: list
defp do_to_list(buf, list, remaining) do
value = :array.get(buf.start, buf.impl)
buf =
if buf.start == buf.size - 1 do
%{buf | start: 0}
else
%{buf | start: buf.start + 1}
end
do_to_list(buf, [value | list], remaining - 1)
end
@doc """
Returns the count of the non-empty values in the buffer.
iex> Cbuf.new(5) |> Cbuf.insert("hi") |> Cbuf.count()
1
iex> Cbuf.new(5) |> Cbuf.count()
0
iex> Cbuf.new(5) |> Cbuf.insert(nil) |> Cbuf.count()
1
"""
def count(buf) do
case {buf.start, buf.current, buf.empty} do
{s, c, false} when s == c + 1 ->
buf.size
{s, c, false} when c > s ->
c + 1 - s
{0, 0, true} ->
0
{0, 0, false} ->
1
end
end
@doc """
Queries `buf` for the presence of `val`.
iex> Cbuf.new(5) |> Cbuf.insert("hello") |> Cbuf.member?("hello")
true
iex> Cbuf.new(5) |> Cbuf.insert("hello") |> Cbuf.member?("nope")
false
"""
def member?(buf, val) do
:array.sparse_foldl(fn _idx, v, acc -> acc || val == v end, false, buf.impl)
end
defimpl Collectable do
def into(original) do
collector_fun = fn
buf, {:cont, val} ->
Cbuf.insert(buf, val)
buf, :done ->
buf
_buf, :halt ->
:ok
end
{original, collector_fun}
end
end
defimpl Enumerable do
def count(buf), do: {:ok, Cbuf.count(buf)}
def member?(buf, val), do: {:ok, Cbuf.member?(buf, val)}
def reduce(buf, acc, fun), do: Enumerable.List.reduce(Cbuf.to_list(buf), acc, fun)
def slice(_buf), do: {:error, __MODULE__}
end
defimpl Inspect do
import Inspect.Algebra
def inspect(buf, opts) do
concat(["#Cbuf<", to_doc(Cbuf.to_list(buf), opts), ">"])
end
end
end