From d2789f5cf57c2acc3b31266bbf234bc63409feb7 Mon Sep 17 00:00:00 2001
From: fahimfaisaal <fahimfaisal.soikot@gmail.com>
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 <fahimfaisal.soikot@gmail.com>
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
