Skip to content

Commit

Permalink
feat: add doubly-linked list implementation (#970)
Browse files Browse the repository at this point in the history
* feat: add doubly-linked list implementation

Add doubly-linked list implemented as a circular list. The API of
the `List` class is almost identical to the `Deque` API.

A `List` uses more memory than a `Queue`, but a `List` takes O(1)
time to do insertions and removals anywhere in the list, whereas a
`Queue` only takes O(1) time to do insertions and removals at the
front and back of the queue, and O(n) everywhere else.

Also add a testsuite for the `List` class.

* feat: adjust push() and unshift() to return the added list node

* feat: add LRU and MRU cache implementations

* feat: add remove() to cache classes

Add remove(value) to remove a value from the cache independently
from the eviction policy.
  • Loading branch information
johnnylam88 authored Aug 14, 2021
1 parent 09df20c commit e35f10b
Show file tree
Hide file tree
Showing 4 changed files with 808 additions and 0 deletions.
100 changes: 100 additions & 0 deletions src/tools/cache.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { expect, test } from "@jest/globals";
import { LRUCache, MRUCache } from "./cache";

test("new LRU cache is not full", () => {
const cache = new LRUCache<number>(3);
expect(cache.isFull()).toBe(false);
});

test("put into empty LRU cache", () => {
const cache = new LRUCache<number>(3);
const value = cache.put(10);
expect(value).toBe(undefined);
expect(cache.oldest()).toBe(10);
expect(cache.oldest()).toBe(cache.newest());
});

test("remove from empty LRU cache", () => {
const cache = new LRUCache<number>(3);
cache.remove(10);
expect(cache.asArray()).toEqual({});
});

test("put into non-empty LRU cache", () => {
const cache = new LRUCache<number>(3);
cache.put(10);
cache.put(20);
const value = cache.put(30);
expect(value).toBe(undefined);
expect(cache.oldest()).toBe(10);
expect(cache.newest()).toBe(30);
expect(cache.asArray()).toEqual({ 1: 10, 2: 20, 3: 30 });
});

test("remove from non-empty LRU cache", () => {
const cache = new LRUCache<number>(3);
cache.put(10);
cache.put(20);
cache.remove(10);
expect(cache.oldest()).toBe(20);
expect(cache.asArray()).toEqual({ 1: 20 });
});

test("put into full LRU cache", () => {
const cache = new LRUCache<number>(3);
cache.put(10);
cache.put(20);
cache.put(30);
const value = cache.put(40);
expect(value).toBe(10);
expect(cache.asArray()).toEqual({ 1: 20, 2: 30, 3: 40 });
});

test("new MRU cache is not full", () => {
const cache = new MRUCache<number>(3);
expect(cache.isFull()).toBe(false);
});

test("remove from empty MRU uache", () => {
const cache = new MRUCache<number>(3);
cache.remove(10);
expect(cache.asArray()).toEqual({});
});

test("put into empty MRU cache", () => {
const cache = new MRUCache<number>(3);
const value = cache.put(10);
expect(value).toBe(undefined);
expect(cache.oldest()).toBe(10);
expect(cache.oldest()).toBe(cache.newest());
});

test("put into non-empty MRU cache", () => {
const cache = new MRUCache<number>(3);
cache.put(10);
cache.put(20);
const value = cache.put(30);
expect(value).toBe(undefined);
expect(cache.oldest()).toBe(10);
expect(cache.newest()).toBe(30);
expect(cache.asArray()).toEqual({ 1: 10, 2: 20, 3: 30 });
});

test("remove from non-empty MRU cache", () => {
const cache = new MRUCache<number>(3);
cache.put(10);
cache.put(20);
cache.remove(10);
expect(cache.oldest()).toBe(20);
expect(cache.asArray()).toEqual({ 1: 20 });
});

test("put into full MRU cache", () => {
const cache = new MRUCache<number>(3);
cache.put(10);
cache.put(20);
cache.put(30);
const value = cache.put(40);
expect(value).toBe(30);
expect(cache.asArray()).toEqual({ 1: 10, 2: 20, 3: 40 });
});
67 changes: 67 additions & 0 deletions src/tools/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { LuaObj } from "@wowts/lua";
import { List, ListNode } from "./list";

class Cache<T> {
list: List<T>;
nodeByValue: LuaObj<ListNode<T>> = {};

constructor(public size: number) {
this.list = new List<T>();
}

isFull() {
return this.list.length >= this.size;
}

newest() {
return this.list.back();
}

oldest() {
return this.list.front();
}

asArray() {
return this.list.asArray();
}

evict() {
return this.list.shift();
}

put(value: T) {
this.remove(value);
const evicted = (this.isFull() && this.evict()) || undefined;
/* Pretend to cast to string to satisfy TypeScript.
* Lua tables can accept anything as a valid key.
*/
const key = value as unknown as string;
this.nodeByValue[key] = this.list.push(value);
return evicted;
}

remove(value: T) {
/* Pretend to cast to string to satisfy TypeScript.
* Lua tables can accept anything as a valid key.
*/
const key = value as unknown as string;
const node = this.nodeByValue[key];
if (node) {
this.list.remove(node);
}
}
}

export class LRUCache<T> extends Cache<T> {
evict() {
// LRU policy evicts the oldest item
return this.list.shift();
}
}

export class MRUCache<T> extends Cache<T> {
evict() {
// MRU policy evicts the newest item
return this.list.pop();
}
}
Loading

0 comments on commit e35f10b

Please sign in to comment.