From d2789f5cf57c2acc3b31266bbf234bc63409feb7 Mon Sep 17 00:00:00 2001 From: fahimfaisaal Date: Mon, 21 Mar 2022 20:24:41 +0600 Subject: [PATCH 1/2] feat: added new clean LFUCache class --- Cache/LFUCache.js | 302 ++++++++++++++++++++++++++---------- Cache/test/LFUCache.test.js | 72 ++++++--- 2 files changed, 269 insertions(+), 105 deletions(-) diff --git a/Cache/LFUCache.js b/Cache/LFUCache.js index 67974f0ffa..2c9ff7e71a 100644 --- a/Cache/LFUCache.js +++ b/Cache/LFUCache.js @@ -1,106 +1,238 @@ -class DoubleLinkedListNode { - // Double Linked List Node built specifically for LFU Cache - constructor (key, val) { +class CacheNode { + constructor (key, value, frequency) { this.key = key - this.val = val - this.freq = 0 - this.next = null - this.prev = null + this.value = value + this.frequency = frequency + + return Object.seal(this) } } -class DoubleLinkedList { - // Double Linked List built specifically for LFU Cache - constructor () { - this.head = new DoubleLinkedListNode(null, null) - this.rear = new DoubleLinkedListNode(null, null) - this.head.next = this.rear - this.rear.prev = this.head - } +// This frequency map class will act like javascript Map DS with more two custom method refresh & refresh +class FrequencyMap extends Map { + static get [Symbol.species] () { return Map } // for using Symbol.species we can access Map constructor @see -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species + get [Symbol.toStringTag] () { return '' } - _positionNode (node) { - // Helper function to position a node based on the frequency of the key - while (node.prev.key && node.prev.freq > node.freq) { - const node1 = node - const node2 = node.prev - node1.prev = node2.prev - node2.next = node1.prev - node1.next = node2 - node2.prev = node1 - } - } + /** + * @method refresh + * @description - It's revive a CacheNode, increment of this nodes frequency and refresh the frequencyMap via new incremented nodes frequency + * @param {CacheNode} node + */ + refresh (node) { + const { frequency } = node + const freqSet = this.get(frequency) + freqSet.delete(node) + + node.frequency++ - add (node) { - // Adds the given node to the end of the list (before rear) and positions it based on frequency - const temp = this.rear.prev - temp.next = node - node.prev = temp - this.rear.prev = node - node.next = this.rear - this._positionNode(node) + this.insert(node) } - remove (node) { - // Removes and returns the given node from the list - const tempLast = node.prev - const tempNext = node.next - node.prev = null - node.next = null - tempLast.next = tempNext - tempNext.prev = tempLast + /** + * @method insert + * @description - Add new CacheNode into HashSet by the frequency + * @param {CacheNode} node + */ + insert (node) { + const { frequency } = node - return node + if (!this.has(frequency)) { + this.set(frequency, new Set()) + } + + this.get(frequency).add(node) } } class LFUCache { - // LFU Cache to store a given capacity of data - // The Double Linked List is used to store the order of deletion from the cache - // The rear.prev holds the most frequently used key and the head.next holds the least used key - // When the number of elements reaches the capacity, the least frequently used item is removed before adding the next key - constructor (capacity) { - this.list = new DoubleLinkedList() - this.capacity = capacity - this.numKeys = 0 - this.hits = 0 - this.miss = 0 - this.cache = {} - } + #capacity + #frequencyMap - cacheInfo () { - // Return the details for the cache instance [hits, misses, capacity, current_size] - return `CacheInfo(hits=${this.hits}, misses=${this.miss}, capacity=${this.capacity}, current size=${this.numKeys})` - } + /** + * @param {number} capacity - The range of LFUCache + * @returns {LFUCache} - sealed + */ + constructor (capacity) { + this.#capacity = capacity + this.#frequencyMap = new FrequencyMap() + this.misses = 0 + this.hits = 0 + this.cache = new Map() + + return Object.seal(this) + } + + get capacity () { + return this.#capacity + } + + get size () { + return this.cache.size + } + + set capacity (newCapacity) { + if (this.#capacity > newCapacity) { + let diff = this.#capacity - newCapacity // get the decrement number of capacity + + while (diff--) { + this.#removeCacheNode() + } - set (key, value) { - // Sets the value for the input key and updates the Double Linked List - if (!(key in this.cache)) { - if (this.numKeys >= this.capacity) { - const keyToDelete = this.list.head.next.key - this.list.remove(this.cache[keyToDelete]) - delete this.cache[keyToDelete] - this.numKeys -= 1 + this.cache.size === 0 && this.#frequencyMap.clear() } - this.cache[key] = new DoubleLinkedListNode(key, value) - this.list.add(this.cache[key]) - this.numKeys += 1 - } else { - const node = this.list.remove(this.cache[key]) - node.val = value - this.list.add(node) + + this.#capacity = newCapacity } - } - get (key) { - // Returns the value for the input key and updates the Double Linked List. Returns null if key is not present in cache - if (key in this.cache) { - this.hits += 1 - this.list.add(this.list.remove(this.cache[key])) - return this.cache[key].val + get info () { + return Object.freeze({ + misses: this.misses, + hits: this.hits, + capacity: this.capacity, + currentSize: this.size, + leastFrequency: this.leastFrequency + }) + } + + get leastFrequency () { + const freqCacheIterator = this.#frequencyMap.keys() + let leastFrequency = freqCacheIterator.next().value || null + + // select the non-empty frequency Set + while (this.#frequencyMap.get(leastFrequency)?.size === 0) { + leastFrequency = freqCacheIterator.next().value + } + + return leastFrequency + } + + #removeCacheNode () { + const leastFreqSet = this.#frequencyMap.get(this.leastFrequency) + // Select the least recently used node from the least Frequency set + const LFUNode = leastFreqSet.values().next().value + + leastFreqSet.delete(LFUNode) + this.cache.delete(LFUNode.key) + } + + has (key) { + key = String(key) // converted to string + + return this.cache.has(key) + } + + /** + * @method get + * @description - This method return the value of key & refresh the frequencyMap by the oldNode + * @param {string} key + * @returns {any} + */ + get (key) { + key = String(key) // converted to string + + if (this.cache.has(key)) { + const oldNode = this.cache.get(key) + this.#frequencyMap.refresh(oldNode) + + this.hits++ + + return oldNode.value + } + + this.misses++ + return null + } + + /** + * @method set + * @description - This method stored the value by key & add frequency if it doesn't exist + * @param {string} key + * @param {any} value + * @param {number} frequency + * @returns {LFUCache} + */ + set (key, value, frequency = 1) { + key = String(key) // converted to string + + if (this.#capacity === 0) { + throw new RangeError('LFUCache ERROR: The Capacity is 0') + } + + if (this.cache.has(key)) { + const node = this.cache.get(key) + node.value = value + + this.#frequencyMap.refresh(node) + + return this + } + + // if the cache size is full, then it's delete the Least Frequency Used node + if (this.#capacity === this.cache.size) { + this.#removeCacheNode() + } + + const newNode = new CacheNode(key, value, frequency) + + this.cache.set(key, newNode) + this.#frequencyMap.insert(newNode) + + return this + } + + /** + * @method parse + * @description - This method receive a valid LFUCache JSON & run JSON.prase() method and marge with existing LFUCache + * @param {JSON} json + * @returns {LFUCache} - merged + */ + parse (json) { + const { misses, hits, cache } = JSON.parse(json) + + this.misses += misses ?? 0 + this.hits += hits ?? 0 + + for (const key in cache) { + const { value, frequency } = cache[key] + this.set(key, value, frequency) + } + + return this + } + + /** + * @method clear + * @description - This method cleared the whole LFUCache + * @returns {LFUCache} + */ + clear () { + this.cache.clear() + this.#frequencyMap.clear() + + return this + } + + /** + * @method toString + * @description - This method generate a JSON format of LFUCache & return it. + * @param {number} indent + * @returns {string} - JSON + */ + toString (indent) { + const replacer = (_, value) => { + if (value instanceof Set) { + return [...value] + } + + if (value instanceof Map) { + return Object.fromEntries(value) + } + + return value + } + + return JSON.stringify(this, replacer, indent) } - this.miss += 1 - return null - } } -export { LFUCache } +export default LFUCache diff --git a/Cache/test/LFUCache.test.js b/Cache/test/LFUCache.test.js index 94ac0165e2..212ab68939 100644 --- a/Cache/test/LFUCache.test.js +++ b/Cache/test/LFUCache.test.js @@ -1,39 +1,71 @@ -import { LFUCache } from '../LFUCache' +import LFUCache from '../LFUCache' import { fibonacciCache } from './cacheTest' -describe('LFUCache', () => { - it('Example 1 (Small Cache, size=2)', () => { +describe('Testing LFUCache class', () => { + it('Example 1 (Small Cache, size = 2)', () => { const cache = new LFUCache(2) - cache.set(1, 1) - cache.set(2, 2) - expect(cache.get(1)).toBe(1) - expect(cache.get(2)).toBe(2) + expect(cache.capacity).toBe(2) + + cache.set(1, 1) // frequency = 1 + cache.set(2, 2) // frequency = 1 + + expect(cache.get(1)).toBe(1) // frequency = 2 + expect(cache.get(2)).toBe(2) // frequency = 2 // Additional entries triggers cache rotate - cache.set(3, 3) + cache.set(3, 3) // frequency = 1 & key 1 removed from the cached, cause now it's tie and followed the LRU system + + expect(cache.get(1)).toBe(null) // misses = 1 + expect(cache.get(2)).toBe(2) // frequency = 3 + expect(cache.get(3)).toBe(3) // frequency = 2 + + cache.set(4, 4) // frequency = 1 & key 3 removed cause the frequency of 3 is 2 which is least frequency + expect(cache.get(1)).toBe(null) // misses = 2 + expect(cache.get(2)).toBe(2) // frequency = 4 + expect(cache.get(3)).toBe(null) // misses = 3 + expect(cache.get(4)).toBe(4) // frequency = 2 which is least + + expect(cache.info).toEqual({ + misses: 3, + hits: 6, + capacity: 2, + currentSize: 2, + leastFrequency: 2 + }) + + const json = '{"misses":3,"hits":6,"cache":{"2":{"key":"2","value":2,"frequency":4},"4":{"key":"4","value":4,"frequency":2}}}' + expect(cache.toString()).toBe(json) + + const cacheInstance = cache.parse(json) // again merge the json - // Then we should have a cache miss for the first entry added - expect(cache.get(1)).toBe(null) - expect(cache.get(2)).toBe(2) - expect(cache.get(3)).toBe(3) + expect(cacheInstance).toBe(cache) // return the same cache - cache.set(4, 4) - expect(cache.get(1)).toBe(null) // cache miss - expect(cache.get(2)).toBe(null) // cache miss - expect(cache.get(3)).toBe(3) - expect(cache.get(4)).toBe(4) + expect(cache.info).toEqual({ // after merging the info + misses: 6, + hits: 12, + capacity: 2, + currentSize: 2, + leastFrequency: 3 + }) - expect(cache.cacheInfo()).toBe('CacheInfo(hits=6, misses=3, capacity=2, current size=2)') + const clearedCache = cache.clear() // clear the cache + expect(clearedCache.size).toBe(0) }) - it('Example 2 (Computing Fibonacci Series, size=100)', () => { + it('Example 2 (Computing Fibonacci Series, size = 100)', () => { const cache = new LFUCache(100) for (let i = 1; i <= 100; i++) { fibonacciCache(i, cache) } - expect(cache.cacheInfo()).toBe('CacheInfo(hits=193, misses=103, capacity=100, current size=98)') + expect(cache.info).toEqual({ + misses: 103, + hits: 193, + capacity: 100, + currentSize: 98, + leastFrequency: 1 + }) }) }) From a65f86680e0470d76e35cdec9ab4e1a34c185c3e Mon Sep 17 00:00:00 2001 From: fahimfaisaal Date: Mon, 21 Mar 2022 20:47:06 +0600 Subject: [PATCH 2/2] fixed: resolved spell mistake & added test casses --- Cache/LFUCache.js | 20 ++++++++++++++++++-- Cache/test/LFUCache.test.js | 12 ++++++++---- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/Cache/LFUCache.js b/Cache/LFUCache.js index 2c9ff7e71a..9bb08f6879 100644 --- a/Cache/LFUCache.js +++ b/Cache/LFUCache.js @@ -8,7 +8,7 @@ class CacheNode { } } -// This frequency map class will act like javascript Map DS with more two custom method refresh & refresh +// This frequency map class will act like javascript Map DS with more two custom method refresh & insert class FrequencyMap extends Map { static get [Symbol.species] () { return Map } // for using Symbol.species we can access Map constructor @see -> https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species get [Symbol.toStringTag] () { return '' } @@ -62,14 +62,25 @@ class LFUCache { return Object.seal(this) } + /** + * Get the capacity of the LFUCache + * @returns {number} + */ get capacity () { return this.#capacity } + /** + * Get the current size of LFUCache + * @returns {number} + */ get size () { return this.cache.size } + /** + * Set the capacity of the LFUCache if you decrease the capacity its removed CacheNodes following the LFU - least frequency used + */ set capacity (newCapacity) { if (this.#capacity > newCapacity) { let diff = this.#capacity - newCapacity // get the decrement number of capacity @@ -115,6 +126,11 @@ class LFUCache { this.cache.delete(LFUNode.key) } + /** + * if key exist then return true otherwise false + * @param {any} key + * @returns {boolean} + */ has (key) { key = String(key) // converted to string @@ -182,7 +198,7 @@ class LFUCache { /** * @method parse - * @description - This method receive a valid LFUCache JSON & run JSON.prase() method and marge with existing LFUCache + * @description - This method receive a valid LFUCache JSON & run JSON.prase() method and merge with existing LFUCache * @param {JSON} json * @returns {LFUCache} - merged */ diff --git a/Cache/test/LFUCache.test.js b/Cache/test/LFUCache.test.js index 212ab68939..a10c91b859 100644 --- a/Cache/test/LFUCache.test.js +++ b/Cache/test/LFUCache.test.js @@ -3,7 +3,9 @@ import { fibonacciCache } from './cacheTest' describe('Testing LFUCache class', () => { it('Example 1 (Small Cache, size = 2)', () => { - const cache = new LFUCache(2) + const cache = new LFUCache(1) // initially capacity 1 + + cache.capacity = 2 // increase the capacity expect(cache.capacity).toBe(2) @@ -41,12 +43,14 @@ describe('Testing LFUCache class', () => { expect(cacheInstance).toBe(cache) // return the same cache + cache.capacity = 1 // decrease the capacity + expect(cache.info).toEqual({ // after merging the info misses: 6, hits: 12, - capacity: 2, - currentSize: 2, - leastFrequency: 3 + capacity: 1, + currentSize: 1, + leastFrequency: 5 }) const clearedCache = cache.clear() // clear the cache