dlinked 0.1.7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 91b4fd6c724736c40143c48dcc7c10a0335926cf8cbafc4e36020d7fd0ca97ed
4
- data.tar.gz: db2963f5328e509eead6dd403ad4cde8cf820115db33861e8ec019c1bc56d1f5
3
+ metadata.gz: 8215705b10ca404e892cc7b1d6571d96ee66aa8521941f42d6e7051582d2372a
4
+ data.tar.gz: bff1d797d11300cdae48d6f7a3623e17bb7e996db7457b270884e5e9312192b6
5
5
  SHA512:
6
- metadata.gz: 5674461e19a17fa60a0d3f3022899be02824bade1390f2b5dc7506c787b6cd4c6abee66269e3af34f0c1cbe7949874ebc56c8101e14d3b32bd3ed0e7bbb931a5
7
- data.tar.gz: 38a9b833f855d57ab85a8ec3a1f74a56a090dab041a00d3618ea789b5b834ecff1a84e2eb6a30facc8d7eee5256f33c88a70df6dbc169ddaf9590d65ee30385b
6
+ metadata.gz: 69e363bbc4d74898aa65673c0f46250633f9448d7c406ecfe089860a2005142905a170f0da5724d3a1766fe4ff70ddbbd2a77653be86b9fab08efd8b23a538a9
7
+ data.tar.gz: 129ab2caf81b5d8fb6d24c67ccad979fff1ce928811341f28104f5b0f851f1d05106783935d2e17bddd1bc9edf953f539332d4cc01131286495bd529a90dc65d
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0,
71
+ available at [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+ [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,88 @@
1
+ # Contributing to DLinked
2
+
3
+ First off, thank you for considering contributing to DLinked! It's people like you that make open source such a great community.
4
+
5
+ We welcome any type of contribution, not just code. You can help with:
6
+ * **Reporting a bug**
7
+ * **Discussing the current state of the code**
8
+ * **Submitting a fix**
9
+ * **Proposing new features**
10
+ * **Becoming a maintainer**
11
+
12
+ ## Getting Started
13
+
14
+ ### Development Environment
15
+
16
+ To get started with the development environment, you'll need:
17
+ * Ruby (see `.ruby-version` for the required version)
18
+ * Bundler
19
+
20
+ 1. **Fork the repository** on GitHub.
21
+ 2. **Clone your fork** locally:
22
+ ```bash
23
+ git clone https://github.com/your-username/dlinked.git
24
+ cd dlinked
25
+ ```
26
+ 3. **Install dependencies** using Bundler:
27
+ ```bash
28
+ bundle install
29
+ ```
30
+
31
+ ### Running Tests
32
+
33
+ This project uses Minitest for testing. To run the full test suite, use the following command:
34
+
35
+ ```bash
36
+ bundle exec rake test
37
+ ```
38
+
39
+ This will also run SimpleCov to generate a code coverage report in the `coverage/` directory. When submitting a pull request, please ensure that your changes are covered by tests and that you maintain or increase the test coverage.
40
+
41
+ ### Code Style
42
+
43
+ This project uses RuboCop to enforce a consistent code style. To check your code for style violations, run:
44
+
45
+ ```bash
46
+ bundle exec rubocop
47
+ ```
48
+
49
+ Many style violations can be automatically fixed by running:
50
+
51
+ ```bash
52
+ bundle exec rubocop -A
53
+ ```
54
+
55
+ Please ensure your code follows the project's style guidelines before submitting a pull request.
56
+
57
+ ## How to Contribute
58
+
59
+ ### Reporting Bugs
60
+
61
+ If you find a bug, please open an issue on GitHub. Please include:
62
+ * A clear and descriptive title.
63
+ * A detailed description of the problem, including steps to reproduce it.
64
+ * The expected behavior and what actually happened.
65
+ * The version of DLinked and Ruby you are using.
66
+
67
+ ### Submitting Changes (Pull Requests)
68
+
69
+ 1. Create a new branch for your changes:
70
+ ```bash
71
+ git checkout -b your-feature-branch-name
72
+ ```
73
+ 2. Make your changes, and add or update tests as needed.
74
+ 3. Ensure the test suite passes (`bundle exec rake test`).
75
+ 4. Ensure your code passes the RuboCop checks (`bundle exec rubocop`).
76
+ 5. Commit your changes with a clear and descriptive commit message.
77
+ 6. Push your branch to your fork on GitHub:
78
+ ```bash
79
+ git push origin your-feature-branch-name
80
+ ```
81
+ 7. Open a pull request from your fork to the main DLinked repository.
82
+ 8. In the pull request description, please explain the problem you are solving and the changes you have made.
83
+
84
+ ## Code of Conduct
85
+
86
+ By participating in this project, you are expected to uphold our [Code of Conduct](CODE_OF_CONDUCT.md).
87
+
88
+ Thank you for your contribution!
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,10 +27,19 @@ 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
+ ```
32
+
33
+ ## Documentation
31
34
 
35
+ To generate the YARD documentation for this project, run:
36
+
37
+ ```bash
38
+ bundle exec yard doc
32
39
  ```
33
40
 
41
+ This will create a `doc/` directory containing the full HTML documentation. You can view it by opening `doc/index.html` in your browser.
42
+
34
43
  ## Usage
35
44
  ### 1. Basic Initialization and O(1) Operations
36
45
  Demonstrate creating the list, adding elements to both ends, and removing them quickly.
@@ -189,6 +198,109 @@ list1.concat(list2)
189
198
  list1.to_a # => [1, 2, 3, 4]
190
199
  ```
191
200
 
201
+
202
+ ### 7. DLinked::CacheList (LRU Cache Utility)
203
+ `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:
204
+ - #prepend_key(key, value) (Add as MRU)
205
+ - #move_to_head_by_key(key) (Touch/Access)
206
+ - #pop_key (Evict LRU)
207
+ - #remove_by_key(key) (Remove)
208
+
209
+ - **Most Recently Used (MRU)** items are at the **head** of the list.
210
+ - **Least Recently Used (LRU)** items are at the **tail** of the list.
211
+
212
+ This makes it highly efficient for tracking key access order in a memory-limited cache.
213
+
214
+ ```ruby
215
+ require 'dlinked'
216
+
217
+ # 1. Initialization
218
+ lru_list = DLinked::CacheList.new
219
+ lru_list.size # => 0
220
+
221
+ # 2. Add keys to the cache (as MRU)
222
+ # In a real cache, the value might be the cached data itself.
223
+ # For key tracking, value can be the same as the key.
224
+ lru_list.prepend_key(:key1, :key1)
225
+ lru_list.prepend_key(:key2, :key2)
226
+ lru_list.prepend_key(:key3, :key3)
227
+
228
+ # List order (MRU to LRU): [:key3, :key2, :key1]
229
+ lru_list.to_a # => [:key3, :key2, :key1]
230
+
231
+ # 3. "Touch" an existing key, moving it to the head (MRU)
232
+ lru_list.move_to_head_by_key(:key1)
233
+
234
+ # List order is now: [:key1, :key3, :key2]
235
+ lru_list.to_a # => [:key1, :key3, :key2]
236
+
237
+ # 4. Evict the least recently used key (from the tail)
238
+ evicted_key = lru_list.pop_key
239
+ evicted_key # => :key2
240
+
241
+ # List order is now: [:key1, :key3]
242
+ lru_list.to_a # => [:key1, :key3]
243
+
244
+ # 5. Remove a specific key (O(1) operation)
245
+ lru_list.remove_by_key(:key3)
246
+ lru_list.to_a # => [:key1]
247
+
248
+ # 6. Clear the list and the key map (O(1) operation)
249
+ lru_list.clear
250
+ lru_list.size # => 0
251
+
252
+ ```
253
+
254
+ ### Real-World Example: A Complete LRU Cache
255
+ While `DLinked::CacheList` provides the low-level, high-performance key tracking, you can easily build a complete, practical `LRUCache` class around it.
256
+
257
+ The following example demonstrates how to combine `DLinked::CacheList` with a `Hash` for data storage to create a fully functional LRU cache.
258
+
259
+ ```ruby
260
+ # A complete, working implementation of a Least Recently Used (LRU) Cache
261
+ # built on top of DLinked::CacheList.
262
+ class LRUCache
263
+ attr_reader :capacity, :size
264
+
265
+ def initialize(capacity)
266
+ raise ArgumentError, 'Capacity must be a positive integer' unless capacity.is_a?(Integer) && capacity > 0
267
+
268
+ @capacity = capacity
269
+ @size = 0
270
+ @list = DLinked::CacheList.new
271
+ @data = {}
272
+ end
273
+
274
+ def get(key)
275
+ return nil unless @data.key?(key)
276
+ @list.move_to_head_by_key(key)
277
+ @data[key]
278
+ end
279
+
280
+ def set(key, value)
281
+ if @data.key?(key)
282
+ @list.move_to_head_by_key(key)
283
+ else
284
+ @list.prepend_key(key, value)
285
+ @size += 1
286
+ evict if @size > @capacity
287
+ end
288
+ @data[key] = value
289
+ end
290
+
291
+ private
292
+
293
+ def evict
294
+ lru_key = @list.pop_key
295
+ return unless lru_key
296
+ @data.delete(lru_key)
297
+ @size -= 1
298
+ end
299
+ end
300
+ ```
301
+
302
+ For a complete, runnable demonstration of this `LRUCache` class in action, see the [lru_cache_example.rb](./lru_cache_example.rb) file.
303
+
192
304
  ## ⚡ Performance Characteristics
193
305
  This library is designed to offer the guaranteed performance benefits of a Doubly Linked List over a standard Ruby `Array` for certain operations.
194
306
 
@@ -202,6 +314,34 @@ This library is designed to offer the guaranteed performance benefits of a Doubl
202
314
 
203
315
 
204
316
 
317
+ ## ⚡ Performance Benchmarks
318
+
319
+ To quantify the performance benefits of `DLinked::List` over Ruby's `Array`, a benchmark suite is available in `benchmark.rb`. It uses the `benchmark-ips` gem to compare the performance of single operations on a pre-filled data structure of 10,000 items.
320
+
321
+ The benchmark measures a single operation (e.g., `shift`) and immediately performs the inverse operation (e.g., `push`) to ensure the list size remains constant for every measurement. This provides a more accurate comparison of how each data structure handles these calls on a large collection.
322
+
323
+ You can run the benchmark yourself:
324
+
325
+ ```bash
326
+ bundle install
327
+ bundle exec ruby benchmark.rb
328
+ ```
329
+
330
+ **Results:**
331
+
332
+ The results below were generated on Ruby 3.1.4. They demonstrate that for a list of 10,000 items, the performance of Ruby's native, C-implemented `Array` is significantly faster than `DLinked::List`'s pure Ruby implementation, even for operations where the linked list has a better theoretical time complexity.
333
+
334
+ | Operation | Comparison | Analysis |
335
+ | :--- | :--- | :--- |
336
+ | `append` / `push` | `Array` is ~5.2x faster | `Array` is faster. This is expected as it's a highly optimized C implementation, while `DLinked::List` has the overhead of Ruby method calls and `Node` object allocation. |
337
+ | `prepend` / `unshift`| `Array` is ~5.1x faster | Surprisingly, `Array#unshift` is still faster at this scale. The cost of memory shifting in C for 10,000 items is lower than the overhead of `DLinked::List`'s Ruby implementation. |
338
+ | `pop` | `Array` is ~5.3x faster | Similar to `push`, the native `Array` implementation is faster for this O(1) operation. |
339
+ | `shift` | `Array` is ~5.4x faster | Like `unshift`, `Array#shift`'s O(n) operation in C is faster than `DLinked::List`'s O(1) operation in Ruby at this list size, due to the overhead of the Ruby implementation. |
340
+
341
+ **Conclusion:**
342
+
343
+ These benchmarks show that the raw speed of the underlying C implementation of `Array` outweighs the Big-O algorithmic advantages of a pure Ruby linked list for collections of this size. `DLinked::List` is better suited for educational purposes or for algorithms where the explicit node structure and pointer manipulation are more important than raw wall-clock performance against `Array`.
344
+
205
345
  ## 💾 Memory Usage
206
346
 
207
347
  While memory usage is highly dependent on the objects stored, the overhead of the list structure itself is minimal and highly efficient:
data/benchmark.rb CHANGED
@@ -1,54 +1,78 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # benchmark.rb - Compare Struct vs Class performance
4
- # Run with: ruby benchmark.rb
5
- require 'benchmark'
3
+ require 'benchmark/ips'
4
+ require 'dlinked'
6
5
 
7
- # Struct version
8
- NodeStruct = Struct.new(:value, :prev, :next)
6
+ puts "Ruby version: #{RUBY_VERSION}"
7
+ puts "DLinked version: #{DLinked::VERSION}"
9
8
 
10
- # Class version
11
- class NodeClass
12
- attr_accessor :value, :prev, :next
9
+ LIST_SIZE = 10_000
10
+ puts "\n--- Benchmarking single operations on a list of #{LIST_SIZE} items ---"
11
+ puts "A single operation is performed, and then undone to maintain list size."
13
12
 
14
- def initialize(value, prev_node, next_node)
15
- @value = value
16
- @prev = prev_node
17
- @next = next_node
13
+ # --- Setup ---
14
+ array = (0...LIST_SIZE).to_a
15
+ list = DLinked::List.new
16
+ array.each { |i| list.append(i) }
17
+
18
+ # --- Benchmark Suite ---
19
+
20
+ puts "\nAppend/Push at the end:"
21
+ Benchmark.ips do |x|
22
+ x.report("Array#push") do
23
+ array.push(0)
24
+ array.pop
18
25
  end
19
- end
20
26
 
21
- N = 2_000_000
27
+ x.report("DLinked::List#append") do
28
+ list.append(0)
29
+ list.pop
30
+ end
22
31
 
23
- puts "Creating and accessing #{N} nodes:\n\n"
32
+ x.compare!
33
+ end
24
34
 
25
- Benchmark.bm(20) do |x|
26
- x.report('Class creation:') do
27
- N.times { |i| NodeClass.new(i, nil, nil) }
35
+ puts "\nPrepend/Unshift at the beginning:"
36
+ Benchmark.ips do |x|
37
+ x.report("Array#unshift") do
38
+ array.unshift(0)
39
+ array.shift
28
40
  end
29
41
 
30
- x.report('Struct creation:') do
31
- N.times { |i| NodeStruct.new(i, nil, nil) }
42
+ x.report("DLinked::List#prepend") do
43
+ list.prepend(0)
44
+ list.shift
32
45
  end
33
46
 
34
- # Test access speed (the important part!)
35
- struct_nodes = Array.new(1000) { |i| NodeStruct.new(i, nil, nil) }
36
- class_nodes = Array.new(1000) { |i| NodeClass.new(i, nil, nil) }
47
+ x.compare!
48
+ end
37
49
 
38
- x.report('Class access:') do
39
- N.times do
40
- node = class_nodes[rand(1000)]
41
- v = node.value
42
- node.value = v + 1
43
- end
50
+ puts "\nPop from the end:"
51
+ Benchmark.ips do |x|
52
+ x.report("Array#pop") do
53
+ el = array.pop
54
+ array.push(el)
44
55
  end
45
- x.report('Struct access:') do
46
- N.times do
47
- node = struct_nodes[rand(1000)]
48
- v = node.value
49
- node.value = v + 1
50
- end
56
+
57
+ x.report("DLinked::List#pop") do
58
+ el = list.pop
59
+ list.append(el)
51
60
  end
61
+
62
+ x.compare!
52
63
  end
53
64
 
54
- puts "\nConclusion: Run this benchmark on your target Ruby version to decide!"
65
+ puts "\nShift from the beginning:"
66
+ Benchmark.ips do |x|
67
+ x.report("Array#shift") do
68
+ el = array.shift
69
+ array.push(el)
70
+ end
71
+
72
+ x.report("DLinked::List#shift") do
73
+ el = list.shift
74
+ list.append(el)
75
+ end
76
+
77
+ x.compare!
78
+ end
data/dlinked.gemspec CHANGED
@@ -38,4 +38,5 @@ Gem::Specification.new do |spec|
38
38
  spec.add_development_dependency "rubocop-minitest", "~> 0.16.0"
39
39
  spec.add_development_dependency "simplecov", "~> 0.22"
40
40
  spec.add_development_dependency "yard", "~> 0.9"
41
+ spec.add_development_dependency "benchmark-ips", "~> 2.8"
41
42
  end
@@ -0,0 +1,211 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DLinked
4
+ # A specialized doubly linked list for managing keys in a Least Recently Used (LRU) cache.
5
+ #
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.
9
+ #
10
+ # The list maintains items in order from Most Recently Used (MRU) at the
11
+ # head to Least Recently Used (LRU) at the tail.
12
+ #
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
44
+ class CacheList < DLinked::List
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.
49
+ attr_accessor :key
50
+
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.
56
+ def initialize(value, prev = nil, next_node = nil, key = nil)
57
+ super(value, prev, next_node)
58
+ @key = key
59
+ end
60
+ end
61
+
62
+ # @!attribute [r] lru_nodes
63
+ # @return [Hash{Object => DLinked::CacheList::Node}] The internal hash map.
64
+ attr_reader :lru_nodes
65
+
66
+ # Initializes a new, empty `CacheList`.
67
+ def initialize
68
+ super
69
+ @lru_nodes = {}
70
+ end
71
+
72
+ # --- LRU Management Methods (O(1)) ---
73
+
74
+ # Adds a new key-value pair to the head of the list (making it the MRU item).
75
+ #
76
+ # This is an **O(1)** operation.
77
+ #
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.
87
+ def prepend_key(key, value)
88
+ raise "Key #{key} already exists in the LRU list." if @lru_nodes.key?(key)
89
+
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
94
+ end
95
+
96
+ # Removes the Least Recently Used (LRU) item from the tail of the list.
97
+ #
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.
107
+ def pop_key
108
+ node = list_pop_logic
109
+ return nil unless node
110
+
111
+ @lru_nodes.delete(node.key)
112
+ node.key
113
+ end
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.
117
+ #
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]
126
+ #
127
+ # @param key [Object] The key of the item to move.
128
+ # @return [true, false] `true` if the move was successful, `false` if the key was not found.
129
+ def move_to_head_by_key(key)
130
+ node = @lru_nodes[key]
131
+ return false unless node
132
+
133
+ _move_to_head(node)
134
+ true
135
+ end
136
+
137
+ # Removes an item from the list and map by its key.
138
+ #
139
+ # This is an **O(1)** operation.
140
+ #
141
+ # @example
142
+ # lru = DLinked::CacheList.new
143
+ # lru.prepend_key(:a, 1)
144
+ # lru.remove_by_key(:a) # => true
145
+ # lru.empty? # => true
146
+ #
147
+ # @param key [Object] The key of the item to remove.
148
+ # @return [true, false] `true` if removed, `false` if the key was not found.
149
+ def remove_by_key(key)
150
+ node = @lru_nodes.delete(key)
151
+ return false unless node
152
+
153
+ _remove_node(node)
154
+ true
155
+ end
156
+
157
+ # Clears the list and the internal key map.
158
+ #
159
+ # This is an **O(1)** operation.
160
+ #
161
+ # @return [self]
162
+ def clear
163
+ @lru_nodes.clear
164
+ super
165
+ end
166
+
167
+ # --- PROTECTED / INTERNAL O(1) NODE METHODS ---
168
+
169
+ protected
170
+
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
174
+ def list_pop_logic
175
+ return nil if empty?
176
+
177
+ node = @tail
178
+ @tail = @tail.prev
179
+ @tail ? @tail.next = nil : @head = nil
180
+ @size -= 1
181
+ node
182
+ end
183
+
184
+ # Deletes a given node reference in O(1) time.
185
+ # @param node [DLinked::CacheList::Node] The node object to remove.
186
+ # @return [Object] The value of the removed node.
187
+ # @api protected
188
+ def _remove_node(node)
189
+ node.prev.next = node.next if node.prev
190
+ node.next.prev = node.prev if node.next
191
+
192
+ @head = node.next if node == @head
193
+ @tail = node.prev if node == @tail
194
+
195
+ @size -= 1
196
+ node.value
197
+ end
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
202
+ def _move_to_head(node)
203
+ return if node == @head
204
+
205
+ _remove_node(node)
206
+ node.next = @head
207
+ node.prev = nil
208
+ list_prepend_logic(node)
209
+ end
210
+ end
211
+ end