// Copyright 2021 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import Foundation /// A generic circular queue structure. struct RingBuffer: Sequence { /// An array of heartbeats treated as a circular queue and intialized with a fixed capacity. private var circularQueue: [Element?] /// The current "tail" and insert point for the `circularQueue`. private var tailIndex: Array.Index /// Error types for `RingBuffer` operations. enum Error: LocalizedError { case outOfBoundsPush(pushIndex: Array.Index, endIndex: Array.Index) var errorDescription: String { switch self { case let .outOfBoundsPush(pushIndex, endIndex): return "Out-of-bounds push at index \(pushIndex) to ring buffer with" + "end index of \(endIndex)." } } } /// Designated initializer. /// - Parameter capacity: An `Int` representing the capacity. init(capacity: Int) { circularQueue = Array(repeating: nil, count: capacity) tailIndex = circularQueue.startIndex } /// Pushes an element to the back of the buffer, returning the element (`Element?`) that was /// overwritten. /// - Parameter element: The element to push to the back of the buffer. /// - Returns: The element that was overwritten or `nil` if nothing was overwritten. /// - Complexity: O(1) @discardableResult mutating func push(_ element: Element) throws -> Element? { guard circularQueue.count > 0 else { // Do not push if `circularQueue` is a fixed empty array. return nil } guard circularQueue.indices.contains(tailIndex) else { // We have somehow entered an invalid state (#10025). throw Self.Error.outOfBoundsPush( pushIndex: tailIndex, endIndex: circularQueue.endIndex ) } let replaced = circularQueue[tailIndex] circularQueue[tailIndex] = element // Increment index, wrapping around to the start if needed. tailIndex += 1 if tailIndex >= circularQueue.endIndex { tailIndex = circularQueue.startIndex } return replaced } /// Pops an element from the back of the buffer, returning the element (`Element?`) that was /// popped. /// - Returns: The element that was popped or `nil` if there was no element to pop. /// - Complexity: O(1) @discardableResult mutating func pop() -> Element? { guard circularQueue.count > 0 else { // Do not pop if `circularQueue` is a fixed empty array. return nil } // Decrement index, wrapping around to the back if needed. tailIndex -= 1 if tailIndex < circularQueue.startIndex { tailIndex = circularQueue.endIndex - 1 } guard let popped = circularQueue[tailIndex] else { return nil // There is no element to pop. } circularQueue[tailIndex] = nil return popped } func makeIterator() -> IndexingIterator<[Element]> { circularQueue .compactMap { $0 } // Remove `nil` elements. .makeIterator() } } // MARK: - Codable extension RingBuffer: Codable where Element: Codable {}