From c99ec2d834950115b0ae381fcbc890acee098177 Mon Sep 17 00:00:00 2001 From: Tom Payne Date: Mon, 4 Feb 2013 17:29:46 +0100 Subject: [PATCH] Add ol.structs.LRUCache --- src/ol/structs/lrucache.js | 271 ++++++++++++++++++++++++++++++++++ test/spec/ol/lrucache.test.js | 162 ++++++++++++++++++++ 2 files changed, 433 insertions(+) create mode 100644 src/ol/structs/lrucache.js create mode 100644 test/spec/ol/lrucache.test.js diff --git a/src/ol/structs/lrucache.js b/src/ol/structs/lrucache.js new file mode 100644 index 0000000000..863859bdba --- /dev/null +++ b/src/ol/structs/lrucache.js @@ -0,0 +1,271 @@ +goog.provide('ol.structs.LRUCache'); + +goog.require('goog.asserts'); +goog.require('goog.object'); + + + +/** + * @constructor + * Implements a Least-Recently-Used cache where the keys do not conflict with + * Object's properties (e.g. 'hasOwnProperty' is not allowed as a key). Expiring + * items from the cache is the responsibility of the user. + */ +ol.structs.LRUCache = function() { + + /** + * @private + * @type {number} + */ + this.count_ = 0; + + /** + * @private + * @type {Object.} + */ + this.entries_ = {}; + + /** + * @private + * @type {?ol.structs.LRUCacheEntry_} + */ + this.oldest_ = null; + + /** + * @private + * @type {?ol.structs.LRUCacheEntry_} + */ + this.newest_ = null; + +}; + + +/** + * FIXME empty description for jsdoc + */ +ol.structs.LRUCache.prototype.assertValid = function() { + if (this.count_ === 0) { + goog.asserts.assert(goog.object.isEmpty(this.entries_)); + goog.asserts.assert(goog.isNull(this.oldest_)); + goog.asserts.assert(goog.isNull(this.newest_)); + } else { + goog.asserts.assert(goog.object.getCount(this.entries_) == this.count_); + goog.asserts.assert(!goog.isNull(this.oldest_)); + goog.asserts.assert(goog.isNull(this.oldest_.older)); + goog.asserts.assert(!goog.isNull(this.newest_)); + goog.asserts.assert(goog.isNull(this.newest_.newer)); + var i, entry; + var older = null; + i = 0; + for (entry = this.oldest_; !goog.isNull(entry); entry = entry.newer) { + goog.asserts.assert(entry.older === older); + older = entry; + ++i; + } + goog.asserts.assert(i == this.count_); + var newer = null; + i = 0; + for (entry = this.newest_; !goog.isNull(entry); entry = entry.older) { + goog.asserts.assert(entry.newer === newer); + newer = entry; + ++i; + } + goog.asserts.assert(i == this.count_); + } +}; + + +/** + * FIXME empty description for jsdoc + */ +ol.structs.LRUCache.prototype.clear = function() { + this.count_ = 0; + this.entries_ = {}; + this.oldest_ = null; + this.newest_ = null; +}; + + +/** + * @param {string} key Key. + * @return {boolean} Contains key. + */ +ol.structs.LRUCache.prototype.containsKey = function(key) { + return key in this.entries_; +}; + + +/** + * @param {Function} f Function. + * @param {Object=} opt_obj Object. + */ +ol.structs.LRUCache.prototype.forEach = function(f, opt_obj) { + var entry = this.oldest_; + while (!goog.isNull(entry)) { + f.call(opt_obj, entry.value, entry.key, this); + entry = entry.newer; + } +}; + + +/** + * @param {string} key Key. + * @return {*} Value. + */ +ol.structs.LRUCache.prototype.get = function(key) { + var entry = this.entries_[key]; + goog.asserts.assert(goog.isDef(entry)); + if (entry === this.newest_) { + return entry.value; + } else if (entry === this.oldest_) { + this.oldest_ = this.oldest_.newer; + this.oldest_.older = null; + } else { + entry.newer.older = entry.older; + entry.older.newer = entry.newer; + } + entry.newer = null; + entry.older = this.newest_; + this.newest_.newer = entry; + this.newest_ = entry; + return entry.value; +}; + + +/** + * @return {number} Count. + */ +ol.structs.LRUCache.prototype.getCount = function() { + return this.count_; +}; + + +/** + * @return {Array.} Keys. + */ +ol.structs.LRUCache.prototype.getKeys = function() { + var keys = new Array(this.count_); + var i = 0; + var entry; + for (entry = this.newest_; !goog.isNull(entry); entry = entry.older) { + keys[i++] = entry.key; + } + goog.asserts.assert(i == this.count_); + return keys; +}; + + +/** + * @return {string} Last key. + */ +ol.structs.LRUCache.prototype.getLastKey = function() { + goog.asserts.assert(!goog.isNull(this.newest_)); + return this.newest_.key; +}; + + +/** + * @return {Array} Values. + */ +ol.structs.LRUCache.prototype.getValues = function() { + var values = new Array(this.count_); + var i = 0; + var entry; + for (entry = this.newest_; !goog.isNull(entry); entry = entry.older) { + values[i++] = entry.value; + } + goog.asserts.assert(i == this.count_); + return values; +}; + + +/** + * @return {*} Last value. + */ +ol.structs.LRUCache.prototype.peekLast = function() { + goog.asserts.assert(!goog.isNull(this.oldest_)); + return this.oldest_.value; +}; + + +/** + * @return {string} Last key. + */ +ol.structs.LRUCache.prototype.peekLastKey = function() { + goog.asserts.assert(!goog.isNull(this.oldest_)); + return this.oldest_.key; +}; + + +/** + * @return {*} value Value. + */ +ol.structs.LRUCache.prototype.pop = function() { + goog.asserts.assert(!goog.isNull(this.oldest_)); + goog.asserts.assert(!goog.isNull(this.newest_)); + var entry = this.oldest_; + goog.asserts.assert(entry.key in this.entries_); + delete this.entries_[entry.key]; + if (!goog.isNull(entry.newer)) { + entry.newer.older = null; + } + this.oldest_ = entry.newer; + if (goog.isNull(this.oldest_)) { + this.newest_ = null; + } + --this.count_; + return entry.value; +}; + + +/** + * @param {string} key Key. + * @param {*} value Value. + */ +ol.structs.LRUCache.prototype.set = function(key, value) { + goog.asserts.assert(!(key in {})); + goog.asserts.assert(!(key in this.entries_)); + var entry = new ol.structs.LRUCacheEntry_(key, value, null, this.newest_); + if (goog.isNull(this.newest_)) { + this.oldest_ = entry; + } else { + this.newest_.newer = entry; + } + this.newest_ = entry; + this.entries_[key] = entry; + ++this.count_; +}; + + + +/** + * @constructor + * @private + * @param {string} key Key. + * @param {*} value Value. + * @param {ol.structs.LRUCacheEntry_} newer Newer. + * @param {ol.structs.LRUCacheEntry_} older Older. + */ +ol.structs.LRUCacheEntry_ = function(key, value, newer, older) { + + /** + * @type {string} + */ + this.key = key; + + /** + * @type {*} + */ + this.value = value; + + /** + * @type {ol.structs.LRUCacheEntry_} + */ + this.newer = newer; + + /** + * @type {ol.structs.LRUCacheEntry_} + */ + this.older = older; + +}; diff --git a/test/spec/ol/lrucache.test.js b/test/spec/ol/lrucache.test.js new file mode 100644 index 0000000000..7ceb8a0115 --- /dev/null +++ b/test/spec/ol/lrucache.test.js @@ -0,0 +1,162 @@ +goog.provide('ol.test.LRUCache'); + + +describe('ol.structs.LRUCache', function() { + + var lruCache; + + function fillLRUCache(lruCache) { + lruCache.set('a', 0); + lruCache.set('b', 1); + lruCache.set('c', 2); + lruCache.set('d', 3); + } + + beforeEach(function() { + lruCache = new ol.structs.LRUCache(); + }); + + describe('empty cache', function() { + it('has size zero', function() { + expect(lruCache.getCount()).toEqual(0); + }); + it('has no keys', function() { + expect(lruCache.getKeys()).toEqual([]); + }); + it('has no values', function() { + expect(lruCache.getValues()).toEqual([]); + }); + }); + + describe('populating', function() { + it('returns the correct size', function() { + fillLRUCache(lruCache); + expect(lruCache.getCount()).toEqual(4); + }); + it('contains the correct keys in the correct order', function() { + fillLRUCache(lruCache); + expect(lruCache.getKeys()).toEqual(['d', 'c', 'b', 'a']); + }); + it('contains the correct values in the correct order', function() { + fillLRUCache(lruCache); + expect(lruCache.getValues()).toEqual([3, 2, 1, 0]); + }); + it('reports which keys are contained', function() { + fillLRUCache(lruCache); + expect(lruCache.containsKey('a')).toBeTruthy(); + expect(lruCache.containsKey('b')).toBeTruthy(); + expect(lruCache.containsKey('c')).toBeTruthy(); + expect(lruCache.containsKey('d')).toBeTruthy(); + expect(lruCache.containsKey('e')).toBeFalsy(); + }); + }); + + describe('getting the oldest key', function() { + it('moves the key to newest position', function() { + fillLRUCache(lruCache); + lruCache.get('a'); + expect(lruCache.getCount()).toEqual(4); + expect(lruCache.getKeys()).toEqual(['a', 'd', 'c', 'b']); + expect(lruCache.getValues()).toEqual([0, 3, 2, 1]); + }); + }); + + describe('getting a key in the middle', function() { + it('moves the key to newest position', function() { + fillLRUCache(lruCache); + lruCache.get('b'); + expect(lruCache.getCount()).toEqual(4); + expect(lruCache.getKeys()).toEqual(['b', 'd', 'c', 'a']); + expect(lruCache.getValues()).toEqual([1, 3, 2, 0]); + }); + }); + + describe('getting the newest key', function() { + it('maintains the key to newest position', function() { + fillLRUCache(lruCache); + lruCache.get('d'); + expect(lruCache.getCount()).toEqual(4); + expect(lruCache.getKeys()).toEqual(['d', 'c', 'b', 'a']); + expect(lruCache.getValues()).toEqual([3, 2, 1, 0]); + }); + }); + + describe('setting a new value', function() { + it('adds it as the newest value', function() { + fillLRUCache(lruCache); + lruCache.set('e', 4); + expect(lruCache.getKeys()).toEqual(['e', 'd', 'c', 'b', 'a']); + expect(lruCache.getValues()).toEqual([4, 3, 2, 1, 0]); + }); + }); + + describe('setting an existing value', function() { + it('raises an exception', function() { + fillLRUCache(lruCache); + expect(function() { + lruCache.set('a', 0); + }).toThrow(); + }); + }); + + describe('setting a disallowed key', function() { + it('raises an exception', function() { + expect(function() { + lruCache.set('hasOwnProperty', 0); + }).toThrow(); + }); + }); + + describe('popping a value', function() { + it('returns the least-recent-used value', function() { + fillLRUCache(lruCache); + expect(lruCache.pop()).toEqual(0); + expect(lruCache.getCount()).toEqual(3); + expect(lruCache.containsKey('a')).toBeFalsy(); + expect(lruCache.pop()).toEqual(1); + expect(lruCache.getCount()).toEqual(2); + expect(lruCache.containsKey('b')).toBeFalsy(); + expect(lruCache.pop()).toEqual(2); + expect(lruCache.getCount()).toEqual(1); + expect(lruCache.containsKey('c')).toBeFalsy(); + expect(lruCache.pop()).toEqual(3); + expect(lruCache.getCount()).toEqual(0); + expect(lruCache.containsKey('d')).toBeFalsy(); + }); + }); + + describe('peeking at the last value', function() { + it('returns the last key', function() { + fillLRUCache(lruCache); + expect(lruCache.peekLast()).toEqual(0); + }); + it('throws an exception when the cache is empty', function() { + expect(function() { + lruCache.peekLast(); + }).toThrow(); + }); + }); + + describe('peeking at the last key', function() { + it('returns the last key', function() { + fillLRUCache(lruCache); + expect(lruCache.peekLastKey()).toEqual('a'); + }); + it('throws an exception when the cache is empty', function() { + expect(function() { + lruCache.peekLastKey(); + }).toThrow(); + }); + }); + + describe('clearing the cache', function() { + it('clears the cache', function() { + fillLRUCache(lruCache); + lruCache.clear(); + expect(lruCache.getCount()).toEqual(0); + expect(lruCache.getKeys()).toEqual([]); + expect(lruCache.getValues()).toEqual([]); + }); + }); + +});