Skip to content

Commit

Permalink
Add last / addFirst to CircularBuffer
Browse files Browse the repository at this point in the history
Motivation:

Its often useful to be able to add / retrieve elements on both sides of a CircularBuffer.

Modifications:

Add last / addFirst to be able to act on both ends.
Add tests

Result:

Fixes #279
  • Loading branch information
normanmaurer committed Apr 23, 2018
1 parent cbb3f19 commit be447f9
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 22 deletions.
91 changes: 69 additions & 22 deletions Sources/NIO/CircularBuffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,36 +50,63 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {
///
/// Amortized *O(1)*
public mutating func append(_ value: E) {
return self.addLast(value)
}

/// Add an element to the end of the ring buffer.
///
/// Amortized *O(1)*
public mutating func addLast(_ value: E) {
self.buffer[self.tailIdx] = value
self.tailIdx = (self.tailIdx + 1) & self.mask

if self.headIdx == self.tailIdx {
// No more room left for another append so grow the buffer now.
var newBacking: ContiguousArray<E?> = []
let newCapacity = self.buffer.count << 1 // Double the storage.
precondition(newCapacity > 0, "Can't double capacity of \(self.buffer.count)")
assert(newCapacity % 2 == 0)

newBacking.reserveCapacity(newCapacity)
newBacking.append(contentsOf: self.buffer[self.headIdx..<self.buffer.count])
if self.headIdx > 0 {
newBacking.append(contentsOf: self.buffer[0..<self.headIdx])
}
newBacking.append(contentsOf: repeatElement(nil, count: newCapacity - newBacking.count))
self.tailIdx = self.buffer.count
self.headIdx = 0
self.buffer = newBacking
self.doubleCapacity()
}
}

/// Add an element to the front of the ring buffer.
///
/// Amortized *O(1)*
public mutating func addFirst(_ value: E) {
let idx = (self.headIdx - 1) & mask
self.buffer[idx] = value
self.headIdx = idx

if self.headIdx == self.tailIdx {
// No more room left for another append so grow the buffer now.
self.doubleCapacity()
}
}

/// Double the capacity of the buffer and adjust the headIdx and tailIdx.
private mutating func doubleCapacity() {
var newBacking: ContiguousArray<E?> = []
let newCapacity = self.buffer.count << 1 // Double the storage.
precondition(newCapacity > 0, "Can't double capacity of \(self.buffer.count)")
assert(newCapacity % 2 == 0)

newBacking.reserveCapacity(newCapacity)
newBacking.append(contentsOf: self.buffer[self.headIdx..<self.buffer.count])
if self.headIdx > 0 {
newBacking.append(contentsOf: self.buffer[0..<self.headIdx])
}
newBacking.append(contentsOf: repeatElement(nil, count: newCapacity - newBacking.count))
self.tailIdx = self.buffer.count
self.headIdx = 0
self.buffer = newBacking
}

/// Remove the front element of the ring buffer.
///
/// *O(1)*
public mutating func removeFirst() -> E {
precondition(!self.isEmpty)

let value = self.buffer[self.headIdx]
self.buffer[headIdx] = nil

precondition(value != nil, "CircularBuffer is empty")

self.buffer[self.headIdx] = nil
self.headIdx = (self.headIdx + 1) & self.mask

return value!
Expand All @@ -89,11 +116,31 @@ public struct CircularBuffer<E>: CustomStringConvertible, AppendableCollection {
///
/// *O(1)*
public var first: E? {
if self.isEmpty {
return nil
} else {
return self.buffer[self.headIdx]
}
// As self.buffer is filled with `nil` this will correctly return `nil` if the CircularBuffer is empty.
return self.buffer[self.headIdx]
}

/// Remove the last element of the ring buffer.
///
/// *O(1)*
public mutating func removeLast() -> E {
let idx = (self.tailIdx - 1) & self.mask
let value = self.buffer[idx]

precondition(value != nil, "CircularBuffer is empty")

self.buffer[idx] = nil
self.tailIdx = idx

return value!
}

/// Return the last element of the ring.
///
/// *O(1)*
public var last: E? {
// As self.buffer is filled with `nil` this will correctly return `nil` if the CircularBuffer is empty.
return self.buffer[(self.tailIdx - 1) & self.mask]
}

private func bufferIndex(ofIndex index: Int) -> Int {
Expand Down
3 changes: 3 additions & 0 deletions Tests/NIOTests/CircularBufferTests+XCTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ extension CircularBufferTests {
("testSliceTheRing", testSliceTheRing),
("testCount", testCount),
("testFirst", testFirst),
("testLast", testLast),
("testOperateOnBothSides", testOperateOnBothSides),
("testAddFirstExpandBuffer", testAddFirstExpandBuffer),
]
}
}
Expand Down
43 changes: 43 additions & 0 deletions Tests/NIOTests/CircularBufferTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,47 @@ class CircularBufferTests: XCTestCase {
XCTAssertEqual(1, ring.removeFirst())
XCTAssertNil(ring.first)
}

func testLast() {
var ring = CircularBuffer<Int>(initialRingCapacity: 3)
XCTAssertNil(ring.last)
ring.addFirst(1)
XCTAssertEqual(1, ring.last)
XCTAssertEqual(1, ring.removeLast())
XCTAssertNil(ring.last)
XCTAssertEqual(0, ring.count)
XCTAssertTrue(ring.isEmpty)
}

func testOperateOnBothSides() {
var ring = CircularBuffer<Int>(initialRingCapacity: 3)
XCTAssertNil(ring.last)
ring.addFirst(1)
ring.addFirst(2)

XCTAssertEqual(1, ring.last)
XCTAssertEqual(2, ring.first)

XCTAssertEqual(1, ring.removeLast())
XCTAssertEqual(2, ring.removeFirst())

XCTAssertNil(ring.last)
XCTAssertNil(ring.first)
XCTAssertEqual(0, ring.count)
XCTAssertTrue(ring.isEmpty)
}


func testAddFirstExpandBuffer() {
var ring = CircularBuffer<Int>(initialRingCapacity: 3)
for f in 1..<1000 {
ring.addFirst(f)
XCTAssertEqual(f, ring.count)
}
for f in 1..<1000 {
XCTAssertEqual(f, ring.removeLast())
}
XCTAssertTrue(ring.isEmpty)
XCTAssertEqual(0, ring.count)
}
}

0 comments on commit be447f9

Please # to comment.