dlinked 0.1.8 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,43 +1,69 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DLinked
4
- # DLinked::CacheList
4
+ # A specialized doubly linked list for managing keys in a Least Recently Used (LRU) cache.
5
5
  #
6
- # A specialized doubly linked list subclass used as the central key management
7
- # component for an in-memory LRU cache.
6
+ # This class extends {DLinked::List} by integrating a `Hash` map (`@lru_nodes`)
7
+ # to provide **O(1)** time complexity for all critical cache operations:
8
+ # adding, accessing (touching), evicting, and removing keys.
8
9
  #
9
- # It integrates a hash map (`@lru_nodes`) to provide **O(1)** operations for:
10
- # - Prepending/Adding a new key (MRU).
11
- # - Touching/Moving an existing key to the head (MRU).
12
- # - Evicting the least recently used key (LRU).
13
- # - Removing an item by key.
10
+ # The list maintains items in order from Most Recently Used (MRU) at the
11
+ # head to Least Recently Used (LRU) at the tail.
14
12
  #
15
- # It maintains the standard list behavior of the superclass {DLinked::List}
16
- # but focuses on key-based operations rather than simple value manipulation.
13
+ # @example Implementing a simple LRU Cache
14
+ # class MyLRUCache
15
+ # def initialize(capacity)
16
+ # @capacity = capacity
17
+ # @list = DLinked::CacheList.new
18
+ # @data = {}
19
+ # end
20
+ #
21
+ # def get(key)
22
+ # return nil unless @data.key?(key)
23
+ # @list.move_to_head_by_key(key) # Mark as recently used
24
+ # @data[key]
25
+ # end
26
+ #
27
+ # def set(key, value)
28
+ # if @data.key?(key)
29
+ # @list.move_to_head_by_key(key)
30
+ # else
31
+ # @list.prepend_key(key, value)
32
+ # evict if @list.size > @capacity
33
+ # end
34
+ # @data[key] = value
35
+ # end
36
+ #
37
+ # private
38
+ #
39
+ # def evict
40
+ # lru_key = @list.pop_key
41
+ # @data.delete(lru_key)
42
+ # end
43
+ # end
17
44
  class CacheList < DLinked::List
18
- class Node < DLinked::List::Node
19
- # Add an attribute to hold the cache key
45
+ # A specialized node for the `CacheList` that includes a `key`.
46
+ class Node < DLinked::List::Node
47
+ # @!attribute [rw] key
48
+ # @return [Object] The cache key associated with this node.
20
49
  attr_accessor :key
21
50
 
22
- # IMPORTANT: Accept the required three arguments from the base List#prepend,
23
- # but only initialize the base class with value, prev, and next.
51
+ # Initializes a new CacheList Node.
52
+ # @param value [Object] The value to store in the node.
53
+ # @param prev [Node, nil] The preceding node.
54
+ # @param next_node [Node, nil] The succeeding node.
55
+ # @param key [Object] The cache key for this node.
24
56
  def initialize(value, prev = nil, next_node = nil, key = nil)
25
- # Call the parent Node initializer using the first three arguments
26
- super(value, prev, next_node)
27
-
28
- # Initialize the new, specialized attribute
57
+ super(value, prev, next_node)
29
58
  @key = key
30
59
  end
31
60
  end
61
+
32
62
  # @!attribute [r] lru_nodes
33
- # @return [Hash] The internal hash map storing {key => DLinked::CacheList::Node} references.
63
+ # @return [Hash{Object => DLinked::CacheList::Node}] The internal hash map.
34
64
  attr_reader :lru_nodes
35
- # # --- 1. OVERRIDE THE CONSTANT ---
36
- Node = DLinked::CacheList::Node
37
-
38
65
 
39
- # Initializes a new CacheList instance.
40
- # @return [void]
66
+ # Initializes a new, empty `CacheList`.
41
67
  def initialize
42
68
  super
43
69
  @lru_nodes = {}
@@ -45,94 +71,106 @@ module DLinked
45
71
 
46
72
  # --- LRU Management Methods (O(1)) ---
47
73
 
48
- # 2. Expose a key-based prepend method
74
+ # Adds a new key-value pair to the head of the list (making it the MRU item).
49
75
  #
50
- # Adds a new key-value pair to the head of the list (Most Recently Used / MRU).
51
- # This operation is atomic, adding the node to the list and storing the
52
- # node reference in the internal hash map.
76
+ # This is an **O(1)** operation.
53
77
  #
54
- # @param key [Object] The cache key to be managed by the LRU list.
55
- # @param value [Object] The value to store inside the list node (usually the same as the key in an LRU list).
56
- # @raise [RuntimeError] If the key already exists in the LRU map.
57
- # @return [true] Returns true on successful insertion.
78
+ # @example
79
+ # lru = DLinked::CacheList.new
80
+ # lru.prepend_key(:a, 1)
81
+ # lru.to_a # => [1]
82
+ #
83
+ # @param key [Object] The cache key.
84
+ # @param value [Object] The value to store in the list node.
85
+ # @raise [RuntimeError] If the key already exists.
86
+ # @return [true] `true` on successful insertion.
58
87
  def prepend_key(key, value)
59
- if @lru_nodes.key?(key)
60
- raise "Key #{key} already exists in the LRU list."
61
- end
88
+ raise "Key #{key} already exists in the LRU list." if @lru_nodes.key?(key)
62
89
 
63
- # The specialized Node creation must happen here
64
- node = Node.new(value, nil, @head, key)
65
-
66
- # Use the base class's list logic
67
- list_prepend_logic(node)
68
-
69
- # Store the node reference in the map (O(1))
70
- @lru_nodes[key] = node
71
-
72
- true
90
+ node = Node.new(value, nil, @head, key)
91
+ list_prepend_logic(node) # Use base class's logic
92
+ @lru_nodes[key] = node
93
+ true
73
94
  end
74
95
 
75
- # 3. Expose a key-based pop method (for eviction)
76
- #
77
96
  # Removes the Least Recently Used (LRU) item from the tail of the list.
78
- # This performs an O(1) removal and updates the internal hash map.
79
97
  #
80
- # @return [Object, nil] Returns the key of the removed LRU item, or nil if the list is empty.
98
+ # This is an **O(1)** operation.
99
+ #
100
+ # @example
101
+ # lru = DLinked::CacheList.new
102
+ # lru.prepend_key(:a, 1)
103
+ # lru.prepend_key(:b, 2) # List is now [:b, :a]
104
+ # lru.pop_key # => :a
105
+ #
106
+ # @return [Object, nil] The key of the removed LRU item, or `nil` if the list is empty.
81
107
  def pop_key
82
- # O(1) - Base class's pop operation on the tail node
83
- node = list_pop_logic
108
+ node = list_pop_logic
84
109
  return nil unless node
85
-
110
+
86
111
  @lru_nodes.delete(node.key)
87
- return node.key
112
+ node.key
88
113
  end
89
-
90
- # 4. Expose the O(1) touch operation
114
+
115
+ # Moves an existing key from its current position to the head (making it the MRU item).
116
+ # This is a core "touch" operation for an LRU cache.
91
117
  #
92
- # Moves an existing key from its current position to the head (MRU).
93
- # This is a core O(1) LRU "touch" operation.
118
+ # This is an **O(1)** operation.
119
+ #
120
+ # @example
121
+ # lru = DLinked::CacheList.new
122
+ # lru.prepend_key(:a, 1)
123
+ # lru.prepend_key(:b, 2) # List order: [:b, :a]
124
+ # lru.move_to_head_by_key(:a) # "touch" :a
125
+ # lru.to_a # => [1, 2] (node values), key order is now [:a, :b]
94
126
  #
95
127
  # @param key [Object] The key of the item to move.
96
- # @return [true, false] Returns true if the move was successful, false if the key was not found.
128
+ # @return [true, false] `true` if the move was successful, `false` if the key was not found.
97
129
  def move_to_head_by_key(key)
98
130
  node = @lru_nodes[key]
99
131
  return false unless node
100
132
 
101
- # Calls the internal O(1) relocation method
102
- _move_to_head(node)
133
+ _move_to_head(node)
103
134
  true
104
135
  end
105
136
 
106
- # 5. Expose O(1) removal
137
+ # Removes an item from the list and map by its key.
138
+ #
139
+ # This is an **O(1)** operation.
107
140
  #
108
- # Removes an item from the list by its key.
109
- # This is an O(1) operation using the stored node reference.
141
+ # @example
142
+ # lru = DLinked::CacheList.new
143
+ # lru.prepend_key(:a, 1)
144
+ # lru.remove_by_key(:a) # => true
145
+ # lru.empty? # => true
110
146
  #
111
147
  # @param key [Object] The key of the item to remove.
112
- # @return [true, false] Returns true if the key was removed, false if the key was not found.
148
+ # @return [true, false] `true` if removed, `false` if the key was not found.
113
149
  def remove_by_key(key)
114
150
  node = @lru_nodes.delete(key)
115
151
  return false unless node
116
-
117
- # Calls the internal O(1) node deletion method
152
+
118
153
  _remove_node(node)
119
154
  true
120
155
  end
121
156
 
122
- # Clears both the doubly linked list and the internal LRU map.
123
- # @return [void]
157
+ # Clears the list and the internal key map.
158
+ #
159
+ # This is an **O(1)** operation.
160
+ #
161
+ # @return [self]
124
162
  def clear
125
163
  @lru_nodes.clear
126
164
  super
127
165
  end
128
-
166
+
129
167
  # --- PROTECTED / INTERNAL O(1) NODE METHODS ---
130
168
 
131
169
  protected
132
170
 
133
- # New helper: Returns the node that was popped/shifted
134
- # This bypasses the base list's public #pop to allow access to the Node object.
135
- # @return [DLinked::CacheList::Node, nil] The node object from the tail.
171
+ # Pops the tail node and returns the full node object.
172
+ # @return [DLinked::CacheList::Node, nil] The node from the tail.
173
+ # @api protected
136
174
  def list_pop_logic
137
175
  return nil if empty?
138
176
 
@@ -140,43 +178,34 @@ module DLinked
140
178
  @tail = @tail.prev
141
179
  @tail ? @tail.next = nil : @head = nil
142
180
  @size -= 1
143
- return node
181
+ node
144
182
  end
145
183
 
146
- # O(1) Deletion from a known node reference.
147
- # Used internally by {#remove_by_key} and {#_move_to_head}.
184
+ # Deletes a given node reference in O(1) time.
148
185
  # @param node [DLinked::CacheList::Node] The node object to remove.
149
186
  # @return [Object] The value of the removed node.
187
+ # @api protected
150
188
  def _remove_node(node)
151
- # 1. Update the pointers of the surrounding nodes
152
189
  node.prev.next = node.next if node.prev
153
190
  node.next.prev = node.prev if node.next
154
-
155
- # 2. Update list head/tail if necessary
191
+
156
192
  @head = node.next if node == @head
157
193
  @tail = node.prev if node == @tail
158
-
194
+
159
195
  @size -= 1
160
196
  node.value
161
197
  end
162
-
163
- # O(1) Move to Head using O(1) removal and base class prepend logic.
164
- # @param node [DLinked::CacheList::Node] The node object to move to the head.
165
- # @return [void]
198
+
199
+ # Moves a given node reference to the head in O(1) time.
200
+ # @param node [DLinked::CacheList::Node] The node object to move.
201
+ # @api protected
166
202
  def _move_to_head(node)
167
203
  return if node == @head
168
-
169
- # 1. Remove from current spot (O(1))
170
- _remove_node(node)
171
-
172
- # 2. Re-establish forward link and clear backward link for clean insertion
173
- node.next = @head
174
- node.prev = nil
175
204
 
176
- # 3. Use the base class logic to insert at head. Size counter is correct
177
- # since _remove_node decremented and list_prepend_logic increments.
205
+ _remove_node(node)
206
+ node.next = @head
207
+ node.prev = nil
178
208
  list_prepend_logic(node)
179
209
  end
180
-
181
210
  end
182
211
  end