How do you make a fixed size circular buffer

Question from JohnDoe#9991

How would you have an object with 2 values that'd work like this

>(0,1)

>add(2)

(1,2)

>add(3)

(2,3)

>add(10)

(3,10)

>add(50)

(10, 50)

etc

ie replacing the oldest value

An ordered array with unlimited size and removing the oldest value when something is added would do but that'd be ugly

In general you can do that with a "circular buffer"

Here you go.

class CircularBuffer:
    def __init__(self, size=2):
        self._buffer = []
        self._size = size
        self._index = 0
    
    def add(self, item):
        # This follows your "add"
        if len(self._buffer) < self._size:
            self._buffer.append(item)
        else:
            self._buffer[self._index] = item
        
        self._index = (self._index + 1) % self._size
        
    def __len__(self):
        return len(self._buffer)
        
    def __iter__(self):
        for item in self._buffer:
            yield item
            
    def __getitem__(self, key):
        return self._buffer[key]
        
    def __repr__(self):
        return f"CircularBuffer (buffer={self._buffer}, size={self._size}, index={self._index})" 
        
        
c = CircularBuffer(size=2)
print(c)
c.add(0)
print(c)
c.add(1)
print(c)
c.add(2)
print(c)
c.add(3)
print(c)
c.add(10)
print(c)
c.add(50)
print(c)

print(c[0])
print(c[1])
print(len(c))
print(list(c))
CircularBuffer (buffer=[], size=2, index=0)
CircularBuffer (buffer=[0], size=2, index=1)
CircularBuffer (buffer=[0, 1], size=2, index=0)
CircularBuffer (buffer=[2, 1], size=2, index=1)
CircularBuffer (buffer=[2, 3], size=2, index=0)
CircularBuffer (buffer=[10, 3], size=2, index=1)
CircularBuffer (buffer=[10, 50], size=2, index=0)
10
50
2
[10, 50]

This is a mutable implementation. If you want an immutable implementation I can write that up as well, but it will take a bit more time.

The key is the modulo operator.

We track the "last inserted element" by always storing it to the right of the last element we stored, meaning if I last inserted something at index 0, the next thing i will insert at index 1.

If I reach the "end" of the list or array then I need to "circle" around back to the first element at index 0.

To keep track of this information we remember an integer representing the last index we inserted at, then each time we add something to the list we increment the integer to "move to the right" and to make it "loop around" we use the modulo operator.

"modulo" just means the remainder you would get when dividing two numbers. For example

15 % 4 is 3

This is because we can fit three 4s into 15 (4 * 3 = 12), but we don't have enough to fit another four so we have a "remainder" of 3 (15 - 12 = 3)

When you repeatedly do n = (n + 1) % some_limit then the value of n will go up to 1 minus the limit before looping back around to zero.

Try these on paper to get a better idea of why

0 % 3 = ?
1 % 3 = ?
2 % 3 = ?
3 % 3 = ?
4 % 3 = ?
5 % 3 = ?
6 % 3 = ?
7 % 3 = ?

For your specific case of just 2 items the modulo stuff is kinda overkill (from a cognitive load standpoint) so just do whatever solves your problem the simplest.

But if you wanted an object that holds max n elements that works like you described (where old elements are replaced in place) this is probably the cleanest way to do that.

If the "where" in the list doesn't matter then maybe consider using a deque with a size cap


<- Index