go.charczuk.com@v0.0.0-20240327042549-bc490516bd1a/sdk/collections/lru.go (about)

     1  /*
     2  
     3  Copyright (c) 2023 - Present. Will Charczuk. All rights reserved.
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file at the root of the repository.
     5  
     6  */
     7  
     8  package collections
     9  
    10  // LRU is a structure that allows you to evict items based on
    11  // which were used last.
    12  //
    13  // It combines a light linked list structure with a map for
    14  // fast lookups and fast evictions based on which item
    15  // is at the "tail" of the linked list.
    16  type LRU[K comparable, V any] struct {
    17  	Capacity int
    18  	OnEvict  func(K, V)
    19  
    20  	head   *lruItem[K, V]
    21  	tail   *lruItem[K, V]
    22  	lookup map[K]*lruItem[K, V]
    23  }
    24  
    25  // Get returns an item with a given key.
    26  func (lru *LRU[K, V]) Get(k K) (v V, ok bool) {
    27  	if lru.lookup == nil {
    28  		return
    29  	}
    30  	var i *lruItem[K, V]
    31  	if i, ok = lru.lookup[k]; !ok {
    32  		return
    33  	}
    34  	lru.moveToTail(i)
    35  	v = i.value
    36  	return
    37  }
    38  
    39  // Touch moves a given key to the end of the lru queue.
    40  func (lru *LRU[K, V]) Touch(k K) (ok bool) {
    41  	if lru.lookup == nil {
    42  		return
    43  	}
    44  
    45  	item, ok := lru.lookup[k]
    46  	if !ok {
    47  		return
    48  	}
    49  	lru.moveToTail(item)
    50  	return
    51  }
    52  
    53  // Get returns an item with a given key.
    54  func (lru *LRU[K, V]) Set(k K, v V) {
    55  	if lru.lookup == nil {
    56  		lru.lookup = make(map[K]*lruItem[K, V])
    57  	}
    58  
    59  	if item, ok := lru.lookup[k]; ok {
    60  		item.value = v
    61  		lru.moveToTail(item)
    62  		return
    63  	}
    64  
    65  	newItem := &lruItem[K, V]{
    66  		key:   k,
    67  		value: v,
    68  	}
    69  	lru.lookup[k] = newItem
    70  	if lru.head == nil {
    71  		lru.head = newItem
    72  		lru.tail = newItem
    73  		return
    74  	}
    75  	lru.moveToTail(newItem)
    76  
    77  	if lru.Capacity > 0 && len(lru.lookup) > lru.Capacity {
    78  		delete(lru.lookup, lru.head.key)
    79  		if lru.OnEvict != nil {
    80  			lru.OnEvict(lru.head.key, lru.head.value)
    81  		}
    82  		lru.removeHead()
    83  	}
    84  }
    85  
    86  // Remove removes an element.
    87  func (lru *LRU[K, V]) Remove(k K) (ok bool) {
    88  	if lru.lookup == nil {
    89  		return
    90  	}
    91  
    92  	var i *lruItem[K, V]
    93  	if i, ok = lru.lookup[k]; !ok {
    94  		return
    95  	}
    96  	delete(lru.lookup, k)
    97  
    98  	if lru.head == i {
    99  		lru.removeHead()
   100  		return
   101  	}
   102  	lru.removeItem(i)
   103  	return
   104  }
   105  
   106  // Head returns the head, or oldest, key and value.
   107  func (lru *LRU[K, V]) Head() (k K, v V, ok bool) {
   108  	if lru.head == nil {
   109  		return
   110  	}
   111  	k = lru.head.key
   112  	v = lru.head.value
   113  	ok = true
   114  	return
   115  }
   116  
   117  // Tail returns the tail, or most recently used, key and value.
   118  func (lru *LRU[K, V]) Tail() (k K, v V, ok bool) {
   119  	if lru.tail == nil {
   120  		return
   121  	}
   122  	k = lru.tail.key
   123  	v = lru.tail.value
   124  	ok = true
   125  	return
   126  }
   127  
   128  // Len returns the number of items in the lru cache.
   129  func (lru *LRU[K, V]) Len() int { return len(lru.lookup) }
   130  
   131  // Each calls a given function for each element in the lru cache.
   132  func (lru *LRU[K, V]) Each(fn func(K, V)) {
   133  	current := lru.head
   134  	for current != nil {
   135  		fn(current.key, current.value)
   136  		current = current.previous
   137  	}
   138  }
   139  
   140  //
   141  // internal helpers
   142  //
   143  
   144  func (lru *LRU[K, V]) moveToTail(i *lruItem[K, V]) {
   145  	if lru.tail == i {
   146  		return
   147  	}
   148  
   149  	// remove item from existing place in list
   150  	if lru.head == i {
   151  		lru.head = i.previous
   152  		lru.head.next = nil
   153  	} else {
   154  		after := i.previous
   155  		before := i.next
   156  		if after != nil {
   157  			after.next = before
   158  		}
   159  		if before != nil {
   160  			before.previous = after
   161  		}
   162  	}
   163  
   164  	// append to tail
   165  	i.next = lru.tail
   166  	i.previous = nil
   167  	lru.tail.previous = i
   168  	lru.tail = i
   169  }
   170  
   171  func (lru *LRU[K, V]) removeHead() {
   172  	if lru.head == nil {
   173  		return
   174  	}
   175  
   176  	// if we have a single element,
   177  	// we will need to change the tail
   178  	// pointer as well
   179  	if lru.head == lru.tail {
   180  		lru.head = nil
   181  		lru.tail = nil
   182  		return
   183  	}
   184  
   185  	// remove from head
   186  	after := lru.head.previous
   187  	if after != nil {
   188  		after.next = nil
   189  	}
   190  	lru.head = after
   191  }
   192  
   193  func (lru *LRU[K, V]) removeItem(i *lruItem[K, V]) {
   194  	after := i.previous
   195  	before := i.next
   196  	if after != nil {
   197  		after.next = before
   198  	}
   199  	if before != nil {
   200  		before.previous = after
   201  	}
   202  	if lru.tail == i {
   203  		lru.tail = i.next
   204  		if lru.tail != nil {
   205  			lru.tail.previous = nil
   206  		}
   207  	}
   208  }
   209  
   210  type lruItem[K comparable, V any] struct {
   211  	key   K
   212  	value V
   213  
   214  	// next points towards the head
   215  	next *lruItem[K, V]
   216  	// previous points towards the tail
   217  	previous *lruItem[K, V]
   218  }