From 4fbc30a264f88bbd1bb4a4af76bcb8eb3fe40d51 Mon Sep 17 00:00:00 2001 From: Brendan Chen Date: Wed, 2 Jul 2025 18:34:55 -0400 Subject: [PATCH] Add CircularQueue.ts and test file --- src/types/CircularQueue.ts | 97 ++++++++++++++++- test/types/CircularQueue.test.ts | 176 +++++++++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 5 deletions(-) create mode 100644 test/types/CircularQueue.test.ts diff --git a/src/types/CircularQueue.ts b/src/types/CircularQueue.ts index 57298a0..9ef6ca0 100644 --- a/src/types/CircularQueue.ts +++ b/src/types/CircularQueue.ts @@ -2,6 +2,8 @@ export class CircularQueue { private startIndex: number; private endIndex: number; private _data: T[]; + private _size: number; + private _capacity: number; constructor( size: number, @@ -10,18 +12,103 @@ export class CircularQueue { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Indexed_collections#sparse_arrays this._data = new Array(size); this.startIndex = 0; - this.endIndex = size - 1; + this.endIndex = 0; + this._size = 0; + this._capacity = size; } + size = (): number => this._size; + + get = (index: number): T | undefined => { + if (index < 0 || index >= this._size) { + return undefined; + } + const actualIndex = (this.startIndex + index) % this._capacity; + return this._data[actualIndex]; + }; + appendWithSorting = ( data: T, - sortingCallback: ((a: T, b: T) => number) | undefined + sortingCallback: (a: T, b: T) => number ) => { - // In case something is added that's not sorted, the sortingCallback - // will be used to sort + if (this._size === 0) { + this._data[this.startIndex] = data; + this._size = 1; + this.endIndex = this.startIndex; + return; + } + + if (this._size < this._capacity) { + this.endIndex = (this.endIndex + 1) % this._capacity; + this._data[this.endIndex] = data; + this._size++; + } else { + this.startIndex = (this.startIndex + 1) % this._capacity; + this.endIndex = (this.endIndex + 1) % this._capacity; + this._data[this.endIndex] = data; + } + + this.sortData(sortingCallback); } - popFront = (data: T) => { + popFront = () => { + if (this._size === 0) { + return; + } + this._data[this.startIndex] = undefined as any; + if (this._size === 1) { + this._size = 0; + this.startIndex = 0; + this.endIndex = 0; + } else { + this.startIndex = (this.startIndex + 1) % this._capacity; + this._size--; + } } + + binarySearch = ( + searchKey: K, + keyExtractor: (item: T) => K + ): T | undefined => { + if (this._size === 0) { + return undefined; + } + + let left = 0; + let right = this._size - 1; + + while (left <= right) { + const mid = Math.floor((left + right) / 2); + const midItem = this.get(mid)!; + const midKey = keyExtractor(midItem); + + if (midKey === searchKey) { + return midItem; + } else if (midKey < searchKey) { + left = mid + 1; + } else { + right = mid - 1; + } + } + + return undefined; + } + + private sortData = (sortingCallback: (a: T, b: T) => number) => { + const items: T[] = []; + for (let i = 0; i < this._size; i++) { + const item = this.get(i); + if (item !== undefined) { + items.push(item); + } + } + + items.sort(sortingCallback); + + for (let i = 0; i < items.length; i++) { + const actualIndex = (this.startIndex + i) % this._capacity; + this._data[actualIndex] = items[i]; + } + }; } diff --git a/test/types/CircularQueue.test.ts b/test/types/CircularQueue.test.ts new file mode 100644 index 0000000..60ac085 --- /dev/null +++ b/test/types/CircularQueue.test.ts @@ -0,0 +1,176 @@ +import { describe, expect, it } from "@jest/globals"; +import { CircularQueue } from "../../src/types/CircularQueue"; + +interface TestItem { + id: number; + value: string; +} + +describe("CircularQueue", () => { + const testItems = { + first: { id: 1, value: "first" }, + second: { id: 2, value: "second" }, + third: { id: 3, value: "third" }, + fourth: { id: 4, value: "fourth" }, + test: { id: 1, value: "test" }, + apple: { id: 1, value: "apple" }, + banana: { id: 2, value: "banana" }, + cherry: { id: 3, value: "cherry" }, + grape: { id: 5, value: "grape" }, + orange: { id: 7, value: "orange" }, + a: { id: 1, value: "a" }, + b: { id: 2, value: "b" }, + c: { id: 3, value: "c" }, + d: { id: 4, value: "d" } + }; + + const sortingCallbacks = { + byId: (a: TestItem, b: TestItem) => a.id - b.id, + byValue: (a: TestItem, b: TestItem) => a.value.localeCompare(b.value) + }; + + const keyExtractors = { + id: (item: TestItem) => item.id, + value: (item: TestItem) => item.value + }; + + const createQueueWithItems = (size: number, items: TestItem[], sortingCallback: (a: TestItem, b: TestItem) => number) => { + const queue = new CircularQueue(size); + items.forEach(item => queue.appendWithSorting(item, sortingCallback)); + return queue; + }; + + describe("constructor", () => { + it("creates queue with specified size", () => { + const queue = new CircularQueue(5); + expect(queue).toBeDefined(); + }); + }); + + describe("appendWithSorting", () => { + it("adds items to the queue with sorting callback", () => { + const queue = createQueueWithItems(3, [testItems.third, testItems.first, testItems.second], sortingCallbacks.byId); + + expect(queue.size()).toBe(3); + expect(queue.get(0)).toEqual(testItems.first); + expect(queue.get(1)).toEqual(testItems.second); + expect(queue.get(2)).toEqual(testItems.third); + }); + + it("overwrites oldest items when queue is full", () => { + const queue = createQueueWithItems(2, [testItems.first, testItems.second, testItems.third], sortingCallbacks.byId); + + expect(queue.size()).toBe(2); + }); + + it("handles appending to empty queue", () => { + const queue = createQueueWithItems(3, [testItems.test], sortingCallbacks.byId); + + expect(queue.size()).toBe(1); + expect(queue.get(0)).toEqual(testItems.test); + }); + }); + + describe("popFront", () => { + it("removes the oldest item from queue", () => { + const queue = createQueueWithItems(3, [testItems.first, testItems.second], sortingCallbacks.byId); + + expect(queue.size()).toBe(2); + queue.popFront(); + expect(queue.size()).toBe(1); + expect(queue.get(0)).toEqual(testItems.second); + }); + + it("handles popping from empty queue", () => { + const queue = new CircularQueue(3); + + expect(() => queue.popFront()).not.toThrow(); + expect(queue.size()).toBe(0); + }); + + it("handles popping until empty", () => { + const queue = createQueueWithItems(2, [testItems.first, testItems.second], sortingCallbacks.byId); + + queue.popFront(); + expect(queue.size()).toBe(1); + queue.popFront(); + expect(queue.size()).toBe(0); + queue.popFront(); + expect(queue.size()).toBe(0); + }); + }); + + describe("binarySearch", () => { + it("finds item using key extractor function", () => { + const queue = createQueueWithItems(5, [testItems.apple, testItems.cherry, testItems.grape, testItems.orange], sortingCallbacks.byId); + + const result = queue.binarySearch(5, keyExtractors.id); + + expect(result).toEqual(testItems.grape); + }); + + it("returns undefined when item not found", () => { + const queue = createQueueWithItems(5, [testItems.apple, testItems.cherry, testItems.orange], sortingCallbacks.byId); + + const result = queue.binarySearch(5, keyExtractors.id); + + expect(result).toBeUndefined(); + }); + + it("finds first item", () => { + const queue = createQueueWithItems(5, [testItems.apple, testItems.cherry, testItems.orange], sortingCallbacks.byId); + + const result = queue.binarySearch(1, keyExtractors.id); + + expect(result).toEqual(testItems.apple); + }); + + it("finds last item", () => { + const queue = createQueueWithItems(5, [testItems.apple, testItems.cherry, testItems.orange], sortingCallbacks.byId); + + const result = queue.binarySearch(7, keyExtractors.id); + + expect(result).toEqual(testItems.orange); + }); + + it("returns undefined for empty queue", () => { + const queue = new CircularQueue(5); + const result = queue.binarySearch(1, keyExtractors.id); + + expect(result).toBeUndefined(); + }); + + it("works with string keys", () => { + const queue = createQueueWithItems(5, [testItems.apple, testItems.banana, testItems.cherry], sortingCallbacks.byValue); + + const result = queue.binarySearch("banana", keyExtractors.value); + + expect(result).toEqual(testItems.banana); + }); + + it("maintains sorted order assumption", () => { + const queue = createQueueWithItems(5, [testItems.d, testItems.a, testItems.c, testItems.b], sortingCallbacks.byValue); + + expect(queue.binarySearch("a", keyExtractors.value)).toEqual(testItems.a); + expect(queue.binarySearch("b", keyExtractors.value)).toEqual(testItems.b); + expect(queue.binarySearch("c", keyExtractors.value)).toEqual(testItems.c); + expect(queue.binarySearch("d", keyExtractors.value)).toEqual(testItems.d); + expect(queue.binarySearch("z", keyExtractors.value)).toBeUndefined(); + }); + }); + + describe("integration", () => { + it("handles appendWithSorting, popFront, and binarySearch together", () => { + const queue = createQueueWithItems(3, [testItems.third, testItems.first, testItems.second], sortingCallbacks.byId); + + expect(queue.binarySearch(2, keyExtractors.id)).toEqual(testItems.second); + + queue.popFront(); + expect(queue.binarySearch(1, keyExtractors.id)).toBeUndefined(); + expect(queue.binarySearch(2, keyExtractors.id)).toEqual(testItems.second); + + queue.appendWithSorting(testItems.fourth, sortingCallbacks.byId); + expect(queue.binarySearch(4, keyExtractors.id)).toEqual(testItems.fourth); + }); + }); +}); \ No newline at end of file