From a45135a1ff2335e308eefcdf9e2024449428b0fc Mon Sep 17 00:00:00 2001 From: Frank Hunleth Date: Sat, 18 Jul 2026 15:48:57 -0400 Subject: [PATCH] Allow 0-length circular buffer This comes up when code using CircularBuffer accumulate changes to an entry before putting it into the buffer. For this case, the CircularBuffer is allocated with n-1 entries. The entry accumulating changes gets committed to the CircularBuffer and then a new entry is started. This means that there are a max of n entries each time. The edge case is when n=1. When that happens, special case code needed to be added since CircularBuffers couldn't be length 0. Supporting length 0 gets rid of that special case code. The effect to the CircularBuffer implementation is to add an empty buffer check whenever it's time to reload the b list. This happens once every n insertions where n is the length of the CircularBuffer, so in addition to being a simple check, it's also not even run that often. Property tests that didn't require at least one element in the buffer were updated to exercise the 0-length case. --- lib/circular_buffer.ex | 13 ++++++++----- test/circular_buffer_test.exs | 14 +++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/lib/circular_buffer.ex b/lib/circular_buffer.ex index e2a6ec3..152602f 100644 --- a/lib/circular_buffer.ex +++ b/lib/circular_buffer.ex @@ -34,7 +34,7 @@ defmodule CircularBuffer do @opaque t() :: %__MODULE__{ a: list(), b: list(), - max_size: pos_integer(), + max_size: non_neg_integer(), count: non_neg_integer() } @@ -43,8 +43,8 @@ defmodule CircularBuffer do @doc """ Creates a new circular buffer with a given size. """ - @spec new(pos_integer()) :: t() - def new(size) when is_integer(size) and size > 0 do + @spec new(non_neg_integer()) :: t() + def new(size) when is_integer(size) and size >= 0 do %CB{a: [], b: [], max_size: size, count: 0} end @@ -60,11 +60,14 @@ defmodule CircularBuffer do %{cb | a: [item | cb.a], count: cb.count + 1} end - def insert(%CB{b: []} = cb, item) do - new_b = cb.a |> Enum.reverse() |> tl() + def insert(%CB{a: a, b: []} = cb, item) when a != [] do + new_b = a |> Enum.reverse() |> tl() %{cb | a: [item], b: new_b} end + # max_size==0 case + def insert(cb, _item), do: cb + @doc """ Converts a circular buffer to a list. The list is ordered from oldest to newest elements based on their insertion order. diff --git a/test/circular_buffer_test.exs b/test/circular_buffer_test.exs index 4fb3871..9f01299 100644 --- a/test/circular_buffer_test.exs +++ b/test/circular_buffer_test.exs @@ -11,8 +11,8 @@ defmodule CircularBufferTest do alias CircularBuffer, as: CB - property "new/1 returns an empty buffer for positive sizes" do - forall size <- pos_integer() do + property "new/1 returns an empty buffer for non-negative sizes" do + forall size <- non_neg_integer() do cb = CB.new(size) cb.max_size == size and @@ -23,10 +23,10 @@ defmodule CircularBufferTest do end end - property "new/1 raises for non-positive sizes" do - forall n <- non_neg_integer() do + property "new/1 raises for negative sizes" do + forall n <- neg_integer() do try do - CB.new(-n) + CB.new(n) false rescue FunctionClauseError -> true @@ -39,14 +39,14 @@ defmodule CircularBufferTest do end property "the count matches the number of elements in the buffer" do - forall {size, is} <- {pos_integer(), list(integer())} do + forall {size, is} <- {non_neg_integer(), list(integer())} do buffer = Enum.reduce(is, CB.new(size), fn i, cb -> CB.insert(cb, i) end) slow_cb_count(buffer) == buffer.count end end property "can tell the current number of elements" do - forall {size, is} <- {pos_integer(), list(integer())} do + forall {size, is} <- {non_neg_integer(), list(integer())} do buffer = Enum.reduce(is, CB.new(size), fn i, cb -> CB.insert(cb, i) end) Enum.count(buffer) == min(size, length(is)) end