Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/DataStructures.jl
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,11 @@ module DataStructures

export
CircularBuffer,
CircularVectorBuffer,
capacity,
isfull
include("circular_buffer.jl")
include("circular_vector_buffer.jl")

export status
export deref_key, deref_value, deref, advance, regress
Expand Down
6 changes: 3 additions & 3 deletions src/circular_buffer.jl
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""
CircularBuffer{T}(n)

The CircularBuffer type implements a circular buffer of fixed capacity
where new items are pushed to the back of the list, overwriting values
The CircularBuffer type implements a circular buffer of fixed capacity
where new items are pushed to the back of the list, overwriting values
in a circular fashion.

Allocate a buffer of elements of type `T` with maximum capacity `n`.
Expand Down Expand Up @@ -113,7 +113,7 @@ end
"""
pushfirst!(cb, data)

Insert one or more items at the beginning of CircularBuffer
Insert one or more items at the beginning of CircularBuffer
and overwrite back if full.
"""
function pushfirst!(cb::CircularBuffer, data)
Expand Down
191 changes: 191 additions & 0 deletions src/circular_vector_buffer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"""
CircularVectorBuffer{T}(n)

The CircularVectorBuffer type implements a circular buffer of fixed capacity
where new items are pushed to the back of the list, overwriting values
in a circular fashion.

Allocate a buffer of elements of type `T` with maximum capacity `n`.
"""
mutable struct CircularVectorBuffer{T} <: AbstractMatrix{T}
capacity::Int
first::Int
length::Int
buffer::Matrix{T}

CircularVectorBuffer{T}(capacity::Int, second_dim::Int) where {T} = new{T}(capacity, 1, 0, Matrix{T}(undef, capacity, second_dim))
end

"""
empty!(cb)

Reset the buffer.
"""
function Base.empty!(cb::CircularVectorBuffer)
cb.length = 0
cb
end

Base.@propagate_inbounds function _buffer_index_checked(cb::CircularVectorBuffer, i::Int)
@boundscheck if i < 1 || i > cb.length
throw(BoundsError(cb, i))
end
_buffer_index(cb, i)
end

@inline function _buffer_index(cb::CircularVectorBuffer, i::Int)
n = cb.capacity
idx = cb.first + i - 1
if idx > n
idx - n
else
idx
end
end

"""
cb[i]

Get the i-th element of CircularVectorBuffer.

* `cb[1]` to get the element at the front
* `cb[end]` to get the element at the back
"""
@inline Base.@propagate_inbounds function Base.getindex(cb::CircularVectorBuffer, i::Int, j::Int)
cb.buffer[_buffer_index_checked(cb, i), j]
end

"""
cb[i] = data

Store data to the i-th element of CircularVectorBuffer.
"""
@inline Base.@propagate_inbounds function Base.setindex!(cb::CircularVectorBuffer, data, i::Int, j::Int)
cb.buffer[_buffer_index_checked(cb, i), j] = data
cb
end

"""
pop!(cb)

Remove the element at the back.
"""
@inline function Base.pop!(cb::CircularVectorBuffer)
if cb.length == 0
throw(ArgumentError("array must be non-empty"))
end
i = _buffer_index(cb, cb.length)
cb.length -= 1
cb.buffer[i, :]
end

"""
push!(cb)

Add an element to the back and overwrite front if full.
"""
@inline function Base.push!(cb::CircularVectorBuffer, data)
# if full, increment and overwrite, otherwise push
if cb.length == cb.capacity
cb.first = (cb.first == cb.capacity ? 1 : cb.first + 1)
else
cb.length += 1
end
cb.buffer[_buffer_index(cb, cb.length), :] = data
cb
end

"""
popfirst!(cb)

Remove the first item (at the back) from CircularVectorBuffer.
"""
function popfirst!(cb::CircularVectorBuffer)
if cb.length == 0
throw(ArgumentError("array must be non-empty"))
end
i = cb.first
cb.first = (cb.first + 1 > cb.capacity ? 1 : cb.first + 1)
cb.length -= 1
cb.buffer[i, :]
end

"""
pushfirst!(cb, data)

Insert one or more items at the beginning of CircularVectorBuffer
and overwrite back if full.
"""
function pushfirst!(cb::CircularVectorBuffer, data)
# if full, decrement and overwrite, otherwise pushfirst
cb.first = (cb.first == 1 ? cb.capacity : cb.first - 1)
if cb.length < cb.capacity
cb.length += 1
end
cb.buffer[cb.first, :] = data
cb
end

"""
append!(cb, datavec)

Push at most last `capacity` items.
"""
function Base.append!(cb::CircularVectorBuffer, datamat::AbstractMatrix)
# push at most last `capacity` items
n = size(datamat, 1)
for i in max(1, n-capacity(cb)+1):n
push!(cb, datamat[i, :])
end
cb
end

"""
fill!(cb, data)

Grows the buffer up-to capacity, and fills it entirely.
It doesn't overwrite existing elements.
"""
function Base.fill!(cb::CircularVectorBuffer, data)
for i in 1:capacity(cb)-cb.length
push!(cb, data)
end
cb
end

"""
length(cb)

Return the number of elements currently in the buffer.
"""
Base.length(cb::CircularVectorBuffer) = cb.length * size(cb.buffer, 2)

"""
size(cb)

Return a tuple with the size of the buffer.
"""
Base.size(cb::CircularVectorBuffer) = (cb.length, size(cb.buffer, 2))

Base.convert(::Type{Array}, cb::CircularVectorBuffer{T}) where {T} = T[x for x in cb]

"""
isempty(cb)

Test whether the buffer is empty.
"""
Base.isempty(cb::CircularVectorBuffer) = cb.length == 0

""""
capacity(cb)

Return capacity of CircularVectorBuffer.
"""
capacity(cb::CircularVectorBuffer) = cb.capacity

"""
isfull(cb)

Test whether the buffer is full.
"""
isfull(cb::CircularVectorBuffer) = cb.length == cb.capacity
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ tests = ["int_set",
"list",
"multi_dict",
"circular_buffer",
"circular_vector_buffer",
"sorting",
"priority_queue"
]
Expand Down
127 changes: 127 additions & 0 deletions test/test_circular_vector_buffer.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
@testset "CircularVectorBuffer" begin

@testset "Core Functionality" begin
cb = CircularVectorBuffer{Int}(5, 2)
@testset "When empty" begin
@test length(cb) == 0
@test capacity(cb) == 5
@test_throws BoundsError first(cb)
@test isempty(cb) == true
@test isfull(cb) == false
end

@testset "With 1 element" begin
push!(cb, [1, 1])
@test length(cb) == 2
@test capacity(cb) == 5
@test isfull(cb) == false
end

@testset "Appending many elements" begin
append!(cb, [2:8 2:8])
@test capacity(cb) == size(cb, 1)
@test length(cb) == size(cb, 1) * size(cb, 2)
@test isempty(cb) == false
@test isfull(cb) == true
@test convert(Matrix, cb) == Int[4:8 4:8]
end

@testset "getindex" begin
@test cb[1,:] == [4, 4]
@test cb[2,:] == [5, 5]
@test cb[3,:] == [6, 6]
@test cb[4,:] == [7, 7]
@test cb[5,:] == [8, 8]
@test_throws BoundsError cb[6,:]
@test_throws BoundsError cb[3:6,:]
@test cb[3:4,:] == Int[6:7 6:7]
@test cb[[1,5],:] == Int[4 4; 8 8]
@test cb[1] == 4
end

@testset "setindex" begin
cb[3,:] .= 999
@test convert(Array, cb) == Int[4 4; 5 5; 999 999; 7 7; 8 8]
cb[1] = 1000
@test convert(Array, cb) == Int[1000 4; 5 5; 999 999; 7 7; 8 8]
end
end


@testset "pushfirst" begin
cb = CircularVectorBuffer{Int}(5, 2) # New, empty one for full test coverage
for i in -5:5
pushfirst!(cb, [i, i + 1])
end
arr = convert(Array, cb)
@test arr == Int[5 6; 4 5; 3 4; 2 3; 1 2]
for (idx, n) in enumerate(5:1)
@test arr[idx, :] == [n, n + 1]
end
end

@testset "Issue 429" begin
cb = CircularVectorBuffer{Int}(5, 2)
map(x -> pushfirst!(cb, [x, x + 1]), 1:8)
pop!(cb)
pushfirst!(cb, [9, 10])
@test size(cb.buffer, 1) == cb.capacity
arr = convert(Array, cb)
@test arr == Int[9 10; 8 9; 7 8; 6 7; 5 6]
end

@testset "Issue 379" begin
cb = CircularVectorBuffer{Int}(5, 2)
pushfirst!(cb, [1, 2])
@test cb == [1 2]
pushfirst!(cb, [2, 3])
@test cb == [2 3; 1 2]
end

@testset "empty!" begin
cb = CircularVectorBuffer{Int}(5, 2)
push!(cb, [13, 14])
empty!(cb)
@test length(cb) == 0
end

@testset "pop!" begin
cb = CircularVectorBuffer{Int}(5, 2)
for i in 0:5 # one extra to force wraparound
push!(cb, [i, i + 1])
end
for j in 5:-1:1
@test pop!(cb) == [j, j + 1]
@test convert(Array, cb) == [1:j-1 2:j]
end
@test isempty(cb)
@test_throws ArgumentError pop!(cb)
end

@testset "popfirst!" begin
cb = CircularVectorBuffer{Int}(5, 2)
for i in 0:5 # one extra to force wraparound
push!(cb, [i, i + 1])
end
for j in 1:5
@test popfirst!(cb) == [j, j + 1]
@test convert(Array, cb) == [j+1:5 j+2:6]
end
@test isempty(cb)
@test_throws ArgumentError popfirst!(cb)
end

@testset "fill!" begin
@testset "fill an empty buffer" begin
cb = CircularVectorBuffer{Int}(3, 2)
fill!(cb, [42, 42])
@test Array(cb) == ones(3,2) * 42
end
@testset "fill a non empty buffer" begin
cb = CircularVectorBuffer{Int}(3, 2)
push!(cb, [21, 22])
fill!(cb, [42, 43])
@test Array(cb) == [21 22; 42 43; 42 43]
end
end
end