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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy