diff --git a/hw04_lru_cache/cache.go b/hw04_lru_cache/cache.go index 738399e..7ea7aaf 100644 --- a/hw04_lru_cache/cache.go +++ b/hw04_lru_cache/cache.go @@ -1,22 +1,82 @@ package hw04_lru_cache //nolint:golint,stylecheck +import ( + "sync" +) + type Key string type Cache interface { - // Place your code here + Set(key Key, value interface{}) bool + Get(key Key) (interface{}, bool) + Clear() } type lruCache struct { - // Place your code here: - // - capacity - // - queue - // - items + sync.Mutex + capacity int + Queue List + Items map[Key]*listItem } type cacheItem struct { - // Place your code here + cKey Key + cValue interface{} } func NewCache(capacity int) Cache { - return &lruCache{} + return &lruCache{ + capacity: capacity, + Queue: NewList(), + Items: make(map[Key]*listItem), + } +} + +func newItem(key Key, value interface{}) *cacheItem { + return &cacheItem{ + cKey: key, + cValue: value, + } +} + +func (c *lruCache) Set(key Key, value interface{}) bool { + c.Lock() + defer c.Unlock() + + if item, ok := c.Items[key]; ok { + item.Value.(*cacheItem).cValue = value + c.Queue.MoveToFront(item) + return true + } + + for c.Queue.Len() >= c.capacity { + latestItem := c.Queue.Back() + + c.Queue.Remove(latestItem) + delete(c.Items, latestItem.Value.(*cacheItem).cKey) + } + + item := newItem(key, value) + c.Items[key] = c.Queue.PushFront(item) + return false +} + +func (c *lruCache) Get(key Key) (interface{}, bool) { + c.Lock() + defer c.Unlock() + + if item, ok := c.Items[key]; ok { + c.Queue.MoveToFront(item) + return item.Value.(*cacheItem).cValue, true + } + + return nil, false +} + +func (c *lruCache) Clear() { + c.Lock() + defer c.Unlock() + + c.Queue = NewList() + c.Items = make(map[Key]*listItem) } diff --git a/hw04_lru_cache/cache_test.go b/hw04_lru_cache/cache_test.go index e99447d..6a20158 100644 --- a/hw04_lru_cache/cache_test.go +++ b/hw04_lru_cache/cache_test.go @@ -50,16 +50,73 @@ func TestCache(t *testing.T) { }) t.Run("purge logic", func(t *testing.T) { - // Write me + c := NewCache(1) + + wasInCache := c.Set("aaa", 100) + require.False(t, wasInCache) + + wasInCache = c.Set("bbb", 200) + require.False(t, wasInCache) + + val, ok := c.Get("aaa") + require.False(t, ok) + require.Equal(t, nil, val) + + val, ok = c.Get("bbb") + require.True(t, ok) + require.Equal(t, 200, val) + }) + + t.Run("push logic", func(t *testing.T) { + c := NewCache(3) + + c.Set("aaa", 100) + c.Set("bbb", 200) + c.Set("ccc", 300) + + // make aaa and bbb more frequent + c.Get("aaa") + c.Get("bbb") + + c.Set("ddd", 400) + + // check purged value + val, ok := c.Get("ccc") + require.False(t, ok) + require.Equal(t, nil, val) + + // check exist values + val, ok = c.Get("aaa") + require.True(t, ok) + require.Equal(t, 100, val) + + val, ok = c.Get("bbb") + require.True(t, ok) + require.Equal(t, 200, val) + + val, ok = c.Get("ddd") + require.True(t, ok) + require.Equal(t, 400, val) + }) + + t.Run("clear", func(t *testing.T) { + c := NewCache(1) + + c.Set("aaa", 100) + c.Set("bbb", 200) + _, ok := c.Get("aaa") + require.False(t, ok) + + c.Clear() + _, ok = c.Get("bbb") + require.False(t, ok) }) } func TestCacheMultithreading(t *testing.T) { - t.Skip() // Remove if task with asterisk completed - c := NewCache(10) wg := &sync.WaitGroup{} - wg.Add(2) + wg.Add(3) go func() { defer wg.Done() @@ -75,5 +132,12 @@ func TestCacheMultithreading(t *testing.T) { } }() + go func() { + defer wg.Done() + for i := 0; i < 1_000_000; i++ { + c.Clear() + } + }() + wg.Wait() } diff --git a/hw04_lru_cache/go.mod b/hw04_lru_cache/go.mod index b4abc63..fdf7771 100644 --- a/hw04_lru_cache/go.mod +++ b/hw04_lru_cache/go.mod @@ -1,4 +1,4 @@ -module github.com/fixme_my_friend/hw04_lru_cache +module github.com/ezhk/golang-learning/hw04_lru_cache go 1.14 diff --git a/hw04_lru_cache/list.go b/hw04_lru_cache/list.go index eab309f..7e899e9 100644 --- a/hw04_lru_cache/list.go +++ b/hw04_lru_cache/list.go @@ -1,17 +1,105 @@ package hw04_lru_cache //nolint:golint,stylecheck type List interface { - // Place your code here + Len() int // длина списка + Front() *listItem // первый Item + Back() *listItem // последний Item + PushFront(v interface{}) *listItem // добавить значение в начало + PushBack(v interface{}) *listItem // добавить значение в конец + Remove(i *listItem) // удалить элемент + MoveToFront(i *listItem) // переместить элемент в начало } type listItem struct { - // Place your code here + Value interface{} // значение + Next *listItem // следующий элемент + Prev *listItem // предыдущий элемент } type list struct { - // Place your code here + // Length stay for back compatibility when we cannot use map + Length int + + First *listItem + Last *listItem } func NewList() List { return &list{} } + +func (l *list) Len() int { + return l.Length +} + +func (l *list) Front() *listItem { + return l.First +} + +func (l *list) Back() *listItem { + return l.Last +} + +func (l *list) PushFront(v interface{}) *listItem { + switch l.Len() { + case 0: + l.First = &listItem{Value: v} + l.Last = l.First + default: + value := &listItem{ + Value: v, + Next: l.First, + } + l.First.Prev = value + l.First = value + } + + l.Length++ + return l.First +} + +func (l *list) PushBack(v interface{}) *listItem { + switch l.Len() { + case 0: + l.Last = &listItem{Value: v} + l.First = l.Last + default: + value := &listItem{ + Value: v, + Prev: l.Last, + } + l.Last.Next = value + l.Last = value + } + + l.Length++ + return l.Last +} + +func (l *list) Remove(i *listItem) { + switch { + // element in the middle + case i.Prev != nil && i.Next != nil: + i.Prev.Next = i.Next + i.Next.Prev = i.Prev + // element in the right/end + case i.Prev != nil: + l.Last = i.Prev + i.Prev.Next = nil + // element in the left/begin + case i.Next != nil: + l.First = i.Next + i.Next.Prev = nil + // stay only one element + default: + l.First = nil + l.Last = nil + } + + l.Length-- +} + +func (l *list) MoveToFront(i *listItem) { + l.Remove(i) + l.PushFront(i.Value) +} diff --git a/hw04_lru_cache/list_test.go b/hw04_lru_cache/list_test.go index 78e1f57..bc30563 100644 --- a/hw04_lru_cache/list_test.go +++ b/hw04_lru_cache/list_test.go @@ -15,6 +15,45 @@ func TestList(t *testing.T) { require.Nil(t, l.Back()) }) + t.Run("zero or one element", func(t *testing.T) { + l := NewList() + + l.PushFront(10) + require.Equal(t, l.Len(), 1) + // validate internal struct: when last and first are equals + require.Equal(t, l.Front(), l.Back()) + + elems := make([]int, 0, l.Len()) + for i := l.Front(); i != nil; i = i.Next { + elems = append(elems, i.Value.(int)) + } + require.Equal(t, elems, []int{10}) + + l.Remove(l.Front()) + require.Equal(t, l.Len(), 0) + require.Equal(t, l.Front(), (*listItem)(nil)) + require.Equal(t, l.Back(), (*listItem)(nil)) + }) + + t.Run("two elements", func(t *testing.T) { + l := NewList() + l.PushFront("begin") + l.PushBack("end") + require.Equal(t, l.Len(), 2) + + elems := make([]string, 0, l.Len()) + for i := l.Front(); i != nil; i = i.Next { + elems = append(elems, i.Value.(string)) + } + require.Equal(t, elems, []string{"begin", "end"}) + + l.Remove(l.Front()) + require.Equal(t, l.Len(), 1) + + // validate last value + require.Equal(t, l.Back().Value.(string), "end") + }) + t.Run("complex", func(t *testing.T) { l := NewList()