dlinked 0.1.7 → 0.1.8

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91b4fd6c724736c40143c48dcc7c10a0335926cf8cbafc4e36020d7fd0ca97ed
4
- data.tar.gz: db2963f5328e509eead6dd403ad4cde8cf820115db33861e8ec019c1bc56d1f5
3
+ metadata.gz: de491a4898093da5aac0b68079ebc40d001a60234ae752ec39250fa3d56d09d2
4
+ data.tar.gz: cdaf5870bba85269d949a0471a8f4481ad1786a18cf1e1f14159439b5d6235f4
5
5
  SHA512:
6
- metadata.gz: 5674461e19a17fa60a0d3f3022899be02824bade1390f2b5dc7506c787b6cd4c6abee66269e3af34f0c1cbe7949874ebc56c8101e14d3b32bd3ed0e7bbb931a5
7
- data.tar.gz: 38a9b833f855d57ab85a8ec3a1f74a56a090dab041a00d3618ea789b5b834ecff1a84e2eb6a30facc8d7eee5256f33c88a70df6dbc169ddaf9590d65ee30385b
6
+ metadata.gz: 57666858cfbccc763cc732022f017fe3739292ec72b58c4c87d699bb65ea25154c5ba04458346fed4fdf53c7fa522801e2ba1c29e8bc53ea880d6d4f31109cea
7
+ data.tar.gz: 2ba4f2c83fff58be2e103968482f402ac8d6c6ce1f60ca5b4256e2e9337b30248afb1de181227b2a102872307d6ea60e4faaa3d30254942b65b9bf9bb6cb92da
data/LICENSE.txt CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  MIT License
3
3
 
4
- Copyright (c) 2025 [Your Name]
4
+ Copyright (c) 2025 [DANIELE FRISANCO]
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -6,9 +6,9 @@ A fast, lightweight doubly linked list implementation for Ruby.
6
6
 
7
7
  - **Lightweight**: Minimal memory footprint using optimized node class
8
8
  - **Fast**: O(1) operations for insertion/deletion at both ends
9
- - **Ruby-native**: Includes Enumerable for full integration with Ruby
9
+ - **Ruby-native**: Includes `Enumerable` for full integration with Ruby
10
10
  - **Bidirectional**: Iterate forward or backward efficiently
11
-
11
+ - **LRU-Ready**: Includes the specialized `DLinked::CacheList` subclass, which integrates a hash map for O(1) LRU cache management (access, insertion, eviction).
12
12
 
13
13
  ## Installation
14
14
 
@@ -27,7 +27,7 @@ gem install dlinked
27
27
  ## Test
28
28
 
29
29
  ```bash
30
- bundle exec ruby test/test_d_linked_list.rb
30
+ bundle exec rake test
31
31
 
32
32
  ```
33
33
 
@@ -189,6 +189,58 @@ list1.concat(list2)
189
189
  list1.to_a # => [1, 2, 3, 4]
190
190
  ```
191
191
 
192
+
193
+ ### 7. DLinked::CacheList (LRU Cache Utility)
194
+ `DLinked::CacheList` is a specialized subclass of `DLinked::List` designed to be the backbone of a **Least Recently Used (LRU) cache**. It combines a doubly linked list with a hash map to provide **O(1)**time complexity for all critical LRU cache operations. All core key management methods have **O(1)** complexity:
195
+ - #prepend_key(key, value) (Add as MRU)
196
+ - #move_to_head_by_key(key) (Touch/Access)
197
+ - #pop_key (Evict LRU)
198
+ - #remove_by_key(key) (Remove)
199
+
200
+ - **Most Recently Used (MRU)** items are at the **head** of the list.
201
+ - **Least Recently Used (LRU)** items are at the **tail** of the list.
202
+
203
+ This makes it highly efficient for tracking key access order in a memory-limited cache.
204
+
205
+ ```ruby
206
+ require 'dlinked'
207
+
208
+ # 1. Initialization
209
+ lru_list = DLinked::CacheList.new
210
+ lru_list.size # => 0
211
+
212
+ # 2. Add keys to the cache (as MRU)
213
+ # In a real cache, the value might be the cached data itself.
214
+ # For key tracking, value can be the same as the key.
215
+ lru_list.prepend_key(:key1, :key1)
216
+ lru_list.prepend_key(:key2, :key2)
217
+ lru_list.prepend_key(:key3, :key3)
218
+
219
+ # List order (MRU to LRU): [:key3, :key2, :key1]
220
+ lru_list.to_a # => [:key3, :key2, :key1]
221
+
222
+ # 3. "Touch" an existing key, moving it to the head (MRU)
223
+ lru_list.move_to_head_by_key(:key1)
224
+
225
+ # List order is now: [:key1, :key3, :key2]
226
+ lru_list.to_a # => [:key1, :key3, :key2]
227
+
228
+ # 4. Evict the least recently used key (from the tail)
229
+ evicted_key = lru_list.pop_key
230
+ evicted_key # => :key2
231
+
232
+ # List order is now: [:key1, :key3]
233
+ lru_list.to_a # => [:key1, :key3]
234
+
235
+ # 5. Remove a specific key (O(1) operation)
236
+ lru_list.remove_by_key(:key3)
237
+ lru_list.to_a # => [:key1]
238
+
239
+ # 6. Clear the list and the key map (O(1) operation)
240
+ lru_list.clear
241
+ lru_list.size # => 0
242
+
243
+ ```
192
244
  ## ⚡ Performance Characteristics
193
245
  This library is designed to offer the guaranteed performance benefits of a Doubly Linked List over a standard Ruby `Array` for certain operations.
194
246
 
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DLinked
4
+ # DLinked::CacheList
5
+ #
6
+ # A specialized doubly linked list subclass used as the central key management
7
+ # component for an in-memory LRU cache.
8
+ #
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.
14
+ #
15
+ # It maintains the standard list behavior of the superclass {DLinked::List}
16
+ # but focuses on key-based operations rather than simple value manipulation.
17
+ class CacheList < DLinked::List
18
+ class Node < DLinked::List::Node
19
+ # Add an attribute to hold the cache key
20
+ attr_accessor :key
21
+
22
+ # IMPORTANT: Accept the required three arguments from the base List#prepend,
23
+ # but only initialize the base class with value, prev, and next.
24
+ 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
29
+ @key = key
30
+ end
31
+ end
32
+ # @!attribute [r] lru_nodes
33
+ # @return [Hash] The internal hash map storing {key => DLinked::CacheList::Node} references.
34
+ attr_reader :lru_nodes
35
+ # # --- 1. OVERRIDE THE CONSTANT ---
36
+ Node = DLinked::CacheList::Node
37
+
38
+
39
+ # Initializes a new CacheList instance.
40
+ # @return [void]
41
+ def initialize
42
+ super
43
+ @lru_nodes = {}
44
+ end
45
+
46
+ # --- LRU Management Methods (O(1)) ---
47
+
48
+ # 2. Expose a key-based prepend method
49
+ #
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.
53
+ #
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.
58
+ def prepend_key(key, value)
59
+ if @lru_nodes.key?(key)
60
+ raise "Key #{key} already exists in the LRU list."
61
+ end
62
+
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
73
+ end
74
+
75
+ # 3. Expose a key-based pop method (for eviction)
76
+ #
77
+ # 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
+ #
80
+ # @return [Object, nil] Returns the key of the removed LRU item, or nil if the list is empty.
81
+ def pop_key
82
+ # O(1) - Base class's pop operation on the tail node
83
+ node = list_pop_logic
84
+ return nil unless node
85
+
86
+ @lru_nodes.delete(node.key)
87
+ return node.key
88
+ end
89
+
90
+ # 4. Expose the O(1) touch operation
91
+ #
92
+ # Moves an existing key from its current position to the head (MRU).
93
+ # This is a core O(1) LRU "touch" operation.
94
+ #
95
+ # @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.
97
+ def move_to_head_by_key(key)
98
+ node = @lru_nodes[key]
99
+ return false unless node
100
+
101
+ # Calls the internal O(1) relocation method
102
+ _move_to_head(node)
103
+ true
104
+ end
105
+
106
+ # 5. Expose O(1) removal
107
+ #
108
+ # Removes an item from the list by its key.
109
+ # This is an O(1) operation using the stored node reference.
110
+ #
111
+ # @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.
113
+ def remove_by_key(key)
114
+ node = @lru_nodes.delete(key)
115
+ return false unless node
116
+
117
+ # Calls the internal O(1) node deletion method
118
+ _remove_node(node)
119
+ true
120
+ end
121
+
122
+ # Clears both the doubly linked list and the internal LRU map.
123
+ # @return [void]
124
+ def clear
125
+ @lru_nodes.clear
126
+ super
127
+ end
128
+
129
+ # --- PROTECTED / INTERNAL O(1) NODE METHODS ---
130
+
131
+ protected
132
+
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.
136
+ def list_pop_logic
137
+ return nil if empty?
138
+
139
+ node = @tail
140
+ @tail = @tail.prev
141
+ @tail ? @tail.next = nil : @head = nil
142
+ @size -= 1
143
+ return node
144
+ end
145
+
146
+ # O(1) Deletion from a known node reference.
147
+ # Used internally by {#remove_by_key} and {#_move_to_head}.
148
+ # @param node [DLinked::CacheList::Node] The node object to remove.
149
+ # @return [Object] The value of the removed node.
150
+ def _remove_node(node)
151
+ # 1. Update the pointers of the surrounding nodes
152
+ node.prev.next = node.next if node.prev
153
+ node.next.prev = node.prev if node.next
154
+
155
+ # 2. Update list head/tail if necessary
156
+ @head = node.next if node == @head
157
+ @tail = node.prev if node == @tail
158
+
159
+ @size -= 1
160
+ node.value
161
+ 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]
166
+ def _move_to_head(node)
167
+ 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
+
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.
178
+ list_prepend_logic(node)
179
+ end
180
+
181
+ end
182
+ end
data/lib/d_linked/list.rb CHANGED
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'list/node'
4
+
4
5
  module DLinked
5
6
  # A fast, lightweight doubly linked list implementation
6
7
  class List
7
8
  include Enumerable
8
9
 
9
10
  # PURE PERFORMANCE: We now use the dedicated Class for Node,
10
- Node = DLinked::List::Node
11
- private_constant :Node
11
+ # Node = DLinked::List::Node
12
12
 
13
13
  attr_reader :size
14
14
  alias length size
@@ -29,11 +29,7 @@ module DLinked
29
29
  # @return [self] Returns the list instance, allowing for method chaining (e.g., list.prepend(1).prepend(2)).
30
30
  def prepend(value)
31
31
  node = Node.new(value, nil, @head)
32
- @head.prev = node if @head
33
- @head = node
34
- @tail ||= node
35
- @size += 1
36
- self
32
+ list_prepend_logic(node)
37
33
  end
38
34
  alias unshift prepend
39
35
 
@@ -45,11 +41,7 @@ module DLinked
45
41
  # @return [DLinked::List] Returns the list instance for method chaining.
46
42
  def append(value)
47
43
  node = Node.new(value, @tail, nil)
48
- @tail.next = node if @tail
49
- @tail = node
50
- @head ||= node
51
- @size += 1
52
- self
44
+ list_append_logic(node)
53
45
  end
54
46
  alias push append
55
47
  alias << append
@@ -366,10 +358,12 @@ module DLinked
366
358
  return prepend(value) if index <= 0
367
359
  return append(value) if index >= @size
368
360
 
361
+ # Find the node to insert BEFORE
369
362
  current = find_node_at_index(index)
370
363
 
371
- # Insert before current node (O(1) linking)
372
364
  new_node = Node.new(value, current.prev, current)
365
+
366
+ # Insert before current node (O(1) linking)
373
367
  current.prev.next = new_node
374
368
  current.prev = new_node
375
369
 
@@ -543,6 +537,24 @@ module DLinked
543
537
  @size -= removed_count
544
538
  result.empty? ? nil : result
545
539
  end
540
+ protected
541
+
542
+ # This method handles the actual pointer manipulation, which is constant across all subclasses
543
+ def list_prepend_logic(node)
544
+ @head.prev = node if @head
545
+ @head = node
546
+ @tail ||= node
547
+ @size += 1
548
+ self
549
+ end
550
+
551
+ def list_append_logic(node)
552
+ @tail.next = node if @tail
553
+ @tail = node
554
+ @head ||= node
555
+ @size += 1
556
+ self
557
+ end
546
558
 
547
559
  private
548
560
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DLinked
4
- VERSION = '0.1.7'
4
+ VERSION = '0.1.8'
5
5
  end
data/lib/dlinked.rb CHANGED
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'd_linked/version'
4
- require_relative 'd_linked/list'
4
+
5
+ require_relative 'd_linked/list'
6
+
7
+ require_relative 'd_linked/cache_list'
5
8
 
6
9
  module DLinked
7
- # The DLinked module serves as the namespace for the gem.
8
- end
10
+ # Namespace definition is fine here, it just re-opens the module.
11
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dlinked
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
4
+ version: 0.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniele Frisanco
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-13 00:00:00.000000000 Z
11
+ date: 2025-12-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -127,6 +127,7 @@ files:
127
127
  - benchmark.rb
128
128
  - dlinked.gemspec
129
129
  - example.rb
130
+ - lib/d_linked/cache_list.rb
130
131
  - lib/d_linked/list.rb
131
132
  - lib/d_linked/list/node.rb
132
133
  - lib/d_linked/version.rb