merge: Added new clean LFUCache class (#939)

* feat: added new clean LFUCache class

* fixed: resolved spell mistake & added test casses
This commit is contained in:
Fahim Faisaal
2022-03-21 22:43:45 +06:00
committed by GitHub
parent 9d2a7f1639
commit ad68c63947
2 changed files with 292 additions and 108 deletions

View File

@ -1,106 +1,254 @@
class DoubleLinkedListNode { class CacheNode {
// Double Linked List Node built specifically for LFU Cache constructor (key, value, frequency) {
constructor (key, val) {
this.key = key this.key = key
this.val = val this.value = value
this.freq = 0 this.frequency = frequency
this.next = null
this.prev = null return Object.seal(this)
} }
} }
class DoubleLinkedList { // This frequency map class will act like javascript Map DS with more two custom method refresh & insert
// Double Linked List built specifically for LFU Cache class FrequencyMap extends Map {
constructor () { 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
this.head = new DoubleLinkedListNode(null, null) get [Symbol.toStringTag] () { return '' }
this.rear = new DoubleLinkedListNode(null, null)
this.head.next = this.rear /**
this.rear.prev = this.head * @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++
this.insert(node)
} }
_positionNode (node) { /**
// Helper function to position a node based on the frequency of the key * @method insert
while (node.prev.key && node.prev.freq > node.freq) { * @description - Add new CacheNode into HashSet by the frequency
const node1 = node * @param {CacheNode} node
const node2 = node.prev */
node1.prev = node2.prev insert (node) {
node2.next = node1.prev const { frequency } = node
node1.next = node2
node2.prev = node1 if (!this.has(frequency)) {
this.set(frequency, new Set())
} }
}
add (node) { this.get(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)
}
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
return node
} }
} }
class LFUCache { class LFUCache {
// LFU Cache to store a given capacity of data #capacity
// The Double Linked List is used to store the order of deletion from the cache #frequencyMap
// 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 = {}
}
cacheInfo () { /**
// Return the details for the cache instance [hits, misses, capacity, current_size] * @param {number} capacity - The range of LFUCache
return `CacheInfo(hits=${this.hits}, misses=${this.miss}, capacity=${this.capacity}, current size=${this.numKeys})` * @returns {LFUCache} - sealed
} */
constructor (capacity) {
this.#capacity = capacity
this.#frequencyMap = new FrequencyMap()
this.misses = 0
this.hits = 0
this.cache = new Map()
set (key, value) { return Object.seal(this)
// 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 * Get the capacity of the LFUCache
this.list.remove(this.cache[keyToDelete]) * @returns {number}
delete this.cache[keyToDelete] */
this.numKeys -= 1 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
while (diff--) {
this.#removeCacheNode()
}
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)
}
}
get (key) { this.#capacity = newCapacity
// 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 get info () {
this.list.add(this.list.remove(this.cache[key])) return Object.freeze({
return this.cache[key].val 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)
}
/**
* if key exist then return true otherwise false
* @param {any} key
* @returns {boolean}
*/
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 merge 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

View File

@ -1,39 +1,75 @@
import { LFUCache } from '../LFUCache' import LFUCache from '../LFUCache'
import { fibonacciCache } from './cacheTest' import { fibonacciCache } from './cacheTest'
describe('LFUCache', () => { describe('Testing LFUCache class', () => {
it('Example 1 (Small Cache, size=2)', () => { it('Example 1 (Small Cache, size = 2)', () => {
const cache = new LFUCache(2) const cache = new LFUCache(1) // initially capacity 1
cache.set(1, 1)
cache.set(2, 2)
expect(cache.get(1)).toBe(1) cache.capacity = 2 // increase the capacity
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 // 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
// Then we should have a cache miss for the first entry added expect(cache.get(1)).toBe(null) // misses = 1
expect(cache.get(1)).toBe(null) expect(cache.get(2)).toBe(2) // frequency = 3
expect(cache.get(2)).toBe(2) expect(cache.get(3)).toBe(3) // frequency = 2
expect(cache.get(3)).toBe(3)
cache.set(4, 4) 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) // cache miss expect(cache.get(1)).toBe(null) // misses = 2
expect(cache.get(2)).toBe(null) // cache miss expect(cache.get(2)).toBe(2) // frequency = 4
expect(cache.get(3)).toBe(3) expect(cache.get(3)).toBe(null) // misses = 3
expect(cache.get(4)).toBe(4) expect(cache.get(4)).toBe(4) // frequency = 2 which is least
expect(cache.cacheInfo()).toBe('CacheInfo(hits=6, misses=3, capacity=2, current size=2)') 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
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: 1,
currentSize: 1,
leastFrequency: 5
})
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) const cache = new LFUCache(100)
for (let i = 1; i <= 100; i++) { for (let i = 1; i <= 100; i++) {
fibonacciCache(i, cache) 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
})
}) })
}) })