Skip to content

Commit

Permalink
feature: circular buffer (#28)
Browse files Browse the repository at this point in the history
* build(scripts): update package script

* feat(circular buffer): implement circular buffer

* test(circular buffer): add test for circular buffer

* docs(readme): add documentation for circular buffer

* refactor(tests): update incorrect var name in deque test
  • Loading branch information
joshuagraber committed Oct 11, 2023
1 parent 6252a9c commit 36e20a5
Show file tree
Hide file tree
Showing 6 changed files with 287 additions and 11 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ _Note: this is primarily for my own learning. I'll aim to keep test coverage at
| [Stack][Stack] | Sequence | Uses a `LinkedList` with the tail as the head of the [Stack][StackWiki] |
| [Queue][Queue] | Sequence | Uses a `LinkedList` with the head as the front of the [Queue][QueueWiki] |
| [Deque (Double-ended Queue)][Deque] | Sequence | Uses a `LinkedList` with the head as the front of the [Deque][QueueWiki] |
| [Circular Buffer][CircularBuffer] | Sequence | Uses JS array as case of [Buffer][BufferWiki] |

## Algorithms

Expand All @@ -30,10 +31,12 @@ _Note: this is primarily for my own learning. I'll aim to keep test coverage at
[Stack]: ./src/data-structures/sequences/stack/index.ts
[Queue]: ./src/data-structures/sequences/queues/queue/index.ts
[Deque]: ./src/data-structures/sequences/queues/deque/index.ts
[CircularBuffer]: ./src/data-structures/sequences/circular-buffer/index.ts

<!-- Algorithms -->
<!-- Resources -->

[DoublyLinkedListWiki]: https://en.wikipedia.org/wiki/Doubly_linked_list;
[StackWiki]: https://en.wikipedia.org/wiki/Stack_(abstract_data_type)
[QueueWiki]: https://en.wikipedia.org/wiki/Queue_(abstract_data_type)
[BufferWiki]: https://en.wikipedia.org/wiki/Circular_buffer
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,7 @@
},
"scripts": {
"build": "run-s clean:build && tsc -p tsconfig.esm.json && tsc -p tsconfig.cjs.json && npm run build:package",
"build:package-file": "node ./scripts/setPackageType.js",
"build:package": "sh ./bin/sh/set_package_type.sh",
"build:package": "sh ./scripts/set_package_type.sh",
"clean": "run-p clean:*",
"clean:build": "rimraf dist",
"clean:test": "rimraf coverage",
Expand Down
2 changes: 1 addition & 1 deletion bin/sh/set_package_type.sh → scripts/set_package_type.sh
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/bin/bash
#!/bin/sh
jq -n '{type: "module"}' > dist/esm/package.json
jq -n '{type: "commonjs"}' > dist/cjs/package.json
119 changes: 119 additions & 0 deletions src/data-structures/sequences/circular-buffer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Imports - util
import { equals, type TypedEqualityFunction } from '../../utils.js';

class CircularBuffer<T> {
private list: T[];
private bufferSize: number;
private capacity: number;

private head: number;
private tail: number;

private equalsImpl: TypedEqualityFunction<T>;

constructor(capacity: number, equalsFunction?: TypedEqualityFunction<T>) {
this.list = new Array(capacity);
this.bufferSize = 0;
this.capacity = capacity;

this.head = 0;
this.tail = 0;

this.equalsImpl = equalsFunction ?? equals;
}

// HELPERS
/**
* Returns size of circular buffer - O(1)
*/
size(): number {
return this.bufferSize;
}

/**
* Returns true if buffer is empty, false otherwise - O(1)
*/
isEmpty(): boolean {
return this.size() === 0;
}

// INSERT
/**
* Enqueues element into queue - O(1)
* @param {T} element - element to be enqueued
*/
enqueue(element: T): void {
this.list[this.tail] = element;

const isOverwritten = this.bufferSize !== 0 && this.tail === this.head;
if (isOverwritten) {
this.head = (this.head + 1) % this.capacity;
}

this.tail = (this.tail + 1) % this.capacity;

this.bufferSize += 1;
}

// ACCESS
/**
* Peeks the element at the front of the buffer - O(1)
* @returns {T} - Frontmost element
*/
peekFront(): T | null {
if (this.isEmpty()) return null;

return this.list[this.head];
}

/**
* Peeks the element at the back of the buffer - O(1)
* @returns {T} - Backmost element
*/
peekBack(): T | null {
if (this.isEmpty()) return null;

let i = this.tail - 1;
if (i < 0) i = this.capacity - 1;

return this.list[i];
}

// SEARCH
/**
* Checks if element is in buffer - O(n)
* @param {T} element - element to search for
* @returns {boolean}
*/
contains(element: T): boolean {
return this.list.some((val: T) => {
return this.equalsImpl(val, element);
});
}

// DELETE
/**
* Dequeues element from queue - O(1)
* @returns {T}
*/
dequeue(): T | null {
if (this.isEmpty()) return null;

const removedVal = this.list[this.head];
this.head = (this.head + 1) % this.capacity;

this.bufferSize -= 1;

return removedVal;
}

/**
* Deletes all elements in buffer - O(capacity)
*/
clear(): void {
this.list = new Array(this.capacity);
this.bufferSize = 0;
}
}

export { CircularBuffer };
155 changes: 155 additions & 0 deletions test/data-structures/sequences/circular-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { CircularBuffer } from '../../../src/data-structures/sequences/circular-buffer/index.js';

describe('Circular Buffer', () => {
let buffer: CircularBuffer<number>;

beforeEach(() => {
buffer = new CircularBuffer(4);
});

describe('empty', () => {
it('returns null when dequeue is called on empty buffer', () => {
expect(buffer.dequeue()).toBe(null);
});

it('returns null when peek() is called on empty buffer', () => {
expect(buffer.peekFront()).toBe(null);
expect(buffer.peekBack()).toBe(null);
});

it('is empty', () => {
expect(buffer.isEmpty()).toBe(true);
});
});

describe('insert and delete', () => {
it('enqueues', () => {
buffer.enqueue(1);
expect(buffer.size()).toBe(1);

buffer.enqueue(2);
expect(buffer.size()).toBe(2);

buffer.enqueue(3);
expect(buffer.size()).toBe(3);
});

it('dequeues', () => {
buffer.enqueue(1);
buffer.enqueue(2);
buffer.enqueue(3);

expect(buffer.size()).toBe(3);

buffer.dequeue();
expect(buffer.size()).toBe(2);

buffer.dequeue();
expect(buffer.size()).toBe(1);

buffer.dequeue();
expect(buffer.size()).toBe(0);
});

it('overwrites nodes', () => {
buffer.enqueue(1);
buffer.enqueue(2);
buffer.enqueue(3);
buffer.enqueue(4);
buffer.enqueue(5);
expect(buffer.contains(1)).toBe(false);
expect(buffer.peekFront()).toBe(2);
expect(buffer.peekBack()).toBe(5);
buffer.enqueue(6);
expect(buffer.contains(2)).toBe(false);
expect(buffer.peekFront()).toBe(3);
expect(buffer.peekBack()).toBe(6);
});

it('clears the buffer', () => {
buffer.enqueue(1);
buffer.enqueue(2);
buffer.enqueue(3);
buffer.enqueue(4);
buffer.clear();
expect(buffer.isEmpty()).toBe(true);

buffer.enqueue(1);
buffer.clear();
expect(buffer.isEmpty()).toBe(true);

buffer.clear();
expect(buffer.isEmpty()).toBe(true);
});
});

describe('Accessing', () => {
it('peeks front', () => {
buffer.enqueue(1);
expect(buffer.peekFront()).toBe(1);
expect(buffer.peekBack()).toBe(1);

buffer.enqueue(2);
expect(buffer.peekFront()).toBe(1);
expect(buffer.peekBack()).toBe(2);
});

it('peeks back', () => {
buffer.enqueue(1);
buffer.enqueue(2);
buffer.enqueue(3);
buffer.enqueue(4);
expect(buffer.peekBack()).toBe(4);
});
});

describe('searching', () => {
it('finds out if buffer contains element', () => {
expect(buffer.contains(1)).toBe(false);
buffer.enqueue(1);
buffer.enqueue(2);
buffer.enqueue(3);

expect(buffer.contains(1)).toBe(true);
expect(buffer.contains(3)).toBe(true);
expect(buffer.contains(8)).toBe(false);
});
});
});

describe('Circular Buffer - complex object', () => {
class Car {
id: number;
topSpeed: number;
engineSize: number;

constructor(id: number) {
this.id = id;
this.topSpeed = 100;
this.engineSize = 100;
}
}

let buffer: CircularBuffer<Car>;

beforeAll(() => {
const ferrari = new Car(123);
const peugeot = new Car(456);
const honda = new Car(789);

buffer = new CircularBuffer(3);

buffer.enqueue(ferrari);
buffer.enqueue(peugeot);
buffer.enqueue(honda);
});

it('checks if queue contains vehicle', () => {
const ferrari = new Car(123);
const honda = new Car(789);

expect(buffer.contains(ferrari)).toBe(true);
expect(buffer.contains(honda)).toBe(true);
expect(buffer.contains(new Car(246))).toBe(false);
});
});
16 changes: 8 additions & 8 deletions test/data-structures/sequences/deque.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,26 +159,26 @@ describe('Queue - complex object', () => {
}
}

let queue: Deque<Car>;
let deque: Deque<Car>;

beforeAll(() => {
const ferrari = new Car(123);
const peugeot = new Car(456);
const honda = new Car(789);

queue = new Deque();
deque = new Deque();

queue.push(ferrari);
queue.push(peugeot);
queue.push(honda);
deque.push(ferrari);
deque.push(peugeot);
deque.push(honda);
});

it('checks if queue contains hero', () => {
const ferrari = new Car(123);
const peugeot = new Car(789);

expect(queue.contains(ferrari)).toBe(true);
expect(queue.contains(peugeot)).toBe(true);
expect(queue.contains(new Car(246))).toBe(false);
expect(deque.contains(ferrari)).toBe(true);
expect(deque.contains(peugeot)).toBe(true);
expect(deque.contains(new Car(246))).toBe(false);
});
});

0 comments on commit 36e20a5

Please sign in to comment.