diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 1c5b486..e7dca6a 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -2,7 +2,7 @@ name: Go on: push: - branches: [ master ] + branches: [ master develop ] pull_request: branches: [ master ] @@ -12,25 +12,19 @@ jobs: name: Build runs-on: ubuntu-latest steps: - - name: Set up Go 1.x uses: actions/setup-go@v2 with: - go-version: ^1.13 + go-version: '1.18-beta1' - name: Check out code into the Go module directory uses: actions/checkout@v2 - - name: Get dependencies - run: | - go get -v -t -d ./... - if [ -f Gopkg.toml ]; then - curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh - dep ensure - fi - - name: Build run: go build -v . - name: Test - run: go test -v . + run: go test -race -v . + + - name: Lint + uses: golangci/golangci-lint-action@v3.2.0 \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..bab3ef7 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,84 @@ +# v1.47.2 +# Please don't remove the first line. It uses in CI to determine the golangci version +run: + deadline: 5m + +issues: + # Maximum issues count per one linter. Set to 0 to disable. Default is 50. + max-issues-per-linter: 0 + # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. + max-same-issues: 0 + + # We want to try and improve the comments in the k6 codebase, so individual + # non-golint items from the default exclusion list will gradually be addded + # to the exclude-rules below + exclude-use-default: false + + exclude-rules: + # Exclude duplicate code and function length and complexity checking in test + # files (due to common repeats and long functions in test code) + - path: _(test|gen)\.go + linters: + - cyclop + - dupl + - gocognit + - funlen + - lll + - linters: + - paralleltest # false positive: https://github.com/kunwardeep/paralleltest/issues/8. + text: "does not use range value in test Run" + +linters-settings: + nolintlint: + # Disable to ensure that nolint directives don't have a leading space. Default is true. + allow-leading-space: false + exhaustive: + default-signifies-exhaustive: true + govet: + check-shadowing: true + cyclop: + max-complexity: 25 + maligned: + suggest-new: true + dupl: + threshold: 150 + goconst: + min-len: 10 + min-occurrences: 4 + funlen: + lines: 80 + statements: 60 + +linters: + enable-all: true + disable: + - nlreturn + - gci + - gochecknoinits + - godot + - godox + - gomodguard + - testpackage + - wsl + - gomnd + - goerr113 # most of the errors here are meant for humans + - goheader + - exhaustivestruct + - thelper + - gocyclo # replaced by cyclop since it also calculates the package complexity + - maligned # replaced by govet 'fieldalignment' + - interfacer # deprecated + - scopelint # deprecated, replaced by exportloopref + - wrapcheck # a little bit too much for k6, maybe after https://github.com/tomarrell/wrapcheck/issues/2 is fixed + - golint # this linter is deprecated + - varnamelen + - ireturn + - tagliatelle + - exhaustruct + - execinquery + - maintidx + - grouper + - decorder + - nonamedreturns + - nosnakecase + fast: false diff --git a/README.md b/README.md index de7ec01..4796905 100644 --- a/README.md +++ b/README.md @@ -1,191 +1,270 @@ -lane -==== +# Lane -Lane package provides queue, priority queue, stack and deque data structures -implementations. Its was designed with simplicity, performance, and concurrent -usage in mind. +[![MIT License](https://img.shields.io/badge/License-MIT-green.svg)](https://choosealicense.com/licenses/mit/) +[![Build Status](https://github.com/oleiade/lane/actions/workflows/go.yml/badge.svg)](https://github.com/oleiade/lane/actions/workflows/go.yml) +[![Go Documentation](https://pkg.go.dev/badge/github.com/oleiade/lane#pkg-types.svg)](https://pkg.go.dev/github.com/oleiade/lane#pkg-types) +[![Go Report Card](https://goreportcard.com/badge/github.com/oleiade/lane)](https://goreportcard.com/report/github.com/oleiade/lane) +![Go Version](https://img.shields.io/github/go-mod/go-version/oleiade/lane) +The Lane package provides implementations of generic `Queue`, `PriorityQueue`, `Stack`, and `Deque` data structures. It was designed with simplicity, performance, and concurrent usage in mind. -#### Priority Queue + -Pqueue is a *heap priority queue* data structure implementation. It can be whether max or min ordered, is synchronized and is safe for concurrent operations. It performs insertion and max/min removal in O(log N) time. +- [Lane](#lane) + - [Installation](#installation) + - [Using `v2` releases](#using-v2-releases) + - [Using `v1` releases](#using-v1-releases) + - [Usage/Examples](#usageexamples) + - [Priority Queue](#priority-queue) + - [Example](#example) + - [Deque](#deque) + - [Deque Example](#deque-example) + - [Queue](#queue) + - [Queue Example](#queue-example) + - [Stack](#stack) + - [Stack Example](#stack-example) + - [Performance](#performance) + - [Documentation](#documentation) + - [License](#license) -##### Example +## Installation -```go - // Let's create a new max ordered priority queue - var priorityQueue *PQueue = NewPQueue(MINPQ) +Using this package requires a working Go environment. [See the install instructions for Go](http://golang.org/doc/install.html). - // And push some prioritized content into it - priorityQueue.Push("easy as", 3) - priorityQueue.Push("123", 2) - priorityQueue.Push("do re mi", 4) - priorityQueue.Push("abc", 1) +Go Modules are required when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). - // Now let's take a look at the min element in - // the priority queue - headValue, headPriority := priorityQueue.Head() - fmt.Println(headValue) // "abc" - fmt.Println(headPriority) // 1 +### Using `v2` releases - // Okay the song order seems to be preserved, let's - // roll - var jacksonFive []string = make([]string, priorityQueue.Size()) +```bash +go get github.com/oleiade/lane/v2 +``` - for i := 0; i < len(jacksonFive); i++ { - value, _ := priorityQueue.Pop() +```go +... +import ( + "github.com/oleiade/lane/v2" // imports as package "cli" +) +... +``` - jacksonFive[i] = value.(string) - } +### Using `v1` releases - fmt.Println(strings.Join(jacksonFive, " ")) +```bash +go get github.com/oleiade/lane ``` -#### Deque +```go +... +import ( + "github.com/oleiade/lane" +) +... +``` -Deque is a *head-tail linked list data* structure implementation. It is based on a doubly-linked list container, so that every operations time complexity is O(1). All operations over an instiated Deque are synchronized and safe for concurrent usage. +## Usage/Examples -Deques can optionally be created with a limited capacity, whereby the return value of the `Append` and `Prepend` return false if the Deque was full and the item was not added. +### Priority Queue -##### Example +`PriorityQueue` is a _heap priority queue_ data structure implementation. It can be either maximum (descending) or minimum (ascending) oriented (ordered). Every operation on a `PriorityQueue` are synchronized and goroutine-safe. It performs `Push` and `Pop` operations in `O(log N)` time. + +#### Example ```go - // Let's create a new deque data structure - var deque *Deque = NewDeque() - - // And push some content into it using the Append - // and Prepend methods - deque.Append("easy as") - deque.Prepend("123") - deque.Append("do re mi") - deque.Prepend("abc") - - // Now let's take a look at what are the first and - // last element stored in the Deque - firstValue := deque.First() - lastValue := deque.Last() - fmt.Println(firstValue) // "abc" - fmt.Println(lastValue) // 1 - - // Okay now let's play with the Pop and Shift - // methods to bring the song words together - var jacksonFive []string = make([]string, deque.Size()) - - for i := 0; i < len(jacksonFive); i++ { - value := deque.Shift() - jacksonFive[i] = value.(string) - } - - // abc 123 easy as do re mi - fmt.Println(strings.Join(jacksonFive, " ")) +// Let's create a new max ordered priority queue +priorityQueue := NewMaxPriorityQueue[string]() + +// And push some prioritized content into it +priorityQueue.Push("easy as", 3) +priorityQueue.Push("123", 2) +priorityQueue.Push("do re mi", 4) +priorityQueue.Push("abc", 1) + +// Now let's take a look at the min element in +// the priority queue +headValue, headPriority, ok := priorityQueue.Head() +if okay { + fmt.Println(headValue) // "abc" + fmt.Println(headPriority) // 1 +} + +// okay, the song order seems to be preserved; let's +// roll +var jacksonFive []string = make([]string, priorityQueue.Size()) + +for i := 0; i < len(jacksonFive); i++ { + value, _, _ := priorityQueue.Pop() + jacksonFive[i] = value +} + +fmt.Println(strings.Join(jacksonFive, " ")) ``` -```go - // Let's create a new musical quartet - quartet := NewCappedDeque(4) +### Deque - // List of young hopeful musicians - musicians := []string{"John", "Paul", "George", "Ringo", "Stuart"} +Deque is a _head-tail linked list data_ structure implementation. It is built upon a doubly-linked list container, and every operation on a `Deque` are performed in `O(1)` time complexity. Every operation on a `Deque` is synchronized and goroutine-safe. - // Add as many of them to the band as we can. - for _, name := range musicians { - if quartet.Append(name) { - fmt.Printf("%s is in the band!\n", name) - } else { - fmt.Printf("Sorry - %s is not in the band.\n", name) - } - } +Deques can optionally be instantiated with a limited capacity using the dedicated `NewBoundDeque` constructor, whereby the return value of the `Append` and `Prepend` return false if the Deque was full and the item was not added. - // Assemble our new rock sensation - var beatles = make([]string, quartet.Size()) +#### Deque Example - for i := 0; i < len(beatles); i++ { - beatles[i] = quartet.Shift().(string) - } +```go +// Let's create a new deque data structure +deque := NewDeque[string]() + +// And push some content into it using the Append +// and Prepend methods +deque.Append("easy as") +deque.Prepend("123") +deque.Append("do re mi") +deque.Prepend("abc") + +// Now let's take a look at what are the first and +// last element stored in the Deque +firstValue, ok := deque.First() +if okay { + fmt.Println(firstValue) // "abc" +} + +lastValue, ok := deque.Last() +if ok { + fmt.Println(lastValue) // 1 +} + +//okay, now let's play with the Pop and Shift +// methods to bring the song words together +var jacksonFive []string = make([]string, deque.Size()) + +for i := 0; i < len(jacksonFive); i++ { + value, ok := deque.Shift() + if okay { + jacksonFive[i] = value + } +} - fmt.Println("The Beatles are:", strings.Join(beatles, ", ")) +// abc 123 easy as do re mi +fmt.Println(strings.Join(jacksonFive, " ")) ``` -#### Queue +### Queue -Queue is a **FIFO** ( *First in first out* ) data structure implementation. It is based on a deque container and focuses its API on core functionalities: Enqueue, Dequeue, Head, Size, Empty. Every operations time complexity is O(1). As it is implemented using a Deque container, every operations over an instiated Queue are synchronized and safe for concurrent usage. +`Queue` is a **FIFO** (_First In First Out_) data structure implementation. It is built upon a `Deque` container and focuses its API on core functionalities: `Enqueue`, `Dequeue`, `Head`. Every operation's time complexity is O(1). Every operation on a `Queue` is synchronized and goroutine-safe. -##### Example +#### Queue Example ```go - import ( - "fmt" - "github.com/oleiade/lane" - "sync" - ) - - func worker(item interface{}, wg *sync.WaitGroup) { - fmt.Println(item) - wg.Done() - } +import ( +"fmt" +"github.com/oleiade/lane/v2" +"sync" +) +func worker(item interface{}, wg *sync.WaitGroup) { + fmt.Println(item) + wg.Done() +} - func main() { - queue := lane.NewQueue() - queue.Enqueue("grumpyClient") - queue.Enqueue("happyClient") - queue.Enqueue("ecstaticClient") +func main() { + // Create a new queue and pretend we're handling starbucks + // clients + queue := NewQueue[string]() - var wg sync.WaitGroup + // Let's add the incoming clients to the queue + queue.Enqueue("grumpyClient") + queue.Enqueue("happyClient") + queue.Enqueue("ecstaticClient") - // Let's handle the clients asynchronously - for queue.Head() != nil { - item := queue.Dequeue() + fmt.Println(queue.Head()) // grumpyClient - wg.Add(1) - go worker(item, &wg) - } - - // Wait until everything is printed - wg.Wait() + // Let's handle the clients asynchronously + for client, okay:= queue.Dequeue(); ok; { + go fmt.Println(client) } + + // Wait until everything is printed + wg.Wait() +} ``` -#### Stack +### Stack -Stack is a **LIFO** ( *Last in first out* ) data structure implementation. It is based on a deque container and focuses its API on core functionalities: Push, Pop, Head, Size, Empty. Every operations time complexity is O(1). As it is implemented using a Deque container, every operations over an instiated Stack are synchronized and safe for concurrent usage. +`Stack` is a **LIFO** ( _Last in first out_ ) data structure implementation. It is built upon a `Deque` container and focuses its API on core functionalities: `Push`, `Pop`, `Head`. Every operation time complexity is O(1). Every operation on a `Stack` is synchronized and goroutine-safe. -##### Example +#### Stack Example ```go - // Create a new stack and put some plates over it - var stack *Stack = NewStack() - - // Let's put some plates on the stack - stack.Push("redPlate") - stack.Push("bluePlate") - stack.Push("greenPlate") - - fmt.Println(stack.Head) // greenPlate - - // What's on top of the stack? - value := stack.Pop() - fmt.Println(value.(string)) // greenPlate - - stack.Push("yellowPlate") - value = stack.Pop() - fmt.Println(value.(string)) // yellowPlate - - // What's on top of the stack? - value = stack.Pop() - fmt.Println(value.(string)) // bluePlate - - // What's on top of the stack? - value = stack.Pop() - fmt.Println(value.(string)) // redPlate +// Create a new stack and put some plates over it +stack := NewStack[string]() + +// Let's put some plates on the stack +stack.Push("redPlate") +stack.Push("bluePlate") +stack.Push("greenPlate") + +fmt.Println(stack.Head()) // greenPlate + +// What's on top of the stack? +value, okay:= stack.Pop() +if okay { + fmt.Println(value) // greenPlate +} + +stack.Push("yellowPlate") + +value, ok = stack.Pop() +if okay { + fmt.Println(value) // yellowPlate +} + +// What's on top of the stack? +value, ok = stack.Pop() +if okay { + fmt.Println(value) // bluePlate +} + +// What's on top of the stack? +value, ok = stack.Pop() +if okay { + fmt.Println(value) // redPlate +} ``` +## Performance + +```bash +go test -bench=. +goos: darwin +goarch: arm64 +pkg: github.com/oleiade/lane/v2 +BenchmarkDequeAppend-8 22954384 54.44 ns/op 32 B/op 1 allocs/op +BenchmarkDequePrepend-8 25097098 44.59 ns/op 32 B/op 1 allocs/op +BenchmarkDequePop-8 63403720 18.99 ns/op 0 B/op 0 allocs/op +BenchmarkDequeShift-8 63390186 20.88 ns/op 0 B/op 0 allocs/op +BenchmarkDequeFirst-8 86662152 13.76 ns/op 0 B/op 0 allocs/op +BenchmarkDequeLast-8 85955014 13.76 ns/op 0 B/op 0 allocs/op +BenchmarkDequeSize-8 86614975 13.77 ns/op 0 B/op 0 allocs/op +BenchmarkDequeEmpty-8 86893297 14.22 ns/op 0 B/op 0 allocs/op +BenchmarkBoundDequeFull-8 590152324 2.039 ns/op 0 B/op 0 allocs/op +BenchmarkBoundDequeAppend-8 64457216 18.50 ns/op 0 B/op 0 allocs/op +BenchmarkBoundDeque-8 64631377 18.48 ns/op 0 B/op 0 allocs/op +BenchmarkPriorityQueuePush-8 19994029 65.67 ns/op 72 B/op 1 allocs/op +BenchmarkPriorityQueuePop-8 62751249 18.52 ns/op 0 B/op 0 allocs/op +BenchmarkPriorityQueueHead-8 86090420 13.77 ns/op 0 B/op 0 allocs/op +BenchmarkPriorityQueueSize-8 86768415 13.77 ns/op 0 B/op 0 allocs/op +BenchmarkPriorityQueueEmpty-8 87036146 13.76 ns/op 0 B/op 0 allocs/op +BenchmarkNewQueue-8 19570003 60.36 ns/op +BenchmarkQueueEnqueue-8 25482283 46.63 ns/op 32 B/op 1 allocs/op +BenchmarkQueueDequeue-8 63715965 18.50 ns/op 0 B/op 0 allocs/op +BenchmarkQueueHead-8 85664312 13.79 ns/op 0 B/op 0 allocs/op +BenchmarkNewStack-8 19925473 59.57 ns/op +BenchmarkStackPop-8 64704993 18.80 ns/op 0 B/op 0 allocs/op +BenchmarkStackHead-8 86119761 13.76 ns/op 0 B/op 0 allocs/op +``` ## Documentation -For a more detailled overview of lane, please refer to [Documentation](http://godoc.org/github.com/oleiade/lane) - +For a more detailed overview of lane, please refer to [Documentation](http://godoc.org/github.com/oleiade/lane) -[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/oleiade/lane/trend.png)](https://bitdeli.com/free "Bitdeli Badge") +## License +[MIT](https://choosealicense.com/licenses/mit/) diff --git a/deque.go b/deque.go index e802105..9e1f719 100644 --- a/deque.go +++ b/deque.go @@ -1,148 +1,200 @@ package lane import ( - "container/list" "sync" ) +// Dequer is the interface that wraps the basic Deque operations. +type Dequer[T any] interface { + Deque[T] | BoundDeque[T] +} + // Deque is a head-tail linked list data structure implementation. -// It is based on a doubly linked list container, so that every -// operations time complexity is O(1). // -// every operations over an instiated Deque are synchronized and -// safe for concurrent usage. -type Deque struct { +// The Deque's implementation is built upon a doubly linked list +// container, so that every operations' time complexity is O(1) (N.B: +// linked-list are not CPU-cache friendly). +// Every operation on a Deque are goroutine-safe and ready +// for concurrent usage. +type Deque[T any] struct { sync.RWMutex - container *list.List - capacity int -} -// NewDeque creates a Deque. -func NewDeque() *Deque { - return NewCappedDeque(-1) + // container is the underlying storage container + // of deque's elements. + container *List[T] } -// NewCappedDeque creates a Deque with the specified capacity limit. -func NewCappedDeque(capacity int) *Deque { - return &Deque{ - container: list.New(), - capacity: capacity, +// NewDeque produces a new Deque instance. +func NewDeque[T any](items ...T) *Deque[T] { + container := New[T]() + + for _, item := range items { + container.PushBack(item) } + + return &Deque[T]{ + container: container, + } +} + +// Append inserts item at the back of the Deque in a O(1) time complexity. +func (d *Deque[T]) Append(item T) { + d.Lock() + defer d.Unlock() + + d.container.PushBack(item) } -// Append inserts element at the back of the Deque in a O(1) time complexity, -// returning true if successful or false if the deque is at capacity. -func (s *Deque) Append(item interface{}) bool { - s.Lock() - defer s.Unlock() +// Prepend inserts item at the Deque's front in a O(1) time complexity. +func (d *Deque[T]) Prepend(item T) { + d.Lock() + defer d.Unlock() + + d.container.PushFront(item) +} - if s.capacity < 0 || s.container.Len() < s.capacity { - s.container.PushBack(item) - return true +// Pop removes and returns the back element of the Deque in O(1) time complexity. +func (d *Deque[T]) Pop() (item T, ok bool) { + d.Lock() + defer d.Unlock() + + lastElement := d.container.Back() + if lastElement != nil { + item = d.container.Remove(lastElement) + ok = true } - return false + return } -// Prepend inserts element at the Deques front in a O(1) time complexity, -// returning true if successful or false if the deque is at capacity. -func (s *Deque) Prepend(item interface{}) bool { - s.Lock() - defer s.Unlock() +// Shift removes and returns the front element of the Deque in O(1) time complexity. +func (d *Deque[T]) Shift() (item T, ok bool) { + d.Lock() + defer d.Unlock() - if s.capacity < 0 || s.container.Len() < s.capacity { - s.container.PushFront(item) - return true + firstElement := d.container.Front() + if firstElement != nil { + item = d.container.Remove(firstElement) + ok = true } - return false + return } -// Pop removes the last element of the deque in a O(1) time complexity -func (s *Deque) Pop() interface{} { - s.Lock() - defer s.Unlock() +// First returns the first value stored in the Deque in O(1) time complexity. +func (d *Deque[T]) First() (item T, ok bool) { + d.RLock() + defer d.RUnlock() + + frontItem := d.container.Front() + if frontItem != nil { + item = frontItem.Value + ok = true + } + + return +} - var item interface{} = nil - var lastContainerItem *list.Element = nil +// Last returns the last value stored in the Deque in O(1) time complexity. +func (d *Deque[T]) Last() (item T, ok bool) { + d.RLock() + defer d.RUnlock() - lastContainerItem = s.container.Back() - if lastContainerItem != nil { - item = s.container.Remove(lastContainerItem) + if backItem := d.container.Back(); backItem != nil { + item = backItem.Value + ok = true } - return item + return } -// Shift removes the first element of the deque in a O(1) time complexity -func (s *Deque) Shift() interface{} { - s.Lock() - defer s.Unlock() +// Size returns the Deque's size. +func (d *Deque[T]) Size() uint { + d.RLock() + defer d.RUnlock() - var item interface{} = nil - var firstContainerItem *list.Element = nil + return d.container.Len() +} - firstContainerItem = s.container.Front() - if firstContainerItem != nil { - item = s.container.Remove(firstContainerItem) - } +// Empty checks if the deque is empty. +func (d *Deque[T]) Empty() bool { + d.RLock() + defer d.RUnlock() - return item + return d.container.Len() == 0 } -// First returns the first value stored in the deque in a O(1) time complexity -func (s *Deque) First() interface{} { - s.RLock() - defer s.RUnlock() +// Capaciter is an interface type providing operations +// related to capacity management. +type Capaciter interface { + // Capacity returns the current capacity of the underlying type implementation. + Capacity() int - item := s.container.Front() - if item != nil { - return item.Value - } else { - return nil - } + // IsFull returns whether the implementing type instance is full. + IsFull() bool } -// Last returns the last value stored in the deque in a O(1) time complexity -func (s *Deque) Last() interface{} { - s.RLock() - defer s.RUnlock() +// BoundDeque is a head-tail linked list data structure implementation +// with a user-defined capacity: any operation leading to the size +// of the container to overflow its capacity will fail. +// +// The BoundDeque's implementation is built upon a doubly linked list +// container, so that every operations' time complexity is O(1) (N.B: +// linked-list are not CPU-cache friendly). +// Every operation on a BoundDeque are goroutine-safe and ready +// for concurrent usage. +type BoundDeque[T any] struct { + Deque[T] + + // capacity defines an upper bound limit for the BoundDeque's size. + capacity uint +} - item := s.container.Back() - if item != nil { - return item.Value - } else { - return nil +// NewBoundDeque produces a new BoundDeque instance with the provided +// capacity. +func NewBoundDeque[T any](capacity uint, values ...T) *BoundDeque[T] { + return &BoundDeque[T]{ + Deque: *NewDeque(values...), + capacity: capacity, } } -// Size returns the actual deque size -func (s *Deque) Size() int { - s.RLock() - defer s.RUnlock() - - return s.container.Len() +// Capacity returns the BoundDeque's capacity. +func (bd *BoundDeque[T]) Capacity() uint { + return bd.capacity } -// Capacity returns the capacity of the deque, or -1 if unlimited -func (s *Deque) Capacity() int { - s.RLock() - defer s.RUnlock() - return s.capacity +// Full checks if the BoundDeque is full. +func (bd *BoundDeque[T]) Full() bool { + return bd.container.Len() >= bd.capacity } -// Empty checks if the deque is empty -func (s *Deque) Empty() bool { - s.RLock() - defer s.RUnlock() +// Append inserts item at the back of the BoundDeque in a O(1) time complexity. +// If the BoundDeque's capacity disallows the insertion, Append returns false. +func (bd *BoundDeque[T]) Append(item T) bool { + bd.Lock() + defer bd.Unlock() + + if bd.Full() { + return false + } + + bd.container.PushBack(item) - return s.container.Len() == 0 + return true } -// Full checks if the deque is full -func (s *Deque) Full() bool { - s.RLock() - defer s.RUnlock() +// Prepend inserts item at the BoundDeque's front in a O(1) time complexity. +// If the BoundDeque's capacity disallows the insertion, Prepend returns false. +func (bd *BoundDeque[T]) Prepend(item T) bool { + bd.Lock() + defer bd.Unlock() + + if bd.Full() { + return false + } + + bd.container.PushFront(item) - return s.capacity >= 0 && s.container.Len() >= s.capacity + return true } diff --git a/deque_test.go b/deque_test.go index 86d2379..f8faedd 100644 --- a/deque_test.go +++ b/deque_test.go @@ -1,383 +1,663 @@ package lane import ( - "strconv" "testing" + + "github.com/stretchr/testify/assert" ) func TestDequeAppend(t *testing.T) { - deque := NewDeque() - sampleSize := 100 - - // Append elements in the Deque and assert it does not fail - for i := 0; i < sampleSize; i++ { - var value string = strconv.Itoa(i) - var ok bool = deque.Append(value) - - assert( - t, - ok == true, - "deque.Append(%d) = %t; want %t", i, ok, true, - ) - } - - assert( - t, - deque.container.Len() == sampleSize, - "deque.container.Len() = %d; want %d", deque.container.Len(), sampleSize, - ) - - assert( - t, - deque.container.Front().Value == "0", - "deque.container.Front().Value = %s; want %s", deque.container.Front().Value, "0", - ) - - assert( - t, - deque.container.Back().Value == "99", - "deque.container.Back().Value = %s; want %s", deque.container.Back().Value, "99", - ) + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + appendValue int + wantContainerLen uint + wantContainerBack bool + wantContainerBackValue int + }{ + { + desc: "append to empty deque inserts value", + deque: NewDeque[int](), + appendValue: 42, + wantContainerLen: 1, + wantContainerBack: true, + wantContainerBackValue: 42, + }, + { + desc: "append inserts value at the back", + deque: NewDeque([]int{40, 41}...), + appendValue: 42, + wantContainerLen: 3, + wantContainerBack: true, + wantContainerBackValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + tC.deque.Append(tC.appendValue) + gotContainerLen := tC.deque.container.Len() + gotContainerBack := tC.deque.container.Back() + + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerBack, gotContainerBack != nil) + + if tC.wantContainerBack { + assert.Equal(t, tC.wantContainerBackValue, gotContainerBack.Value) + } + }) + } } -func TestDequeAppendWithCapacity(t *testing.T) { - dequeSize := 20 - deque := NewCappedDeque(dequeSize) - - // Append the maximum number of elements in the Deque - // and assert it does not fail - for i := 0; i < dequeSize; i++ { - var value string = strconv.Itoa(i) - var ok bool = deque.Append(value) - - assert( - t, - ok == true, - "deque.Append(%d) = %t; want %t", i, ok, true, - ) - } - - // Try to overflow the Deque size limit, and make - // sure appending fails - var ok bool = deque.Append("should not be ok") - assert( - t, - ok == false, - "deque.Append(%s) = %t; want %t", "should not be ok", ok, false, - ) - - assert( - t, - deque.container.Len() == dequeSize, - "deque.container.Len() = %d; want %d", deque.container.Len(), dequeSize, - ) - - assert( - t, - deque.container.Front().Value == "0", - "deque.container.Front().Value = %s; want %s", deque.container.Front().Value, "0", - ) - - assert( - t, - deque.container.Back().Value == "19", - "deque.container.Back().Value = %s; want %s", deque.container.Back().Value, "19", - ) +func BenchmarkDequeAppend(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Append(i) + } } func TestDequePrepend(t *testing.T) { - deque := NewDeque() - sampleSize := 100 - - // Prepend elements in the Deque and assert it does not fail - for i := 0; i < sampleSize; i++ { - var value string = strconv.Itoa(i) - var ok bool = deque.Prepend(value) - - assert( - t, - ok == true, - "deque.Prepend(%d) = %t; want %t", i, ok, true, - ) - } - - assert( - t, - deque.container.Len() == sampleSize, - "deque.container.Len() = %d; want %d", deque.container.Len(), sampleSize, - ) - - assert( - t, - deque.container.Front().Value == "99", - "deque.container.Front().Value = %s; want %s", deque.container.Front().Value, "99", - ) - - assert( - t, - deque.container.Back().Value == "0", - "deque.container.Back().Value = %s; want %s", deque.container.Back().Value, "0", - ) + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + prependValue int + wantContainerLen uint + wantContainerFront bool + wantContainerFrontValue int + }{ + { + desc: "prepend to empty deque", + deque: NewDeque[int](), + prependValue: 42, + wantContainerLen: 1, + wantContainerFront: true, + wantContainerFrontValue: 42, + }, + { + desc: "prepend inserts value at the back", + deque: NewDeque([]int{43, 44}...), + prependValue: 42, + wantContainerLen: 3, + wantContainerFront: true, + wantContainerFrontValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + tC.deque.Prepend(tC.prependValue) + gotContainerLen := tC.deque.container.Len() + gotContainerFront := tC.deque.container.Front() + + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerFront, gotContainerFront != nil) + + if tC.wantContainerFront { + assert.Equal(t, tC.wantContainerFrontValue, gotContainerFront.Value) + } + }) + } } -func TestDequePrependWithCapacity(t *testing.T) { - dequeSize := 20 - deque := NewCappedDeque(dequeSize) - - // Prepend elements in the Deque and assert it does not fail - for i := 0; i < dequeSize; i++ { - var value string = strconv.Itoa(i) - var ok bool = deque.Prepend(value) - - assert( - t, - ok == true, - "deque.Prepend(%d) = %t; want %t", i, ok, true, - ) - } - - // Try to overflow the Deque size limit, and make - // sure appending fails - var ok bool = deque.Prepend("should not be ok") - assert( - t, - ok == false, - "deque.Prepend(%s) = %t; want %t", "should not be ok", ok, false, - ) - - assert( - t, - deque.container.Len() == dequeSize, - "deque.container.Len() = %d; want %d", deque.container.Len(), dequeSize, - ) - - assert( - t, - deque.container.Front().Value == "19", - "deque.container.Front().Value = %s; want %s", deque.container.Front().Value, "19", - ) - - assert( - t, - deque.container.Back().Value == "0", - "deque.container.Back().Value = %s; want %s", deque.container.Back().Value, "0", - ) +func BenchmarkDequePrepend(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Prepend(i) + } } -func TestDequePop_fulfilled_container(t *testing.T) { - deque := NewDeque() - dequeSize := 100 +func TestDequePop(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantOk bool + wantValue int + wantContainerLen uint + wantContainerBack bool + wantContainerBackValue int + }{ + { + desc: "Pop from an empty Deque", + deque: NewDeque[int](), + wantOk: false, + wantValue: 0, + wantContainerLen: 0, + wantContainerBack: false, + }, + { + desc: "Pop removes and returns the back value", + deque: NewDeque([]int{40, 41, 42}...), + wantOk: true, + wantValue: 42, + wantContainerLen: 2, + wantContainerBack: true, + wantContainerBackValue: 41, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue, gotOk := tC.deque.Pop() + gotContainerLen := tC.deque.container.Len() + gotContainerBack := tC.deque.container.Back() - // Populate the test deque - for i := 0; i < dequeSize; i++ { - var value string = strconv.Itoa(i) - deque.Append(value) + assert.Equal(t, tC.wantOk, gotOk) + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerBack, gotContainerBack != nil) + + if tC.wantContainerBack { + assert.Equal(t, tC.wantContainerBackValue, gotContainerBack.Value) + } + }) } +} - // Pop elements of the deque and assert elements come out - // in order and container size is updated accordingly - for i := dequeSize - 1; i >= 0; i-- { - item := deque.Pop() +func BenchmarkDequePop(b *testing.B) { + b.ReportAllocs() - var itemValue string = item.(string) - var expectedValue string = strconv.Itoa(i) + deque := NewDeque[int]() - assert( - t, - itemValue == expectedValue, - "deque.Pop() = %s; want %s", itemValue, expectedValue, - ) + for i := 0; i < b.N; i++ { + deque.Append(i) + } - assert( - t, - deque.container.Len() == i, - "deque.container.Len() = %d; want %d", deque.container.Len(), i, - ) + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Pop() + } +} + +func TestDequeShift(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantOk bool + wantValue int + wantContainerLen uint + wantContainerFront bool + wantContainerFrontValue int + }{ + { + desc: "Shift from an empty Deque", + deque: NewDeque[int](), + wantOk: false, + wantValue: 0, + wantContainerLen: 0, + wantContainerFront: false, + }, + { + desc: "Shift removes and returns the front value", + deque: NewDeque([]int{42, 43, 44}...), + wantOk: true, + wantValue: 42, + wantContainerLen: 2, + wantContainerFront: true, + wantContainerFrontValue: 43, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue, gotOk := tC.deque.Shift() + gotContainerLen := tC.deque.container.Len() + gotContainerBack := tC.deque.container.Front() + assert.Equal(t, tC.wantOk, gotOk) + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerFront, gotContainerBack != nil) + + if tC.wantContainerFront { + assert.Equal(t, tC.wantContainerFrontValue, gotContainerBack.Value) + } + }) } } -func TestDequePop_empty_container(t *testing.T) { - deque := NewDeque() - item := deque.Pop() - - assert( - t, - item == nil, - "item = %v; want %v", item, nil, - ) - - assert( - t, - deque.container.Len() == 0, - "deque.container.Len() = %d; want %d", deque.container.Len(), 0, - ) +func BenchmarkDequeShift(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + for i := 0; i < b.N; i++ { + deque.Append(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Shift() + } } -func TestDequeShift_fulfilled_container(t *testing.T) { - deque := NewDeque() - dequeSize := 100 +func TestDequeFirst(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantOk bool + wantValue int + wantContainerLen uint + wantContainerFront bool + wantContainerFrontValue int + }{ + { + desc: "First from an empty Deque", + deque: NewDeque[int](), + wantOk: false, + wantValue: 0, + wantContainerLen: 0, + wantContainerFront: false, + }, + { + desc: "First returns the front value", + deque: NewDeque([]int{42, 43, 44}...), + wantOk: true, + wantValue: 42, + wantContainerLen: 3, + wantContainerFront: true, + wantContainerFrontValue: 42, + }, + } - // Populate the test deque - for i := 0; i < dequeSize; i++ { - var value string = strconv.Itoa(i) - deque.Append(value) + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue, gotOk := tC.deque.First() + gotContainerLen := tC.deque.container.Len() + gotContainerBack := tC.deque.container.Front() + + assert.Equal(t, tC.wantOk, gotOk) + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerFront, gotContainerBack != nil) + + if tC.wantContainerFront { + assert.Equal(t, tC.wantContainerFrontValue, gotContainerBack.Value) + } + }) } +} - // Pop elements of the deque and assert elements come out - // in order and container size is updated accordingly - for i := 0; i < dequeSize; i++ { - item := deque.Shift() +func BenchmarkDequeFirst(b *testing.B) { + b.ReportAllocs() - var itemValue string = item.(string) - var expectedValue string = strconv.Itoa(i) + deque := NewDeque[int]() - assert( - t, - itemValue == expectedValue, - "deque.Shift() = %s; want %s", itemValue, expectedValue, - ) + for i := 0; i < b.N; i++ { + deque.Append(i) + } - assert( - t, - // Len should be equal to dequeSize - (i + 1) as i is zero indexed - deque.container.Len() == (dequeSize-(i+1)), - "deque.container.Len() = %d; want %d", deque.container.Len(), dequeSize-i, - ) + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.First() } } -func TestDequeShift_empty_container(t *testing.T) { - deque := NewDeque() - - item := deque.Shift() - assert( - t, - item == nil, - "deque.Shift() = %v; want %v", item, nil, - ) - - assert( - t, - deque.container.Len() == 0, - "deque.container.Len() = %d; want %d", deque.container.Len(), 0, - ) +func TestDequeLast(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantOk bool + wantValue int + wantContainerLen uint + wantContainerBack bool + wantContainerBackValue int + }{ + { + desc: "Last from an empty Deque", + deque: NewDeque[int](), + wantOk: false, + wantValue: 0, + wantContainerLen: 0, + wantContainerBack: false, + }, + { + desc: "Last returns the front value", + deque: NewDeque([]int{40, 41, 42}...), + wantOk: true, + wantValue: 42, + wantContainerLen: 3, + wantContainerBack: true, + wantContainerBackValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue, gotOk := tC.deque.Last() + gotContainerLen := tC.deque.container.Len() + gotContainerBack := tC.deque.container.Back() + + assert.Equal(t, tC.wantOk, gotOk) + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + assert.Equal(t, tC.wantContainerBack, gotContainerBack != nil) + + if tC.wantContainerBack { + assert.Equal(t, tC.wantContainerBackValue, gotContainerBack.Value) + } + }) + } } -func TestDequeFirst_fulfilled_container(t *testing.T) { - deque := NewDeque() - deque.Append("1") - item := deque.First() - - assert( - t, - item == "1", - "deque.First() = %s; want %s", item, "1", - ) - - assert( - t, - deque.container.Len() == 1, - "deque.container.Len() = %d; want %d", deque.container.Len(), 1, - ) +func BenchmarkDequeLast(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + for i := 0; i < b.N; i++ { + deque.Append(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Last() + } +} + +func TestDequeSize(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantValue uint + wantContainerLen uint + }{ + { + desc: "Size of an empty Deque", + deque: NewDeque[int](), + wantValue: 0, + wantContainerLen: 0, + }, + { + desc: "Size of a filled Deque", + deque: NewDeque([]int{40, 41, 42}...), + wantValue: 3, + wantContainerLen: 3, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue := tC.deque.Size() + gotContainerLen := tC.deque.container.Len() + + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + }) + } +} + +func BenchmarkDequeSize(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + for i := 0; i < b.N; i++ { + deque.Append(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Size() + } +} + +func TestDequeEmpty(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *Deque[int] + wantValue bool + wantContainerLen uint + }{ + { + desc: "Empty of an empty Deque", + deque: NewDeque[int](), + wantValue: true, + wantContainerLen: 0, + }, + { + desc: "Empty of a filled Deque", + deque: NewDeque([]int{40, 41, 42}...), + wantValue: false, + wantContainerLen: 3, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotValue := tC.deque.Empty() + gotContainerLen := tC.deque.container.Len() + + assert.Equal(t, tC.wantValue, gotValue) + assert.Equal(t, tC.wantContainerLen, gotContainerLen) + }) + } } -func TestDequeFirst_empty_container(t *testing.T) { - deque := NewDeque() - item := deque.First() - - assert( - t, - item == nil, - "deque.First() = %v; want %v", item, nil, - ) - - assert( - t, - deque.container.Len() == 0, - "deque.container.Len() = %d; want %d", deque.container.Len(), 0, - ) +func BenchmarkDequeEmpty(b *testing.B) { + b.ReportAllocs() + + deque := NewDeque[int]() + + for i := 0; i < b.N; i++ { + deque.Append(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Empty() + } } -func TestDequeLast_fulfilled_container(t *testing.T) { - deque := NewDeque() +func TestBoundDequeFull(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *BoundDeque[int] + wantFull bool + }{ + { + desc: "Empty BoundDeque with non null capacity is not full", + deque: NewBoundDeque[int](1), + wantFull: false, + }, + { + desc: "Empty BoundDeque with null capacity is full", + deque: NewBoundDeque[int](0), + wantFull: true, + }, + { + desc: "Non empty BoundDeque with non null, capacity and available space is not full", + deque: NewBoundDeque(4, []int{40, 41, 42}...), + wantFull: false, + }, + { + desc: "Non empty BoundDeque with non null, capacity and no available space is full", + deque: NewBoundDeque(3, []int{40, 41, 42}...), + wantFull: true, + }, + } - deque.Append("1") - deque.Append("2") - deque.Append("3") + for _, tC := range testCases { + tC := tC - item := deque.Last() + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() - assert( - t, - item == "3", - "deque.Last() = %s; want %s", item, "3", - ) + gotFull := tC.deque.Full() - assert( - t, - deque.container.Len() == 3, - "deque.container.Len() = %d; want %d", deque.container.Len(), 3, - ) + assert.Equal(t, tC.wantFull, gotFull) + }) + } } -func TestDequeLast_empty_container(t *testing.T) { - deque := NewDeque() - item := deque.Last() - - assert( - t, - item == nil, - "deque.Last() = %v; want %v", item, nil, - ) - - assert( - t, - deque.container.Len() == 0, - "deque.container.Len() = %d; want %d", deque.container.Len(), 0, - ) +func BenchmarkBoundDequeFull(b *testing.B) { + b.ReportAllocs() + + deque := NewBoundDeque[int](1) + + for i := 0; i < b.N; i++ { + deque.Append(i) + } + + b.ResetTimer() + for i := 0; i < b.N; i++ { + deque.Full() + } } -func TestDequeEmpty_fulfilled(t *testing.T) { - deque := NewDeque() - deque.Append("1") +// Considering BoundDeque embeds a Deque, no need to cover general +// cases that are not specifically related to capacity management. +func TestBoundDequeAppend(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *BoundDeque[int] + wantOk bool + }{ + { + desc: "Append to BoundDeque with non null capacity and available space", + deque: NewBoundDeque[int](1), + wantOk: true, + }, + { + desc: "Append to BoundDeque with non null capacity and available space", + deque: NewBoundDeque(1, []int{42}...), + wantOk: false, + }, + { + desc: "Append to BoundDeque with null capacity", + deque: NewBoundDeque[int](0), + wantOk: false, + }, + } - assert( - t, - deque.Empty() == false, - "deque.Empty() = %t; want %t", deque.Empty(), false) + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotOk := tC.deque.Append(42) + + assert.Equal(t, tC.wantOk, gotOk) + }) + } } -func TestDequeEmpty_empty_deque(t *testing.T) { - deque := NewDeque() - assert( - t, - deque.Empty() == true, - "deque.Empty() = %t; want %t", deque.Empty(), true, - ) +func BenchmarkBoundDequeAppend(b *testing.B) { + b.ReportAllocs() + + deque := NewBoundDeque[int](1) + + for i := 0; i < b.N; i++ { + deque.Append(i) + } } -func TestDequeFull_fulfilled(t *testing.T) { - deque := NewCappedDeque(3) +// Considering BoundDeque embeds a Deque, no need to cover general +// cases that are not specifically related to capacity management. +func TestBoundDequePrepend(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + deque *BoundDeque[int] + wantOk bool + }{ + { + desc: "Prepend to BoundDeque with non null capacity and available space", + deque: NewBoundDeque[int](1), + wantOk: true, + }, + { + desc: "Prepend to BoundDeque with non null capacity and available space", + deque: NewBoundDeque(1, []int{42}...), + wantOk: false, + }, + { + desc: "Prepend to BoundDeque with null capacity", + deque: NewBoundDeque[int](0), + wantOk: false, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() - deque.Append("1") - deque.Append("2") - deque.Append("3") + gotOk := tC.deque.Prepend(42) - assert( - t, - deque.Full() == true, - "deque.Full() = %t; want %t", deque.Full(), true, - ) + assert.Equal(t, tC.wantOk, gotOk) + }) + } } -func TestDequeFull_non_full_deque(t *testing.T) { - deque := NewCappedDeque(3) - deque.Append("1") +func BenchmarkBoundDeque(b *testing.B) { + b.ReportAllocs() + + deque := NewBoundDeque[int](1) - assert( - t, - deque.Full() == false, - "deque.Full() = %t; want %t", deque.Full(), false, - ) + for i := 0; i < b.N; i++ { + deque.Prepend(i) + } } diff --git a/doc.go b/doc.go index e7d71d9..5d2295f 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,4 @@ -/* -Lane package provides queue, priority queue, stack and deque data structures -implementations. Its was designed with simplicity, performance, and concurrent -usage in mind. -*/ +// Package lane provides queue, priority queue, stack and deque data structures +// implementations. Its was designed with simplicity, performance, and concurrent +// usage in mind. package lane diff --git a/example_lane_test.go b/example_lane_test.go index cb9c4f7..e4e8873 100644 --- a/example_lane_test.go +++ b/example_lane_test.go @@ -5,9 +5,9 @@ import ( "strings" ) -func ExamplePQueue() { +func ExamplePriorityQueue() { // Let's create a new max ordered priority queue - var priorityQueue *PQueue = NewPQueue(MINPQ) + priorityQueue := NewMaxPriorityQueue[string, int]() // And push some prioritized content into it priorityQueue.Push("easy as", 3) @@ -17,18 +17,19 @@ func ExamplePQueue() { // Now let's take a look at the min element in // the priority queue - headValue, headPriority := priorityQueue.Head() - fmt.Println(headValue) // "abc" - fmt.Println(headPriority) // 1 + headValue, headPriority, ok := priorityQueue.Head() + if ok { + fmt.Println(headValue) // "abc" + fmt.Println(headPriority) // 1 + } // Okay the song order seems to be preserved, let's // roll - var jacksonFive []string = make([]string, priorityQueue.Size()) + jacksonFive := make([]string, priorityQueue.Size()) for i := 0; i < len(jacksonFive); i++ { - value, _ := priorityQueue.Pop() - - jacksonFive[i] = value.(string) + value, _, _ := priorityQueue.Pop() + jacksonFive[i] = value } fmt.Println(strings.Join(jacksonFive, " ")) @@ -36,7 +37,7 @@ func ExamplePQueue() { func ExampleDeque() { // Let's create a new deque data structure - var deque *Deque = NewDeque() + deque := NewDeque[string]() // And push some content into it using the Append // and Prepend methods @@ -47,18 +48,25 @@ func ExampleDeque() { // Now let's take a look at what are the first and // last element stored in the Deque - firstValue := deque.First() - lastValue := deque.Last() - fmt.Println(firstValue) // "abc" - fmt.Println(lastValue) // 1 + firstValue, ok := deque.First() + if ok { + fmt.Println(firstValue) // "abc" + } + + lastValue, ok := deque.Last() + if ok { + fmt.Println(lastValue) // 1 + } // Okay now let's play with the Pop and Shift // methods to bring the song words together - var jacksonFive []string = make([]string, deque.Size()) + jacksonFive := make([]string, deque.Size()) for i := 0; i < len(jacksonFive); i++ { - value := deque.Shift() - jacksonFive[i] = value.(string) + value, ok := deque.Shift() + if ok { + jacksonFive[i] = value + } } // abc 123 easy as do re mi @@ -68,7 +76,7 @@ func ExampleDeque() { func ExampleQueue() { // Create a new queue and pretend we're handling starbucks // clients - var queue *Queue = NewQueue() + queue := NewQueue[string]() // Let's add the incoming clients to the queue queue.Enqueue("grumpyClient") @@ -78,14 +86,14 @@ func ExampleQueue() { fmt.Println(queue.Head()) // grumpyClient // Let's handle the clients asynchronously - for client := queue.Dequeue(); client != nil; { + for client, ok := queue.Dequeue(); ok; { go fmt.Println(client) } } func ExampleStack() { // Create a new stack and put some plates over it - var stack *Stack = NewStack() + stack := NewStack[string]() // Let's put some plates on the stack stack.Push("redPlate") @@ -95,18 +103,27 @@ func ExampleStack() { fmt.Println(stack.Head()) // greenPlate // What's on top of the stack? - value := stack.Pop() - fmt.Println(value.(string)) // greenPlate + value, ok := stack.Pop() + if ok { + fmt.Println(value) // greenPlate + } stack.Push("yellowPlate") - value = stack.Pop() - fmt.Println(value.(string)) // yellowPlate + + value, ok = stack.Pop() + if ok { + fmt.Println(value) // yellowPlate + } // What's on top of the stack? - value = stack.Pop() - fmt.Println(value.(string)) // bluePlate + value, ok = stack.Pop() + if ok { + fmt.Println(value) // bluePlate + } // What's on top of the stack? - value = stack.Pop() - fmt.Println(value.(string)) // redPlate + value, ok = stack.Pop() + if ok { + fmt.Println(value) // redPlate + } } diff --git a/go.mod b/go.mod index f92e867..01d6428 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,14 @@ -module github.com/oleiade/lane +module github.com/oleiade/lane/v2 -go 1.15 +go 1.18 + +require ( + github.com/stretchr/testify v1.7.0 + golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 +) + +require ( + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..b462207 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91 h1:tnebWN09GYg9OLPss1KXj8txwZc6X6uMr6VFdcGNbHw= +golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/helpers_test.go b/helpers_test.go deleted file mode 100644 index f54a10a..0000000 --- a/helpers_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package lane - -import ( - "fmt" - "path/filepath" - "reflect" - "runtime" - "testing" -) - -// assert fails the test if the condition is false. -func assert(tb testing.TB, condition bool, msg string, v ...interface{}) { - if !condition { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: "+msg+"\033[39m\n\n", append([]interface{}{filepath.Base(file), line}, v...)...) - tb.FailNow() - } -} - -// ok fails the test if an err is not nil. -func ok(tb testing.TB, err error) { - if err != nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: unexpected error: %s\033[39m\n\n", filepath.Base(file), line, err.Error()) - tb.FailNow() - } -} - -func notOk(tb testing.TB, err error) { - if err == nil { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d: expected error but got nil instead\n\n", filepath.Base(file), line) - tb.FailNow() - } -} - -// equals fails the test if exp is not equal to act. -func equals(tb testing.TB, exp, act interface{}) { - if !reflect.DeepEqual(exp, act) { - _, file, line, _ := runtime.Caller(1) - fmt.Printf("\033[31m%s:%d:\n\n\texp: %#v\n\n\tgot: %#v\033[39m\n\n", filepath.Base(file), line, exp, act) - tb.FailNow() - } -} diff --git a/list.go b/list.go new file mode 100644 index 0000000..b46ec7f --- /dev/null +++ b/list.go @@ -0,0 +1,215 @@ +package lane + +// List represents a doubly linked list. +type List[T any] struct { + root Element[T] + len uint +} + +// Init initializes or clears list l. +func (l *List[T]) Init() *List[T] { + l.root.next = &l.root + l.root.prev = &l.root + l.len = 0 + + return l +} + +// New returns an initialized list. +func New[T any]() *List[T] { + return new(List[T]).Init() +} + +// Len returns the number of elements of list l. +func (l *List[T]) Len() uint { + return l.len +} + +// Front returns the first element of list l or nil. +func (l *List[T]) Front() *Element[T] { + if l.len == 0 { + return nil + } + + return l.root.next +} + +// Back returns the last element of list l or nil. +func (l *List[T]) Back() *Element[T] { + if l.len == 0 { + return nil + } + + return l.root.prev +} + +// PushFront inserts a new element e with value v at +// the front of list l and returns e. +func (l *List[T]) PushFront(v T) *Element[T] { + l.lazyInit() + return l.insertValue(v, &l.root) +} + +// PushBack inserts a new element e with value v at +// the back of list l and returns e. +func (l *List[T]) PushBack(v T) *Element[T] { + l.lazyInit() + + return l.insertValue(v, l.root.prev) +} + +// InsertBefore inserts a new element e with value v +// immediately before mark and returns e. +func (l *List[T]) InsertBefore(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + + return l.insertValue(v, mark.prev) +} + +// InsertAfter inserts a new element e with value v +// immediately after mark and returns e. +func (l *List[T]) InsertAfter(v T, mark *Element[T]) *Element[T] { + if mark.list != l { + return nil + } + + return l.insertValue(v, mark) +} + +// Remove removes e from l if e is an element of list l. +func (l *List[T]) Remove(e *Element[T]) T { + if e.list == l { + l.remove(e) + } + + return e.Value +} + +// MoveToFront moves element e to the front of list l. +func (l *List[T]) MoveToFront(e *Element[T]) { + if e.list != l || l.root.next == e { + return + } + + l.move(e, &l.root) +} + +// MoveToBack moves element e to the back of list l. +func (l *List[T]) MoveToBack(e *Element[T]) { + if e.list != l || l.root.prev == e { + return + } + + l.move(e, l.root.prev) +} + +// MoveBefore moves element e to its new position before mark. +func (l *List[T]) MoveBefore(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + + l.move(e, mark.prev) +} + +// MoveAfter moves element e to its new position after mark. +func (l *List[T]) MoveAfter(e, mark *Element[T]) { + if e.list != l || e == mark || mark.list != l { + return + } + + l.move(e, mark) +} + +// PushBackList inserts a copy of an other list at the back of list l. +func (l *List[T]) PushBackList(other *List[T]) { + l.lazyInit() + + for i, e := other.Len(), other.Front(); i > 0; i, e = i-1, e.Next() { + l.insertValue(e.Value, l.root.prev) + } +} + +// PushFrontList inserts a copy of an other list at the front of list l. +func (l *List[T]) PushFrontList(other *List[T]) { + l.lazyInit() + for i, e := other.Len(), other.Back(); i > 0; i, e = i-1, e.Prev() { + l.insertValue(e.Value, &l.root) + } +} + +func (l *List[T]) lazyInit() { + if l.root.next == nil { + l.Init() + } +} + +func (l *List[T]) insert(e, at *Element[T]) *Element[T] { + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + e.list = l + l.len++ + + return e +} + +func (l *List[T]) insertValue(v T, at *Element[T]) *Element[T] { + return l.insert(&Element[T]{Value: v}, at) +} + +func (l *List[T]) remove(e *Element[T]) *Element[T] { + e.prev.next = e.next + e.next.prev = e.prev + e.next = nil // avoid memory leaks + e.prev = nil // avoid memory leaks + e.list = nil + l.len-- + return e +} + +func (l *List[T]) move(e, at *Element[T]) *Element[T] { + if e == at { + return e + } + + e.prev.next = e.next + e.next.prev = e.prev + + e.prev = at + e.next = at.next + e.prev.next = e + e.next.prev = e + + return e +} + +// Element is a node of a linked list. +type Element[T any] struct { + next, prev *Element[T] + + list *List[T] + + Value T +} + +// Next returns the next list element or nil. +func (e *Element[T]) Next() *Element[T] { + if p := e.next; e.list != nil && p != &e.list.root { + return p + } + + return nil +} + +// Prev returns the previous list element or nil. +func (e *Element[T]) Prev() *Element[T] { + if p := e.prev; e.list != nil && p != &e.list.root { + return p + } + + return nil +} diff --git a/pqueue.go b/pqueue.go deleted file mode 100644 index 85b97a1..0000000 --- a/pqueue.go +++ /dev/null @@ -1,170 +0,0 @@ -package lane - -import ( - "fmt" - "sync" -) - -// PQType represents a priority queue ordering kind (see MAXPQ and MINPQ) -type PQType int - -const ( - MAXPQ PQType = iota - MINPQ -) - -type item struct { - value interface{} - priority int -} - -// PQueue is a heap priority queue data structure implementation. -// It can be whether max or min ordered and it is synchronized -// and is safe for concurrent operations. -type PQueue struct { - sync.RWMutex - items []*item - elemsCount int - comparator func(int, int) bool -} - -func newItem(value interface{}, priority int) *item { - return &item{ - value: value, - priority: priority, - } -} - -func (i *item) String() string { - return fmt.Sprintf("", i.value, i.priority) -} - -// NewPQueue creates a new priority queue with the provided pqtype -// ordering type -func NewPQueue(pqType PQType) *PQueue { - var cmp func(int, int) bool - - if pqType == MAXPQ { - cmp = max - } else { - cmp = min - } - - items := make([]*item, 1) - items[0] = nil // Heap queue first element should always be nil - - return &PQueue{ - items: items, - elemsCount: 0, - comparator: cmp, - } -} - -// Push the value item into the priority queue with provided priority. -func (pq *PQueue) Push(value interface{}, priority int) { - item := newItem(value, priority) - - pq.Lock() - pq.items = append(pq.items, item) - pq.elemsCount += 1 - pq.swim(pq.size()) - pq.Unlock() -} - -// Pop and returns the highest/lowest priority item (depending on whether -// you're using a MINPQ or MAXPQ) from the priority queue -func (pq *PQueue) Pop() (interface{}, int) { - pq.Lock() - defer pq.Unlock() - - if pq.size() < 1 { - return nil, 0 - } - - var max *item = pq.items[1] - - pq.exch(1, pq.size()) - pq.items = pq.items[0:pq.size()] - pq.elemsCount -= 1 - pq.sink(1) - - return max.value, max.priority -} - -// Head returns the highest/lowest priority item (depending on whether -// you're using a MINPQ or MAXPQ) from the priority queue -func (pq *PQueue) Head() (interface{}, int) { - pq.RLock() - defer pq.RUnlock() - - if pq.size() < 1 { - return nil, 0 - } - - headValue := pq.items[1].value - headPriority := pq.items[1].priority - - return headValue, headPriority -} - -// Size returns the elements present in the priority queue count -func (pq *PQueue) Size() int { - pq.RLock() - defer pq.RUnlock() - return pq.size() -} - -// Check queue is empty -func (pq *PQueue) Empty() bool { - pq.RLock() - defer pq.RUnlock() - return pq.size() == 0 -} - -func (pq *PQueue) size() int { - return pq.elemsCount -} - -func max(i, j int) bool { - return i < j -} - -func min(i, j int) bool { - return i > j -} - -func (pq *PQueue) less(i, j int) bool { - return pq.comparator(pq.items[i].priority, pq.items[j].priority) -} - -func (pq *PQueue) exch(i, j int) { - var tmpItem *item = pq.items[i] - - pq.items[i] = pq.items[j] - pq.items[j] = tmpItem -} - -func (pq *PQueue) swim(k int) { - for k > 1 && pq.less(k/2, k) { - pq.exch(k/2, k) - k = k / 2 - } - -} - -func (pq *PQueue) sink(k int) { - for 2*k <= pq.size() { - var j int = 2 * k - - if j < pq.size() && pq.less(j, j+1) { - j++ - } - - if !pq.less(k, j) { - break - } - - pq.exch(k, j) - k = j - } -} diff --git a/pqueue_test.go b/pqueue_test.go deleted file mode 100644 index 726fa69..0000000 --- a/pqueue_test.go +++ /dev/null @@ -1,249 +0,0 @@ -package lane - -import ( - "reflect" - "strconv" - "sync" - "testing" -) - -func TestMaxPQueue_init(t *testing.T) { - pqueue := NewPQueue(MAXPQ) - - assert( - t, - len(pqueue.items) == 1, - "len(pqueue.items) == %d; want %d", len(pqueue.items), 1, - ) - - assert( - t, - pqueue.Size() == 0, - "pqueue.Size() = %d; want %d", pqueue.Size(), 0, - ) - - assert( - t, - pqueue.items[0] == nil, - "pqueue.items[0] = %v; want %v", pqueue.items[0], nil, - ) - - assert( - t, - reflect.ValueOf(pqueue.comparator).Pointer() == reflect.ValueOf(max).Pointer(), - "pqueue.comparator != max", - ) -} - -func TestMinPQueue_init(t *testing.T) { - pqueue := NewPQueue(MINPQ) - - assert( - t, - len(pqueue.items) == 1, - "len(pqueue.items) = %d; want %d", len(pqueue.items), 1, - ) - - assert( - t, - pqueue.Size() == 0, - "pqueue.Size() = %d; want %d", pqueue.Size(), 0, - ) - - assert( - t, - pqueue.items[0] == nil, - "pqueue.items[0] = %v; want %v", pqueue.items[0], nil, - ) - - assert( - t, - reflect.ValueOf(pqueue.comparator).Pointer() == reflect.ValueOf(min).Pointer(), - "pqueue.comparator != min", - ) -} - -func TestMaxPQueuePushAndPop_protects_max_order(t *testing.T) { - pqueue := NewPQueue(MAXPQ) - pqueueSize := 100 - - // Populate the test priority queue with dummy elements - // in asc ordered. - for i := 0; i < pqueueSize; i++ { - var value string = strconv.Itoa(i) - var priority int = i - - pqueue.Push(value, priority) - } - - containerIndex := 1 // binary heap are 1 indexed - for i := 99; i >= 0; i-- { - var expectedValue string = strconv.Itoa(i) - var expectedPriority int = i - - // Avoiding testing arithmetics headaches by using the pop function directly - value, priority := pqueue.Pop() - assert( - t, - value == expectedValue, - "value = %v; want %v", containerIndex, value, expectedValue, - ) - assert( - t, - priority == expectedPriority, - "priority = %v; want %v", containerIndex, priority, expectedValue, - ) - - containerIndex++ - } -} - -func TestMaxPQueuePushAndPop_concurrently_protects_max_order(t *testing.T) { - var wg sync.WaitGroup - - pqueue := NewPQueue(MAXPQ) - pqueueSize := 100 - - // Populate the test priority queue with dummy elements - // in asc ordered. - for i := 0; i < pqueueSize; i++ { - wg.Add(1) - - go func(i int) { - defer wg.Done() - - var value string = strconv.Itoa(i) - var priority int = i - pqueue.Push(value, priority) - }(i) - } - - wg.Wait() - - containerIndex := 1 // binary heap are 1 indexed - for i := 99; i >= 0; i-- { - var expectedValue string = strconv.Itoa(i) - var expectedPriority int = i - - // Avoiding testing arithmetics headaches by using the pop function directly - value, priority := pqueue.Pop() - assert( - t, - value == expectedValue, - "value = %v; want %v", containerIndex, value, expectedValue, - ) - assert( - t, - priority == expectedPriority, - "priority = %v; want %v", containerIndex, priority, expectedValue, - ) - - containerIndex++ - } -} - -func TestMinPQueuePushAndPop_protects_min_order(t *testing.T) { - pqueue := NewPQueue(MINPQ) - pqueueSize := 100 - - // Populate the test priority queue with dummy elements - // in asc ordered. - for i := 0; i < pqueueSize; i++ { - var value string = strconv.Itoa(i) - var priority int = i - - pqueue.Push(value, priority) - } - - for i := 0; i < pqueueSize; i++ { - var expectedValue string = strconv.Itoa(i) - var expectedPriority int = i - - // Avoiding testing arithmetics headaches by using the pop function directly - value, priority := pqueue.Pop() - assert( - t, - value == expectedValue, - "value = %v; want %v", value, expectedValue, - ) - assert( - t, - priority == expectedPriority, - "priority = %v; want %v", priority, expectedValue, - ) - } -} - -func TestMinPQueuePushAndPop_concurrently_protects_min_order(t *testing.T) { - pqueue := NewPQueue(MINPQ) - pqueueSize := 100 - - var wg sync.WaitGroup - - // Populate the test priority queue with dummy elements - // in asc ordered. - for i := 0; i < pqueueSize; i++ { - wg.Add(1) - - go func(i int) { - defer wg.Done() - - var value string = strconv.Itoa(i) - var priority int = i - - pqueue.Push(value, priority) - }(i) - } - - wg.Wait() - - for i := 0; i < pqueueSize; i++ { - var expectedValue string = strconv.Itoa(i) - var expectedPriority int = i - - // Avoiding testing arithmetics headaches by using the pop function directly - value, priority := pqueue.Pop() - assert( - t, - value == expectedValue, - "value = %v; want %v", value, expectedValue, - ) - assert( - t, - priority == expectedPriority, - "priority = %v; want %v", priority, expectedValue, - ) - } -} - -func TestMaxPQueueHead_returns_max_element(t *testing.T) { - pqueue := NewPQueue(MAXPQ) - - pqueue.Push("1", 1) - pqueue.Push("2", 2) - - value, priority := pqueue.Head() - - // First element of the binary heap is always left empty, so container - // size is the number of elements actually stored + 1 - assert(t, len(pqueue.items) == 3, "len(pqueue.items) = %d; want %d", len(pqueue.items), 3) - - assert(t, value == "2", "pqueue.Head().value = %v; want %v", value, "2") - assert(t, priority == 2, "pqueue.Head().priority = %d; want %d", priority, 2) -} - -func TestMinPQueueHead_returns_min_element(t *testing.T) { - pqueue := NewPQueue(MINPQ) - - pqueue.Push("1", 1) - pqueue.Push("2", 2) - - value, priority := pqueue.Head() - - // First element of the binary heap is always left empty, so container - // size is the number of elements actually stored + 1 - assert(t, len(pqueue.items) == 3, "len(pqueue.items) = %d; want %d", len(pqueue.items), 3) - - assert(t, value == "1", "pqueue.Head().value = %v; want %v", value, "1") - assert(t, priority == 1, "pqueue.Head().priority = %d; want %d", priority, 1) -} diff --git a/priority_queue.go b/priority_queue.go new file mode 100644 index 0000000..05373ac --- /dev/null +++ b/priority_queue.go @@ -0,0 +1,184 @@ +package lane + +import ( + "sync" + + "golang.org/x/exp/constraints" +) + +// PriorityQueue is a heap priority queue data structure implementation. +// +// It can be either be minimum (ascending) or maximum (descending) +// oriented/ordered. Its type parameters `T` and `P` respectively +// specify the value underlying type, and the priority underlying type. +// +// Every operations are synchronized and goroutine-safe. +type PriorityQueue[T any, P constraints.Ordered] struct { + sync.RWMutex + items []*priorityQueueItem[T, P] + itemCount uint + comparator func(lhs, rhs P) bool +} + +// NewPriorityQueue instantiates a new PriorityQueue with the provided comparison heuristic. +// The package defines the `Max` and `Min` heuristic to define a maximum-oriented or +// minimum-oriented heuristic respectively. +func NewPriorityQueue[T any, P constraints.Ordered](heuristic func(lhs, rhs P) bool) *PriorityQueue[T, P] { + items := make([]*priorityQueueItem[T, P], 1) + items[0] = nil + + return &PriorityQueue[T, P]{ + items: items, + itemCount: 0, + comparator: heuristic, + } +} + +// NewMaxPriorityQueue instantiates a new maximum oriented PriorityQueue. +func NewMaxPriorityQueue[T any, P constraints.Ordered]() *PriorityQueue[T, P] { + return NewPriorityQueue[T](Maximum[P]) +} + +// NewMinPriorityQueue instantiates a new minimum oriented PriorityQueue. +func NewMinPriorityQueue[T any, P constraints.Ordered]() *PriorityQueue[T, P] { + return NewPriorityQueue[T](Minimum[P]) +} + +// Maximum returns whether `rhs` is greater than `lhs`. +// +// It can be used as a comparison heuristic during a PriorityQueue's +// instantiation. +func Maximum[T constraints.Ordered](lhs, rhs T) bool { + return lhs < rhs +} + +// Minimum returns whether `rhs` is less than `lhs`. +// +// It can be used as a comparison heuristic during a PriorityQueue's +// instantiation. +func Minimum[T constraints.Ordered](lhs, rhs T) bool { + return lhs > rhs +} + +// Push inserts the value in the PriorityQueue with the provided priority +// in at most O(log n) time complexity. +func (pq *PriorityQueue[T, P]) Push(value T, priority P) { + item := newPriorityQueueItem(value, priority) + + pq.Lock() + defer pq.Unlock() + pq.items = append(pq.items, item) + pq.itemCount++ + pq.swim(pq.size()) +} + +// Pop and returns the highest or lowest priority item (depending on the +// comparison heuristic of your PriorityQueue) from the PriorityQueue in +// at most O(log n) complexity. +func (pq *PriorityQueue[T, P]) Pop() (value T, priority P, ok bool) { + pq.Lock() + defer pq.Unlock() + + if pq.size() < 1 { + ok = false + return + } + + max := pq.items[1] + pq.exch(1, pq.size()) + pq.items = pq.items[0:pq.size()] + pq.itemCount-- + pq.sink(1) + + value = max.value + priority = max.priority + ok = true + + return +} + +// Head returns the highest or lowest priority item (depending on +// the comparison heuristic of your PriorityQueue) from the PriorityQueue +// in O(1) complexity. +func (pq *PriorityQueue[T, P]) Head() (value T, priority P, ok bool) { + pq.RLock() + defer pq.RUnlock() + + if pq.size() < 1 { + ok = false + return + } + + value = pq.items[1].value + priority = pq.items[1].priority + ok = true + + return +} + +// Size returns the number of elements present in the PriorityQueue. +func (pq *PriorityQueue[T, P]) Size() uint { + pq.RLock() + defer pq.RUnlock() + return pq.size() +} + +// Empty returns whether the PriorityQueue is empty. +func (pq *PriorityQueue[T, P]) Empty() bool { + pq.RLock() + defer pq.RUnlock() + return pq.size() == 0 +} + +func (pq *PriorityQueue[T, P]) swim(k uint) { + for k > 1 && pq.less(k/2, k) { + pq.exch(k/2, k) + k /= 2 + } +} + +func (pq *PriorityQueue[T, P]) sink(k uint) { + for 2*k <= pq.size() { + j := 2 * k + + if j < pq.size() && pq.less(j, j+1) { + j++ + } + + if !pq.less(k, j) { + break + } + + pq.exch(k, j) + k = j + } +} + +// size is a private method that's not goroutine-safe. +// It is meant to be called by a method who has already +// acquired a lock on the PriorityQueue. +func (pq *PriorityQueue[T, P]) size() uint { + return pq.itemCount +} + +func (pq *PriorityQueue[T, P]) less(lhs, rhs uint) bool { + return pq.comparator(pq.items[lhs].priority, pq.items[rhs].priority) +} + +func (pq *PriorityQueue[T, P]) exch(lhs, rhs uint) { + pq.items[lhs], pq.items[rhs] = pq.items[rhs], pq.items[lhs] +} + +// priorityQueueItem is the underlying PriorityQueue item container. +type priorityQueueItem[T any, P constraints.Ordered] struct { + value T + priority P +} + +// newPriorityQueue instantiates a new priorityQueueItem. +func newPriorityQueueItem[T any, P constraints.Ordered](value T, priority P) *priorityQueueItem[T, P] { + return &priorityQueueItem[T, P]{ + value: value, + priority: priority, + } +} diff --git a/priority_queue_test.go b/priority_queue_test.go new file mode 100644 index 0000000..e47c0d3 --- /dev/null +++ b/priority_queue_test.go @@ -0,0 +1,398 @@ +package lane + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPriorityQueuePush(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + heuristic func(lhs, rhs int) bool + pushItems []*priorityQueueItem[string, int] + wantItemCount uint + wantItems []*priorityQueueItem[string, int] + }{ + { + desc: "Push on empty PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + }, + wantItemCount: 1, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("a", 1), + }, + }, + { + desc: "Push on multiple values on max oriented PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantItemCount: 3, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("c", 3), + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + }, + }, + { + desc: "Push on multiple values on min oriented PriorityQueue", + heuristic: Minimum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantItemCount: 3, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.desc, func(t *testing.T) { + t.Parallel() + + pqueue := NewPriorityQueue[string](testCase.heuristic) + for _, item := range testCase.pushItems { + pqueue.Push(item.value, item.priority) + } + + gotItemCount := pqueue.itemCount + gotItems := pqueue.items + + assert.Equal(t, testCase.wantItemCount, gotItemCount) + assert.Equal(t, testCase.wantItems, gotItems) + }) + } +} + +func BenchmarkPriorityQueuePush(b *testing.B) { + b.ReportAllocs() + + pqueue := NewMaxPriorityQueue[string, int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pqueue.Push("a", 1) + } +} + +func TestPriorityQueuePop(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + heuristic func(lhs, rhs int) bool + pushItems []*priorityQueueItem[string, int] + wantOk bool + wantValue string + wantPriority int + wantItemCount uint + wantItems []*priorityQueueItem[string, int] + }{ + { + desc: "Pop from an empty PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{}, + wantOk: false, + wantValue: "", + wantPriority: 0, + wantItemCount: 0, + wantItems: []*priorityQueueItem[string, int]{ + nil, + }, + }, + { + desc: "Pop from a filled max oriented PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantOk: true, + wantValue: "c", + wantPriority: 3, + wantItemCount: 2, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("b", 2), + newPriorityQueueItem("a", 1), + }, + }, + { + desc: "Pop from a filled min oriented PriorityQueue", + heuristic: Minimum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantOk: true, + wantValue: "a", + wantPriority: 1, + wantItemCount: 2, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.desc, func(t *testing.T) { + t.Parallel() + + pqueue := NewPriorityQueue[string](testCase.heuristic) + for _, item := range testCase.pushItems { + pqueue.Push(item.value, item.priority) + } + + gotValue, gotPriority, gotOk := pqueue.Pop() + gotItemCount := pqueue.itemCount + gotItems := pqueue.items + + assert.Equal(t, testCase.wantOk, gotOk) + assert.Equal(t, testCase.wantValue, gotValue) + assert.Equal(t, testCase.wantPriority, gotPriority) + assert.Equal(t, testCase.wantItemCount, gotItemCount) + assert.Equal(t, testCase.wantItems, gotItems) + }) + } +} + +func BenchmarkPriorityQueuePop(b *testing.B) { + b.ReportAllocs() + + pqueue := NewMaxPriorityQueue[string, int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pqueue.Pop() + } +} + +func TestPriorityQueueHead(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + heuristic func(lhs, rhs int) bool + pushItems []*priorityQueueItem[string, int] + wantOk bool + wantValue string + wantPriority int + wantItemCount uint + wantItems []*priorityQueueItem[string, int] + }{ + { + desc: "Head of an empty PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{}, + wantOk: false, + wantValue: "", + wantPriority: 0, + wantItemCount: 0, + wantItems: []*priorityQueueItem[string, int]{ + nil, + }, + }, + { + desc: "Head of a filled max oriented PriorityQueue", + heuristic: Maximum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantOk: true, + wantValue: "c", + wantPriority: 3, + wantItemCount: 3, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("c", 3), + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + }, + }, + { + desc: "Head of a filled min oriented PriorityQueue", + heuristic: Minimum[int], + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantOk: true, + wantValue: "a", + wantPriority: 1, + wantItemCount: 3, + wantItems: []*priorityQueueItem[string, int]{ + nil, + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.desc, func(t *testing.T) { + t.Parallel() + + pqueue := NewPriorityQueue[string](testCase.heuristic) + for _, item := range testCase.pushItems { + pqueue.Push(item.value, item.priority) + } + + gotValue, gotPriority, gotOk := pqueue.Head() + gotItemCount := pqueue.itemCount + gotItems := pqueue.items + + assert.Equal(t, testCase.wantOk, gotOk) + assert.Equal(t, testCase.wantValue, gotValue) + assert.Equal(t, testCase.wantPriority, gotPriority) + assert.Equal(t, testCase.wantItemCount, gotItemCount) + assert.Equal(t, testCase.wantItems, gotItems) + }) + } +} + +func BenchmarkPriorityQueueHead(b *testing.B) { + b.ReportAllocs() + + pqueue := NewMaxPriorityQueue[string, int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pqueue.Head() + } +} + +func TestPriorityQueueSize(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + pushItems []*priorityQueueItem[string, int] + wantValue uint + }{ + { + desc: "Head of an empty PriorityQueue", + pushItems: []*priorityQueueItem[string, int]{}, + wantValue: 0, + }, + { + desc: "Head of a filled max oriented PriorityQueue", + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantValue: 3, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.desc, func(t *testing.T) { + t.Parallel() + + pqueue := NewMaxPriorityQueue[string, int]() + for _, item := range testCase.pushItems { + pqueue.Push(item.value, item.priority) + } + + gotValue := pqueue.Size() + + assert.Equal(t, testCase.wantValue, gotValue) + }) + } +} + +func BenchmarkPriorityQueueSize(b *testing.B) { + b.ReportAllocs() + + pqueue := NewMaxPriorityQueue[string, int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pqueue.Size() + } +} + +func TestPriorityQueueEmpty(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + pushItems []*priorityQueueItem[string, int] + wantValue bool + }{ + { + desc: "Head of an empty PriorityQueue", + pushItems: []*priorityQueueItem[string, int]{}, + wantValue: true, + }, + { + desc: "Head of a filled max oriented PriorityQueue", + pushItems: []*priorityQueueItem[string, int]{ + newPriorityQueueItem("a", 1), + newPriorityQueueItem("b", 2), + newPriorityQueueItem("c", 3), + }, + wantValue: false, + }, + } + + for _, testCase := range testCases { + testCase := testCase + + t.Run(testCase.desc, func(t *testing.T) { + t.Parallel() + + pqueue := NewMaxPriorityQueue[string, int]() + for _, item := range testCase.pushItems { + pqueue.Push(item.value, item.priority) + } + + gotValue := pqueue.Empty() + + assert.Equal(t, testCase.wantValue, gotValue) + }) + } +} + +func BenchmarkPriorityQueueEmpty(b *testing.B) { + b.ReportAllocs() + + pqueue := NewMaxPriorityQueue[string, int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + pqueue.Empty() + } +} diff --git a/queue.go b/queue.go index dd477a9..492b205 100644 --- a/queue.go +++ b/queue.go @@ -1,34 +1,46 @@ package lane // Queue is a FIFO (First in first out) data structure implementation. -// It is based on a deque container and focuses its API on core -// functionalities: Enqueue, Dequeue, Head, Size, Empty. Every operations time complexity -// is O(1). +// It is based on a Deque container and focuses its API on core +// functionalities: Enqueue, Dequeue, Head, Size, Empty. +// +// Every operation's time complexity is O(1). // // As it is implemented using a Deque container, every operations -// over an instiated Queue are synchronized and safe for concurrent -// usage. -type Queue struct { - *Deque +// over an instiated Queue are synchronized and goroutine-safe. +type Queue[T any] struct { + container *Deque[T] } -func NewQueue() *Queue { - return &Queue{ - Deque: NewDeque(), +// NewQueue produces a new Queue instance. +func NewQueue[T any](items ...T) *Queue[T] { + deque := NewDeque[T]() + + for _, item := range items { + deque.container.PushFront(item) + } + + return &Queue[T]{ + container: deque, } } -// Enqueue adds an item at the back of the queue -func (q *Queue) Enqueue(item interface{}) { - q.Prepend(item) +// Enqueue adds an item at the back of the Queue in O(1) time complexity. +func (q *Queue[T]) Enqueue(item T) { + q.container.Prepend(item) +} + +// Dequeue removes and returns the Queue's front item in O(1) time complexity. +func (q *Queue[T]) Dequeue() (item T, ok bool) { + return q.container.Pop() } -// Dequeue removes and returns the front queue item -func (q *Queue) Dequeue() interface{} { - return q.Pop() +// Head returns the Queue's front queue item in O(1) time complexity. +func (q *Queue[T]) Head() (item T, ok bool) { + return q.container.Last() } -// Head returns the front queue item -func (q *Queue) Head() interface{} { - return q.Last() +// Size returns the size of the Queue. +func (q *Queue[T]) Size() uint { + return q.container.Size() } diff --git a/queue_test.go b/queue_test.go index e0ef210..e715f30 100644 --- a/queue_test.go +++ b/queue_test.go @@ -1,118 +1,240 @@ package lane import ( - "strconv" "testing" + + "github.com/stretchr/testify/assert" ) +func TestNewQueue(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + items []int + wantContainerLastOk bool + wantContainerLastValue int + wantContainerSize uint + }{ + { + desc: "NewQueue initializes a Queue", + wantContainerLastOk: false, + wantContainerSize: 0, + }, + { + desc: "NewQueue with initializer produces FIFO ordering", + items: []int{1, 2, 3}, + wantContainerLastOk: true, + wantContainerLastValue: 1, + wantContainerSize: 3, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotQueue := NewQueue(tC.items...) + gotContainerFirstValue, gotContainerFirstOk := gotQueue.container.Last() + gotContainerSize := gotQueue.container.Size() + + assert.Equal(t, tC.wantContainerLastOk, gotContainerFirstOk) + assert.Equal(t, tC.wantContainerLastValue, gotContainerFirstValue) + assert.Equal(t, tC.wantContainerSize, gotContainerSize) + }) + } +} + +func BenchmarkNewQueue(b *testing.B) { + for i := 0; i < b.N; i++ { + NewQueue[int]() + } +} + func TestQueueEnqueue(t *testing.T) { - queue := NewQueue() - queueSize := 100 - - // Populate test queue and assert Enqueue - // function does not fail - for i := 0; i < queueSize; i++ { - var value string = strconv.Itoa(i) - queue.Enqueue(value) + t.Parallel() + + testCases := []struct { + desc string + queue *Queue[int] + enqueueValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerLastValue int + }{ + { + desc: "Enqueue to an empty Queue inserts value", + queue: NewQueue[int](), + enqueueValue: 42, + wantContainerSize: 1, + wantContainerFirst: true, + wantContainerLastValue: 42, + }, + { + desc: "Enqueue inserts value at the head", + queue: NewQueue([]int{41, 40}...), + enqueueValue: 42, + wantContainerSize: 3, + wantContainerFirst: true, + wantContainerLastValue: 41, + }, } - assert( - t, - queue.Size() == queueSize, - "queue.Size() = %d; want %d", queue.Size(), 3, - ) - - assert( - t, - queue.First() == "99", - "queue.Size() = %s; want %s", queue.First(), "99") - - assert( - t, - queue.Last() == "0", - "queue.Last() = %s; want %s", queue.Last(), "0", - ) + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + tC.queue.Enqueue(tC.enqueueValue) + gotContainerLen := tC.queue.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.queue.container.Last() + + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerLastValue, gotContainerFirstValue) + } + }) + } } -func TestQueueDequeue_fulfilled(t *testing.T) { - queue := NewQueue() - queueSize := 100 +func BenchmarkQueueEnqueue(b *testing.B) { + b.ReportAllocs() + + queue := NewQueue[int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + queue.Enqueue(i) + } +} - // Populate test queue and assert Enqueue - // function does not fail - for i := 0; i < queueSize; i++ { - var value string = strconv.Itoa(i) - queue.Enqueue(value) +func TestQueueDequeue(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + queue *Queue[int] + wantDequeueOk bool + wantDequeueValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerLastValue int + }{ + { + desc: "Dequeue on an empty Queue", + queue: NewQueue[int](), + wantDequeueOk: false, + wantContainerSize: 0, + wantContainerFirst: false, + }, + { + desc: "Dequeue removes and returns the head value", + queue: NewQueue([]int{42, 41, 40}...), + wantDequeueOk: true, + wantDequeueValue: 42, + wantContainerSize: 2, + wantContainerFirst: true, + wantContainerLastValue: 41, + }, } - // Check that while deuqueing, elements come out in - // their insertion order - for i := 0; i < queueSize; i++ { - item := queue.Dequeue() - expectedValue := strconv.Itoa(i) - - assert( - t, - item == expectedValue, - "queue.Dequeue() = %s; want %s", item, expectedValue, - ) - - assert( - t, - queue.Size() == queueSize-(i+1), - "queue.Size() = %d; want %d", queue.Size(), queueSize-(i+1), - ) + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotDequeueValue, gotDequeueOk := tC.queue.Dequeue() + gotContainerLen := tC.queue.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.queue.container.Last() + + assert.Equal(t, tC.wantDequeueOk, gotDequeueOk) + assert.Equal(t, tC.wantDequeueValue, gotDequeueValue) + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerLastValue, gotContainerFirstValue) + } + }) } } -func TestQueueDequeue_empty(t *testing.T) { - queue := NewQueue() - item := queue.Dequeue() - - assert( - t, - item == nil, - "queue.Dequeue() = %v; want %v", item, nil, - ) - - assert( - t, - queue.Size() == 0, - "queue.Size() = %d; want %d", queue.Size(), 0, - ) +func BenchmarkQueueDequeue(b *testing.B) { + b.ReportAllocs() + + queue := NewQueue[int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + queue.Dequeue() + } } -func TestQueueHead_fulfilled(t *testing.T) { - queue := NewQueue() - queue.Enqueue("1") - item := queue.Head() - - assert( - t, - item == "1", - "queue.Enqueue() = %s; want %s", item, "1", - ) - - assert( - t, - queue.Size() == 1, - "queue.Size() = %d; want %d", queue.Size(), 1, - ) +func TestQueueHead(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + queue *Queue[int] + wantHeadOk bool + wantHeadValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerLastValue int + }{ + { + desc: "Head on an empty Queue", + queue: NewQueue[int](), + wantHeadOk: false, + wantContainerSize: 0, + wantContainerFirst: false, + }, + { + desc: "Head returns the head value and leaves the Queue untouched", + queue: NewQueue([]int{42, 41, 40}...), + wantHeadOk: true, + wantHeadValue: 42, + wantContainerSize: 3, + wantContainerFirst: true, + wantContainerLastValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotHeadValue, gotHeadOk := tC.queue.Head() + gotContainerLen := tC.queue.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.queue.container.Last() + + assert.Equal(t, tC.wantHeadOk, gotHeadOk) + assert.Equal(t, tC.wantHeadValue, gotHeadValue) + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerLastValue, gotContainerFirstValue) + } + }) + } } -func TestQueueHead_empty(t *testing.T) { - queue := NewQueue() - item := queue.Head() - - assert( - t, - item == nil, - "queue.Head() = %v; want %v", item, nil, - ) - - assert( - t, - queue.Size() == 0, - "queue.Size() = %d; want %d", queue.Size(), 0, - ) +func BenchmarkQueueHead(b *testing.B) { + b.ReportAllocs() + + queue := NewQueue[int]() + + b.ResetTimer() + for i := 0; i < b.N; i++ { + queue.Head() + } } diff --git a/stack.go b/stack.go index aa9bee9..d7b3776 100644 --- a/stack.go +++ b/stack.go @@ -1,34 +1,44 @@ package lane // Stack is a LIFO (Last in first out) data structure implementation. -// It is based on a deque container and focuses its API on core -// functionalities: Push, Pop, Head, Size, Empty. Every operations time complexity -// is O(1). +// It is based on a Deque container and focuses its API on core +// functionalities: Push, Pop, Head, Size, Empty. +// +// Every operation's time complexity is O(1). // // As it is implemented using a Deque container, every operations -// over an instiated Stack are synchronized and safe for concurrent -// usage. -type Stack struct { - *Deque +// over an instiated Stack are synchronized and goroutine-safe. +type Stack[T any] struct { + container *Deque[T] } -func NewStack() *Stack { - return &Stack{ - Deque: NewDeque(), +// NewStack produces a new Stack instance. +// +// If any initialization variadic items are provided, they +// will be inserted as is: lower index being the head of stack. +func NewStack[T any](items ...T) (stack *Stack[T]) { + // FIXME: unwrap here instead of depending on Deque's for clarity + return &Stack[T]{ + container: NewDeque(items...), } } -// Push adds on an item on the top of the Stack -func (s *Stack) Push(item interface{}) { - s.Prepend(item) +// Push adds on an item on the top of the Stack. +func (s *Stack[T]) Push(item T) { + s.container.Prepend(item) +} + +// Pop removes and returns the item on the top of the Stack. +func (s *Stack[T]) Pop() (item T, ok bool) { + return s.container.Shift() } -// Pop removes and returns the item on the top of the Stack -func (s *Stack) Pop() interface{} { - return s.Shift() +// Head returns the item on the top of the Stack. +func (s *Stack[T]) Head() (item T, ok bool) { + return s.container.First() } -// Head returns the item on the top of the stack -func (s *Stack) Head() interface{} { - return s.First() +// Size returns the size of the Stack. +func (s *Stack[T]) Size() uint { + return s.container.Size() } diff --git a/stack_test.go b/stack_test.go index a18f852..01aac88 100644 --- a/stack_test.go +++ b/stack_test.go @@ -1,138 +1,239 @@ package lane import ( - "strconv" "testing" -) -func TestStackPush(t *testing.T) { - stack := NewStack() - stackSize := 100 + "github.com/stretchr/testify/assert" +) - // Fulfill the test Stack - for i := 0; i < stackSize; i++ { - var value string = strconv.Itoa(i) - stack.Push(value) +func TestNewStack(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + items []int + wantContainerFirstOk bool + wantContainerFirstValue int + wantContainerSize uint + }{ + { + desc: "NewStack initializes a Stack", + wantContainerFirstOk: false, + wantContainerSize: 0, + }, + { + desc: "NewStack with initializer produces FIFO ordering", + items: []int{1, 2, 3}, + wantContainerFirstOk: true, + wantContainerFirstValue: 1, + wantContainerSize: 3, + }, } - assert( - t, - stack.container.Len() == stackSize, - "stack.container.Len() = %d; want %d", stack.container.Len(), stackSize, - ) - - assert( - t, - stack.container.Front().Value == "99", - "stack.container.Front().Value = %s; want %s", stack.container.Front().Value, "99", - ) - - assert( - t, - stack.container.Back().Value == "0", - "stack.container.Back().Value = %s; want %s", stack.container.Back().Value, "0", - ) -} + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() -func TestStackPop_fulfilled(t *testing.T) { - stack := NewStack() - stackSize := 100 + gotStack := NewStack(tC.items...) + gotContainerFirstValue, gotContainerFirstOk := gotStack.container.First() + gotContainerSize := gotStack.container.Size() - // Add elements to the test Stack - for i := 0; i < stackSize; i++ { - var value string = strconv.Itoa(i) - stack.Push(value) + assert.Equal(t, tC.wantContainerFirstOk, gotContainerFirstOk) + assert.Equal(t, tC.wantContainerFirstValue, gotContainerFirstValue) + assert.Equal(t, tC.wantContainerSize, gotContainerSize) + }) } +} + +func TestStackPush(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + stack *Stack[int] + pushValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerFirstValue int + }{ + { + desc: "Push to an empty Stack inserts value", + stack: NewStack[int](), + pushValue: 42, + wantContainerSize: 1, + wantContainerFirst: true, + wantContainerFirstValue: 42, + }, + { + desc: "Push inserts value at the head", + stack: NewStack([]int{41, 40}...), + pushValue: 42, + wantContainerSize: 3, + wantContainerFirst: true, + wantContainerFirstValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC - // Pop elements and assert they come out in LIFO order - for i := 99; i >= 0; i-- { - var expectedValue string = strconv.Itoa(i) - item := stack.Pop() - - assert( - t, - item == expectedValue, - "stack.Pop() = %v; want %v", item, expectedValue, - ) - - assert( - t, - stack.container.Len() == i, - "stack.container.Len() = %d; want %d", stack.container.Len(), i, - ) + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + tC.stack.Push(tC.pushValue) + gotContainerLen := tC.stack.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.stack.container.First() + + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerFirstValue, gotContainerFirstValue) + } + }) } } -func TestStackPop_empty(t *testing.T) { - stack := NewStack() - - item := stack.Pop() - assert( - t, - item == nil, - "stack.Pop() = %v; want %v", item, nil, - ) - - assert( - t, - stack.container.Len() == 0, - "stack.container.Len() = %d; want %d", stack.container.Len(), 0, - ) +func BenchmarkNewStack(b *testing.B) { + for i := 0; i < b.N; i++ { + NewStack[int]() + } } -func TestStackHead_fulfilled(t *testing.T) { - stack := NewStack() - - stack.Push("1") - stack.Push("2") - stack.Push("3") - - item := stack.Head() - assert( - t, - item == "3", - "stack.Head() = %v; want %v", item, "3", - ) - - assert( - t, - stack.container.Len() == 3, - "stack.container.Len() = %d; want %d", stack.container.Len(), 3, - ) +func TestStackPop(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + stack *Stack[int] + wantPopOk bool + wantPopValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerFirstValue int + }{ + { + desc: "Pop on an empty Stack", + stack: NewStack[int](), + wantPopOk: false, + wantContainerSize: 0, + wantContainerFirst: false, + }, + { + desc: "Pop removes and returns the head value", + stack: NewStack([]int{42, 41, 40}...), + wantPopOk: true, + wantPopValue: 42, + wantContainerSize: 2, + wantContainerFirst: true, + wantContainerFirstValue: 41, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotPopValue, gotPopOk := tC.stack.Pop() + gotContainerLen := tC.stack.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.stack.container.First() + + assert.Equal(t, tC.wantPopOk, gotPopOk) + assert.Equal(t, tC.wantPopValue, gotPopValue) + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerFirstValue, gotContainerFirstValue) + } + }) + } } -func TestStackHead_empty(t *testing.T) { - stack := NewStack() - - item := stack.Head() - assert( - t, - item == nil, - "stack.Head() = %v; want %v", item, nil, - ) - - assert( - t, - stack.container.Len() == 0, - "stack.container.Len() = %d; want %d", stack.container.Len(), 0, - ) +func BenchmarkStackPop(b *testing.B) { + b.ReportAllocs() + + stack := NewStack[int]() + + for i := 0; i < b.N; i++ { + stack.Push(i) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stack.Pop() + } } -func TestStackEmpty_fulfilled(t *testing.T) { - stack := NewStack() - stack.Push("1") - assert( - t, - stack.Empty() == false, - "stack.Empty() = %b; want %b", stack.Empty(), false, - ) +func TestStackHead(t *testing.T) { + t.Parallel() + + testCases := []struct { + desc string + stack *Stack[int] + wantHeadOk bool + wantHeadValue int + wantContainerSize uint + wantContainerFirst bool + wantContainerFirstValue int + }{ + { + desc: "Head on an empty Stack", + stack: NewStack[int](), + wantHeadOk: false, + wantContainerSize: 0, + wantContainerFirst: false, + }, + { + desc: "Head returns the head value and leaves the Stack untouched", + stack: NewStack([]int{42, 41, 40}...), + wantHeadOk: true, + wantHeadValue: 42, + wantContainerSize: 3, + wantContainerFirst: true, + wantContainerFirstValue: 42, + }, + } + + for _, tC := range testCases { + tC := tC + + t.Run(tC.desc, func(t *testing.T) { + t.Parallel() + + gotHeadValue, gotHeadOk := tC.stack.Head() + gotContainerLen := tC.stack.container.Size() + gotContainerFirstValue, gotContainerFirstOk := tC.stack.container.First() + + assert.Equal(t, tC.wantHeadOk, gotHeadOk) + assert.Equal(t, tC.wantHeadValue, gotHeadValue) + assert.Equal(t, tC.wantContainerSize, gotContainerLen) + assert.Equal(t, tC.wantContainerFirst, gotContainerFirstOk) + + if tC.wantContainerFirst { + assert.Equal(t, tC.wantContainerFirstValue, gotContainerFirstValue) + } + }) + } } -func TestStackEmpty_empty_queue(t *testing.T) { - stack := NewStack() - assert( - t, - stack.Empty() == true, - "stack.Empty() = %b; want %b", stack.Empty(), true, - ) +func BenchmarkStackHead(b *testing.B) { + b.ReportAllocs() + + stack := NewStack[int]() + + for i := 0; i < b.N; i++ { + stack.Push(i) + } + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + stack.Head() + } }