From ac292ced9b9fcc0184be18f678955553dff580de Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Sat, 24 May 2025 09:29:51 -0500 Subject: [PATCH] initial version --- go.mod | 3 + lru.go | 100 ++++++++++++++++++ lru_test.go | 286 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 go.mod create mode 100644 lru.go create mode 100644 lru_test.go diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..84f6c2e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.sharkk.net/Go/LRU + +go 1.24.1 diff --git a/lru.go b/lru.go new file mode 100644 index 0000000..f3db337 --- /dev/null +++ b/lru.go @@ -0,0 +1,100 @@ +package lru + +import ( + "sync" +) + +type node struct { + key, value any + prev, next *node +} + +type LRUCache struct { + capacity int + cache map[any]*node + head *node + tail *node + mu sync.RWMutex +} + +func NewLRUCache(capacity int) *LRUCache { + head := &node{} + tail := &node{} + head.next = tail + tail.prev = head + + return &LRUCache{ + capacity: capacity, + cache: make(map[any]*node, capacity), + head: head, + tail: tail, + } +} + +func (lru *LRUCache) Get(key any) (any, bool) { + lru.mu.Lock() + defer lru.mu.Unlock() + + if node, exists := lru.cache[key]; exists { + lru.moveToHead(node) + return node.value, true + } + return nil, false +} + +func (lru *LRUCache) Put(key, value any) { + lru.mu.Lock() + defer lru.mu.Unlock() + + if node, exists := lru.cache[key]; exists { + node.value = value + lru.moveToHead(node) + return + } + + newNode := &node{key: key, value: value} + lru.cache[key] = newNode + lru.addToHead(newNode) + + if len(lru.cache) > lru.capacity { + removed := lru.removeTail() + delete(lru.cache, removed.key) + } +} + +func (lru *LRUCache) moveToHead(node *node) { + lru.removeNode(node) + lru.addToHead(node) +} + +func (lru *LRUCache) removeNode(node *node) { + node.prev.next = node.next + node.next.prev = node.prev +} + +func (lru *LRUCache) addToHead(node *node) { + node.prev = lru.head + node.next = lru.head.next + lru.head.next.prev = node + lru.head.next = node +} + +func (lru *LRUCache) removeTail() *node { + node := lru.tail.prev + lru.removeNode(node) + return node +} + +func (lru *LRUCache) Len() int { + lru.mu.RLock() + defer lru.mu.RUnlock() + return len(lru.cache) +} + +func (lru *LRUCache) Clear() { + lru.mu.Lock() + defer lru.mu.Unlock() + lru.cache = make(map[any]*node, lru.capacity) + lru.head.next = lru.tail + lru.tail.prev = lru.head +} diff --git a/lru_test.go b/lru_test.go new file mode 100644 index 0000000..cc4f8d2 --- /dev/null +++ b/lru_test.go @@ -0,0 +1,286 @@ +package lru + +import ( + "sync" + "testing" +) + +func TestNewLRUCache(t *testing.T) { + cache := NewLRUCache(5) + if cache.capacity != 5 { + t.Errorf("expected capacity 5, got %d", cache.capacity) + } + if cache.Len() != 0 { + t.Errorf("expected empty cache, got length %d", cache.Len()) + } +} + +func TestPutAndGet(t *testing.T) { + cache := NewLRUCache(3) + + // Test Put and Get + cache.Put("a", 1) + cache.Put("b", 2) + cache.Put("c", 3) + + val, exists := cache.Get("a") + if !exists || val != 1 { + t.Errorf("expected (1, true), got (%v, %v)", val, exists) + } + + val, exists = cache.Get("b") + if !exists || val != 2 { + t.Errorf("expected (2, true), got (%v, %v)", val, exists) + } + + // Test miss + val, exists = cache.Get("z") + if exists { + t.Errorf("expected (nil, false), got (%v, %v)", val, exists) + } +} + +func TestUpdateExistingKey(t *testing.T) { + cache := NewLRUCache(2) + + cache.Put("a", 1) + cache.Put("a", 10) // Update + + val, exists := cache.Get("a") + if !exists || val != 10 { + t.Errorf("expected (10, true), got (%v, %v)", val, exists) + } + + if cache.Len() != 1 { + t.Errorf("expected length 1, got %d", cache.Len()) + } +} + +func TestEviction(t *testing.T) { + cache := NewLRUCache(3) + + cache.Put("a", 1) + cache.Put("b", 2) + cache.Put("c", 3) + cache.Put("d", 4) // Should evict "a" + + _, exists := cache.Get("a") + if exists { + t.Error("expected 'a' to be evicted") + } + + val, exists := cache.Get("b") + if !exists || val != 2 { + t.Errorf("expected (2, true), got (%v, %v)", val, exists) + } +} + +func TestLRUOrdering(t *testing.T) { + cache := NewLRUCache(3) + + cache.Put("a", 1) + cache.Put("b", 2) + cache.Put("c", 3) + + // Access "a" to make it recently used + cache.Get("a") + + // Add "d", should evict "b" (least recently used) + cache.Put("d", 4) + + _, exists := cache.Get("b") + if exists { + t.Error("expected 'b' to be evicted") + } + + // Verify others still exist + val, exists := cache.Get("a") + if !exists || val != 1 { + t.Errorf("expected (1, true), got (%v, %v)", val, exists) + } + + val, exists = cache.Get("c") + if !exists || val != 3 { + t.Errorf("expected (3, true), got (%v, %v)", val, exists) + } + + val, exists = cache.Get("d") + if !exists || val != 4 { + t.Errorf("expected (4, true), got (%v, %v)", val, exists) + } +} + +func TestUpdateMakesRecent(t *testing.T) { + cache := NewLRUCache(3) + + cache.Put("a", 1) + cache.Put("b", 2) + cache.Put("c", 3) + + // Update "a" to make it recently used + cache.Put("a", 10) + + // Add "d", should evict "b" + cache.Put("d", 4) + + _, exists := cache.Get("b") + if exists { + t.Error("expected 'b' to be evicted") + } + + val, exists := cache.Get("a") + if !exists || val != 10 { + t.Errorf("expected (10, true), got (%v, %v)", val, exists) + } +} + +func TestClear(t *testing.T) { + cache := NewLRUCache(3) + + cache.Put("a", 1) + cache.Put("b", 2) + cache.Put("c", 3) + + cache.Clear() + + if cache.Len() != 0 { + t.Errorf("expected empty cache after clear, got length %d", cache.Len()) + } + + _, exists := cache.Get("a") + if exists { + t.Error("expected cache to be empty after clear") + } +} + +func TestSingleCapacity(t *testing.T) { + cache := NewLRUCache(1) + + cache.Put("a", 1) + cache.Put("b", 2) // Should evict "a" + + _, exists := cache.Get("a") + if exists { + t.Error("expected 'a' to be evicted") + } + + val, exists := cache.Get("b") + if !exists || val != 2 { + t.Errorf("expected (2, true), got (%v, %v)", val, exists) + } +} + +func TestDifferentTypes(t *testing.T) { + cache := NewLRUCache(3) + + // Test with different key/value types + cache.Put(1, "one") + cache.Put("two", 2) + cache.Put(3.14, []int{1, 2, 3}) + + val, exists := cache.Get(1) + if !exists || val != "one" { + t.Errorf("expected ('one', true), got (%v, %v)", val, exists) + } + + val, exists = cache.Get("two") + if !exists || val != 2 { + t.Errorf("expected (2, true), got (%v, %v)", val, exists) + } + + val, exists = cache.Get(3.14) + if !exists { + t.Errorf("expected slice value, got (%v, %v)", val, exists) + } +} + +func TestConcurrentAccess(t *testing.T) { + cache := NewLRUCache(100) + var wg sync.WaitGroup + + // Concurrent puts + for i := range 50 { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.Put(i, i*10) + }(i) + } + + // Concurrent gets + for i := range 50 { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.Get(i) + }(i) + } + + // Concurrent updates + for i := range 25 { + wg.Add(1) + go func(i int) { + defer wg.Done() + cache.Put(i, i*100) + }(i) + } + + // Concurrent len checks + for range 10 { + wg.Add(1) + go func() { + defer wg.Done() + cache.Len() + }() + } + + // Concurrent clear + wg.Add(1) + go func() { + defer wg.Done() + cache.Clear() + }() + + wg.Wait() +} + +func TestRaceCondition(t *testing.T) { + cache := NewLRUCache(10) + done := make(chan bool) + + go func() { + for i := range 1000 { + cache.Put(i%10, i) + } + done <- true + }() + + go func() { + for i := range 1000 { + cache.Get(i % 10) + } + done <- true + }() + + <-done + <-done +} + +func BenchmarkPut(b *testing.B) { + cache := NewLRUCache(1000) + + for i := 0; b.Loop(); i++ { + cache.Put(i%1000, i) + } +} + +func BenchmarkGet(b *testing.B) { + cache := NewLRUCache(1000) + for i := range 1000 { + cache.Put(i, i) + } + + for i := 0; b.Loop(); i++ { + cache.Get(i % 1000) + } +}