Skip to content

Commit

Permalink
fix(LRUCacheWithDelete): addressed issues in PR Yomguithereal#162
Browse files Browse the repository at this point in the history
  • Loading branch information
mrflip committed Nov 17, 2021
1 parent 5698a94 commit 9bfef48
Show file tree
Hide file tree
Showing 5 changed files with 117 additions and 62 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {default as InvertedIndex} from './inverted-index';
export {default as KDTree} from './kd-tree';
export {default as LinkedList} from './linked-list';
export {default as LRUCache} from './lru-cache';
export {default as LRUCacheWithDelete} from './lru-cache-with-delete';
export {default as LRUMap} from './lru-map';
export {default as MultiMap} from './multi-map';
export {default as MultiSet} from './multi-set';
Expand Down
41 changes: 4 additions & 37 deletions lru-cache-with-delete.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,10 @@
* Mnemonist LRUCacheWithDelete Typings
* ===========================
*/
import {IArrayLikeConstructor} from './utils/types';
import LRUCache from './lru-cache';

export default class LRUCacheWithDelete<K, V> implements Iterable<[K, V]> {
export default class LRUCacheWithDelete<K, V> extends LRUCache<K, V> {

// Members
capacity: number;
size: number;
delete(key: K): V | undefined;

// Constructor
constructor(capacity: number);
constructor(KeyArrayClass: IArrayLikeConstructor, ValueArrayClass: IArrayLikeConstructor, capacity: number);

// Methods
clear(): void;
set(key: K, value: V): this;
setpop(key: K, value: V): {evicted: boolean, key: K, value: V};
get(key: K): V | undefined;
peek(key: K): V | undefined;
remove(key: K): this;
has(key: K): boolean;
forEach(callback: (value: V, key: K, cache: this) => void, scope?: any): void;
keys(): IterableIterator<K>;
values(): IterableIterator<V>;
entries(): IterableIterator<[K, V]>;
[Symbol.iterator](): IterableIterator<[K, V]>;
inspect(): any;

// Statics
static from<I, J>(
iterable: Iterable<[I, J]> | {[key: string]: J},
KeyArrayClass: IArrayLikeConstructor,
ValueArrayClass: IArrayLikeConstructor,
capacity?: number
): LRUCacheWithDelete<I, J>;

static from<I, J>(
iterable: Iterable<[I, J]> | {[key: string]: J},
capacity?: number
): LRUCacheWithDelete<I, J>;
}
}
32 changes: 26 additions & 6 deletions lru-cache-with-delete.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ var LRUCache = require('./lru-cache.js'),
typed = require('./utils/typed-arrays.js'),
iterables = require('./utils/iterables.js');

// The only complication with deleting items is that the LRU's
// performance depends on having a fixed-size list of pointers; the
// doubly-linked-list is happy to expand and contract.
//
// On delete, we record the position of the former item's pointer in a
// list of "holes" in the pointer array. On insert, if there is a hole
// the new pointer slots in to fill the hole; otherwise, it is
// appended as usual. (Note: we are only talking here about the
// internal pointer list. set'ing or get'ing an item promotes it
// to the top of the LRU ranking no matter what came before)

function LRUCacheWithDelete(Keys, Values, capacity) {
if (arguments.length < 2) {
LRUCache.call(this, Keys);
Expand Down Expand Up @@ -73,9 +84,11 @@ LRUCacheWithDelete.prototype.setpop = function(key, value) {
// The cache is not yet full
if (this.size < this.capacity) {
if (this.deletedSize > 0) {
// If there is a "hole" in the pointer list, reuse it
pointer = this.deleted[--this.deletedSize];
}
else {
// otherwise append to the pointer list
pointer = this.size;
}
this.size++;
Expand Down Expand Up @@ -115,17 +128,23 @@ LRUCacheWithDelete.prototype.setpop = function(key, value) {
* @param {any} key - Key.
* @return {undefined}
*/
LRUCacheWithDelete.prototype.delete = function(key) {
LRUCacheWithDelete.prototype.delete = function(key) {

var pointer = this.items[key];

if (typeof pointer === 'undefined') {
return;
return undefined;
}

if (this.head === pointer && this.tail === pointer) {
this.clear();
return;
const dead = this.V[pointer];
delete this.items[key];

if (this.size === 1) {
this.size = 0;
this.head = 0;
this.tail = 0;
this.deletedSize = 0;
return dead;
}

var previous = this.backward[pointer],
Expand All @@ -141,9 +160,10 @@ LRUCacheWithDelete.prototype.setpop = function(key, value) {
this.forward[previous] = next;
this.backward[next] = previous;

delete this.items[key];
this.size--;
this.deleted[this.deletedSize++] = pointer;

return dead;
};

/**
Expand Down
97 changes: 78 additions & 19 deletions test/lru-cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -222,45 +222,104 @@ function makeTests(Cache, name) {
});

if (name === 'LRUCacheWithDelete') {

it('should be possible to delete keys from a LRU cache.', function() {
var cache = new Cache(3);

assert.strictEqual(cache.capacity, 3);

cache.set('one', 1);
cache.set('two', 2);
cache.set('three', 3);
cache.set('one', 'uno');
cache.set('two', 'dos');
cache.set('three', 'tres');

assert.deepStrictEqual(Array.from(cache.entries()), [['three', 'tres'], ['two', 'dos'], ['one', 'uno']]);

assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['two', 2], ['one', 1]]);
let dead;

// Delete head
cache.delete('three');
assert.deepStrictEqual(Array.from(cache.entries()), [['two', 2], ['one', 1]]);
dead = cache.delete('three');
assert.deepStrictEqual(Array.from(cache.entries()), [['two', 'dos'], ['one', 'uno']]);
assert.deepStrictEqual(dead, 'tres');

cache.set('three', 'trois');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 'trois'], ['two', 'dos'], ['one', 'uno']]);

cache.set('three', 3);
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['two', 2], ['one', 1]]);
// Delete node which is neither head or tail
cache.delete('two');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3], ['one', 1]]);
dead = cache.delete('two');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 'trois'], ['one', 'uno']]);
assert.deepStrictEqual(dead, 'dos');

// Delete tail
cache.delete('one');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 3]]);
dead = cache.delete('one');
assert.deepStrictEqual(Array.from(cache.entries()), [['three', 'trois']]);
assert.deepStrictEqual(dead, 'uno');

// Delete the only key
cache.delete('three');
dead = cache.delete('three');
assert.deepStrictEqual(dead, 'trois');
assert.strictEqual(cache.capacity, 3);
assert.strictEqual(cache.size, 0);
assert.strictEqual(cache.head, 0);
assert.strictEqual(cache.tail, 0);

cache.set('one', 1);
cache.set('two', 2);
cache.set('three', 3);
cache.set('two', 6);
cache.set('four', 4);
cache.set('one', 'uno');
cache.set('two', 'dos');
cache.set('three', 'tres');
cache.set('two', 'deux');
cache.set('four', 'cuatro');

assert.deepStrictEqual(Array.from(cache.entries()), [['four', 'cuatro'], ['two', 'deux'], ['three', 'tres']]);
});

it('maintains LRU order regardless of deletions', function() {
var cache = new Cache(5);
let dead;

cache.set('one', 'uno'); cache.set('two', 'dos'); cache.set('three', 'tres');
cache.set('four', 'cuatro'); cache.set('five', 'cinco');
cache.get('one'); // order is [ one // five four three two ] <-- two will be removed
cache.set('six', 'seis');
assert.deepStrictEqual(Array.from(cache.entries()),
[['six', 'seis'], ['one', 'uno'], ['five', 'cinco'], ['four', 'cuatro'], ['three', 'tres']]);
dead = cache.delete('five');
assert.deepStrictEqual(dead, 'cinco');
cache.set('one', 'rast');
assert.deepStrictEqual(Array.from(cache.entries()),
[['one', 'rast'], ['six', 'seis'], ['four', 'cuatro'], ['three', 'tres']]);
cache.set('seven', 'siete');
assert.deepStrictEqual(Array.from(cache.entries()),
[['seven', 'siete'], ['one', 'rast'], ['six', 'seis'], ['four', 'cuatro'], ['three', 'tres']]);
cache.set('eight', 'ocho');
assert.deepStrictEqual(Array.from(cache.entries()),
[['eight', 'ocho'], ['seven', 'siete'], ['one', 'rast'], ['six', 'seis'], ['four', 'cuatro']]);
dead = cache.delete('five');
assert.deepStrictEqual(dead, undefined);
assert.deepStrictEqual(Array.from(cache.entries()),
[['eight', 'ocho'], ['seven', 'siete'], ['one', 'rast'], ['six', 'seis'], ['four', 'cuatro']]);
});


it('enjoys a healthy workout', function() {
var cache = new Cache(4);
let dead;
cache.set(0, 'cero'); cache.set(1, 'uno'); cache.set(2, 'dos'); cache.delete(1);
cache.set(3, 'tres'); cache.set(4, 'cuatro'); cache.get(2)
assert.deepStrictEqual(Array.from(cache.entries()), [[2, 'dos'], [4, 'cuatro'], [3, 'tres'], [0, 'cero']]);

cache.set(5, 'cinco'); cache.set(6, 'seis'); cache.delete(1); cache.delete(2); cache.set(5, 'cinq');
assert.deepStrictEqual(Array.from(cache.entries()), [[5, 'cinq'], [6, 'seis'], [4, 'cuatro']]);

cache.set(7, 'siete'); cache.set(8, 'ocho'); cache.set(9, 'nueve'); cache.delete(8) ; cache.set(10, 'diez');
assert.deepStrictEqual(Array.from(cache.entries()), [[10, 'diez'], [9, 'nueve'], [7, 'siete'], [5, 'cinq']]);

cache.set(7, 'sept'); cache.get(5); cache.set(8, 'huit'); cache.set(9, 'neuf'); cache.set(10, 'dix');
assert.deepStrictEqual(Array.from(cache.entries()), [[10, 'dix'], [9, 'neuf'], [8, 'huit'], [5, 'cinq']]);

cache.get(8); cache.delete(10); cache.set(1, 'rast'); cache.set(2, 'deux'); cache.get(8);
assert.deepStrictEqual(Array.from(cache.entries()), [[8, 'huit'], [2, 'deux'], [1, 'rast'], [9, 'neuf']]);

assert.deepStrictEqual(Array.from(cache.entries()), [['four', 4], ['two', 6], ['three', 3]]);
cache.delete(2); cache.delete(9); cache.get(1); cache.set(2, 'dva'); cache.get(1); cache.set(3, 'tri');
assert.deepStrictEqual(Array.from(cache.entries()), [[3, 'tri'], [1, 'rast'], [2, 'dva'], [8, 'huit']]);
});
}
});
Expand Down
8 changes: 8 additions & 0 deletions test/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
InvertedIndex,
LinkedList,
LRUCache,
LRUCacheWithDelete,
LRUMap,
MultiSet,
MultiMap,
Expand Down Expand Up @@ -155,6 +156,13 @@ let lrucache: LRUCache<string, number> = new LRUCache(10);
lrucache.set('one', 34);
let lrucacheItem: number = lrucache.get('one');

/**
* LRUCacheWithDelete
*/
let lrucwd: LRUCacheWithDelete<string, string> = new LRUCacheWithDelete(10);
lrucwd.set('one', 'uno')
let lrucwdItem: string = lrucwd.get('one')

/**
* LRUMap.
*/
Expand Down

0 comments on commit 9bfef48

Please sign in to comment.