├── .github ├── dependabot.yml └── workflows │ └── go-presubmit.yml ├── .gitignore ├── LICENSE ├── README.md ├── cache ├── cache.go ├── cache_test.go ├── example_test.go ├── internal │ └── cachetest │ │ └── cachetest.go └── lru.go ├── compare ├── compare.go └── compare_test.go ├── distinct ├── distinct.go ├── distinct_test.go └── example_test.go ├── go.mod ├── go.sum ├── heapq ├── example_test.go ├── heapq.go └── heapq_test.go ├── internal └── mdtest │ └── mdtest.go ├── mapset ├── example_test.go ├── mapset.go └── mapset_test.go ├── mbits ├── mbits.go └── mbits_test.go ├── mdiff ├── example_test.go ├── format.go ├── mdiff.go ├── mdiff_test.go ├── reader.go └── testdata │ ├── cdiff.txt │ ├── gdiff.txt │ ├── lhs.txt │ ├── odiff.txt │ ├── rhs.txt │ └── udiff.txt ├── mlink ├── example_test.go ├── list.go ├── list_test.go ├── mlink.go ├── mlink_test.go └── queue.go ├── mstr ├── mstr.go └── mstr_test.go ├── mtest ├── mtest.go └── mtest_test.go ├── omap ├── example_test.go ├── omap.go └── omap_test.go ├── queue ├── queue.go └── queue_test.go ├── ring ├── example_test.go ├── ring.go └── ring_test.go ├── shell ├── bench_test.go ├── internal_test.go ├── shell.go └── shell_test.go ├── slice ├── bench_test.go ├── edit.go ├── edit_test.go ├── example_test.go ├── lis.go ├── lis_test.go ├── slice.go ├── slice_test.go └── testdata │ ├── bad-lhs.txt │ └── bad-rhs.txt ├── stack ├── stack.go └── stack_test.go ├── stree ├── README.md ├── bench_test.go ├── cask.txt ├── cursor.go ├── example_test.go ├── internal_test.go ├── limerick.png ├── limerick.txt ├── node.go ├── stree.go └── stree_test.go └── value ├── example_test.go ├── maybe.go ├── maybe_test.go ├── value.go └── value_test.go /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | reviewers: 8 | - creachadair 9 | -------------------------------------------------------------------------------- /.github/workflows/go-presubmit.yml: -------------------------------------------------------------------------------- 1 | name: Go presubmit 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | types: [opened, reopened, synchronize] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: read 13 | 14 | jobs: 15 | build: 16 | name: Go presubmit 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | go-version: ['stable'] 21 | os: ['ubuntu-24.04'] 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Install Go ${{ matrix.go-version }} 25 | uses: actions/setup-go@v5 26 | with: 27 | go-version: ${{ matrix.go-version }} 28 | - uses: creachadair/go-presubmit-action@v2 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .go-update 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (C) 2015 and on, Michael J. Fromberger 4 | All Rights Reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | (1) Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | (2) Redistributions in binary form must reproduce the above copyright 13 | notice, this list of conditions and the following disclaimer in the 14 | documentation and/or other materials provided with the distribution. 15 | 16 | (3) The name of the author may not be used to endorse or promote products 17 | derived from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR "AS IS" AND ANY EXPRESS OR IMPLIED 20 | WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 21 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO 22 | EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 23 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 24 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING 27 | IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 28 | OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mds 2 | 3 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=white)](https://pkg.go.dev/github.com/creachadair/mds) 4 | [![CI](https://github.com/creachadair/mds/actions/workflows/go-presubmit.yml/badge.svg?event=push&branch=main)](https://github.com/creachadair/mds/actions/workflows/go-presubmit.yml) 5 | 6 | This repository defines generic data structures in Go. 7 | 8 | ## Data Structures 9 | 10 | Most of the types in this module share common behaviors: 11 | 12 | - A `Clear` method that discards all the contents of the container. 13 | - A `Peek` method that returns an order statistic of the container. 14 | - An `Each` method that iterates the container in its natural order (usable as a [range function](https://go.dev/blog/range-functions)). 15 | - An `IsEmpty` method that reports whether the container is empty. 16 | - A `Len` method that reports the number of elements in the container. 17 | 18 | ### Packages 19 | 20 | - [heapq](./heapq) a heap-structured priority queue ([package docs](https://godoc.org/github.com/creachadair/mds/heapq)) 21 | - [mapset](./mapset) a basic map-based set implementation ([package docs](https://godoc.org/github.com/creachadair/mds/mapset)) 22 | - [mlink](./mlink) basic linked sequences (list, queue) ([package docs](https://godoc.org/github.com/creachadair/mds/mlink)) 23 | - [omap](./omap) ordered key-value map ([package docs](https://godoc.org/github.com/creachadair/mds/omap)) 24 | - [queue](./queue) an array-based FIFO queue ([package docs](https://godoc.org/github.com/creachadair/mds/queue)) 25 | - [ring](./ring) a circular doubly-linked sequence ([package docs](https://godoc.org/github.com/creachadair/mds/ring)) 26 | - [stack](./stack) an array-based LIFO stack ([package docs](https://godoc.org/github.com/creachadair/mds/stack)) 27 | - [stree](./stree) self-balancing binary-search tree ([package docs](https://godoc.org/github.com/creachadair/mds/stree)) 28 | 29 | ## Utilities 30 | 31 | - [cache](./cache) an in-memory key/value cache ([package docs](https://godoc.org/github.com/creachadair/mds/cache)) 32 | - [distinct](./distinct) a probabilistic distinct-elements counter (CVM) ([package docs](https://godoc.org/github.com/creachadair/mds/distinct)) 33 | - [slice](./slice) helpful functions for manipulating slices ([package docs](https://godoc.org/github.com/creachadair/mds/slice)) 34 | - [mbits](./mbits) helpful functions for manipulating bits and bytes ([package docs](https://godoc.org/github.com/creachadair/mds/mbits)) 35 | - [mdiff](./mdiff) supports creating textual diffs ([package docs](https://godoc.org/github.com/creachadair/mds/mdiff), [example](https://go.dev/play/p/xUYbbwnMkw3)) 36 | - [mstr](./mstr) helpful functions for manipulating strings ([package docs](https://godoc.org/github.com/creachadair/mds/mstr)) 37 | - [mtest](./mtest) a support library for writing tests ([package docs](https://godoc.org/github.com/creachadair/mds/mtest)) 38 | - [shell](./shell) POSIX shell quoting and splitting ([package docs](https://godoc.org/github.com/creachadair/mds/shell)) 39 | - [value](./value) helpful functions for basic values and pointers ([package docs](https://godoc.org/github.com/creachadair/mds/value)) 40 | -------------------------------------------------------------------------------- /cache/cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/cache" 7 | "github.com/creachadair/mds/cache/internal/cachetest" 8 | gocmp "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestLRU(t *testing.T) { 12 | var victims []string 13 | 14 | wantVic := func(t *testing.T, want ...string) { 15 | t.Helper() 16 | if diff := gocmp.Diff(victims, want); diff != "" { 17 | t.Errorf("Victims (-got, +want):\n%s", diff) 18 | } 19 | } 20 | 21 | c := cache.New(cache.LRU[string, string](25). 22 | WithSize(cache.Length). 23 | 24 | // Record evictions so we can verify they happened in the expected order. 25 | OnEvict(func(key, _ string) { 26 | victims = append(victims, key) 27 | }), 28 | ) 29 | 30 | t.Run("New", func(t *testing.T) { 31 | cachetest.Run(t, c, "size = 0", "len = 0") 32 | }) 33 | 34 | t.Run("Fill", func(t *testing.T) { 35 | cachetest.Run(t, c, 36 | "put k1 abcde12345 = true", 37 | "size = 10", "len = 1", 38 | "put k2 fghij67890 = true", 39 | "size = 20", "len = 2", 40 | "put k3 12345 = true", 41 | ) 42 | wantVic(t) 43 | }) 44 | 45 | t.Run("Evict", func(t *testing.T) { 46 | cachetest.Run(t, c, 47 | "put k4 67890 = true", 48 | "len = 3", "size = 20", 49 | "put k5 lmnop = true", 50 | "len = 4", "size = 25", 51 | ) 52 | wantVic(t, "k1") // the eldest so far 53 | }) 54 | 55 | t.Run("Check", func(t *testing.T) { 56 | cachetest.Run(t, c, 57 | "has k1 = false", // was evicted, see above 58 | "has k2 = true", 59 | "has k3 = true", 60 | "has k4 = true", 61 | "has k5 = true", 62 | ) 63 | }) 64 | 65 | t.Run("Access", func(t *testing.T) { 66 | cachetest.Run(t, c, 67 | "get k2 = fghij67890 true", 68 | "get k3 = 12345 true", 69 | "get k7 = '' false", 70 | 71 | // Now k4 is the least-recently accessed 72 | ) 73 | }) 74 | 75 | t.Run("EvictMore", func(t *testing.T) { 76 | victims = nil 77 | 78 | // Size is 25, we add +10. This requires us to evict 10, and the oldest 79 | // eligible are k4 (-5) and k5 (-5). Then we have 15, + 10 == 25 again. 80 | // We are left with k2, k3, and k6 (the one we just added). 81 | cachetest.Run(t, c, 82 | "put k6 appleberry = true", 83 | "size = 25", "len = 3", 84 | "has k2 = true", "has k3 = true", "has k6 = true", 85 | ) 86 | wantVic(t, "k4", "k5") 87 | }) 88 | 89 | t.Run("TooBig", func(t *testing.T) { 90 | victims = nil 91 | 92 | // This value is too big to be cached, make sure it is rejected and that 93 | // it does not throw anything else out -- even if it overlaps with an 94 | // existing key. 95 | cachetest.Run(t, c, 96 | "put k2 1aaaa2bbbb3cccc4ddde5eeee6ffff = false", // length 30 > 25 97 | "len = 3", "size = 25", // we didn't remove anything 98 | "get k2 = fghij67890 true", // we still have the old value for k2 99 | ) 100 | wantVic(t) 101 | }) 102 | 103 | t.Run("Remove", func(t *testing.T) { 104 | cachetest.Run(t, c, "remove k3 = true", "len = 2", "size = 20") 105 | wantVic(t, "k3") 106 | }) 107 | 108 | t.Run("ReAdd", func(t *testing.T) { 109 | cachetest.Run(t, c, "put k3 stump = true", "len = 3", "size = 25") 110 | }) 111 | 112 | t.Run("Clear", func(t *testing.T) { 113 | // Clearing evicts everything, which at this point are k6, k2, and k3 in 114 | // decreasing order of access time (the get of k2 above promoted it). 115 | victims = nil 116 | cachetest.Run(t, c, "clear", "len = 0", "size = 0") 117 | wantVic(t, "k6", "k2", "k3") 118 | }) 119 | } 120 | -------------------------------------------------------------------------------- /cache/example_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/creachadair/mds/cache" 7 | ) 8 | 9 | func Example() { 10 | c := cache.New(cache.LRU[string, int](10)) 11 | for i := range 50 { 12 | c.Put(fmt.Sprint(i+1), i+1) 13 | } 14 | 15 | fmt.Println("size:", c.Size()) 16 | 17 | fmt.Println("has 1:", c.Has("1")) 18 | fmt.Println("has 40:", c.Has("40")) 19 | fmt.Println("has 41:", c.Has("41")) 20 | fmt.Println("has 50:", c.Has("50")) 21 | 22 | fmt.Println(c.Get("41")) // access the value 23 | 24 | c.Put("51", 51) 25 | 26 | fmt.Println("has 42:", c.Has("42")) // gone now 27 | fmt.Println(c.Get("41")) // still around 28 | 29 | c.Clear() 30 | fmt.Println("size:", c.Size()) 31 | 32 | // Output: 33 | // size: 10 34 | // has 1: false 35 | // has 40: false 36 | // has 41: true 37 | // has 50: true 38 | // 41 true 39 | // has 42: false 40 | // 41 true 41 | // size: 0 42 | } 43 | -------------------------------------------------------------------------------- /cache/internal/cachetest/cachetest.go: -------------------------------------------------------------------------------- 1 | // Package cachetest implements a test harness for cache implementations. 2 | package cachetest 3 | 4 | import ( 5 | "errors" 6 | "fmt" 7 | "strconv" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/creachadair/mds/cache" 12 | ) 13 | 14 | // An Op represents the operation code of an instruction. 15 | type Op string 16 | 17 | // The operation codes supported by a cache. 18 | const ( 19 | OpHas Op = "has" 20 | OpGet Op = "get" 21 | OpPut Op = "put" 22 | OpRemove Op = "remove" 23 | OpClear Op = "clear" 24 | OpLen Op = "len" 25 | OpSize Op = "size" 26 | ) 27 | 28 | // An insn is a single instruction in a cache test program. Each instruction 29 | // describes an operation to apply to the cache, the arguments to that 30 | // operation, and the expected results. 31 | type insn struct { 32 | Op Op // the operation to apply 33 | Key string // for has, get, put 34 | Value string // for put, remove 35 | 36 | resV string // for get, the expected value 37 | resOK bool // for has, get, put, remove 38 | resZ int64 // for len, size 39 | text string // for pretty-printing the instruction 40 | } 41 | 42 | func (in insn) String() string { return in.text } 43 | 44 | // Run compiles and evaluates the given test program on c. If the compilation 45 | // step fails, no operations are applied to c, and the test fails immediately. 46 | // Otherwise, the whole program is run and errors are logged as appropriate. 47 | // 48 | // The general format of a test program instruction is: 49 | // 50 | // opcode [args...] [= results ...] 51 | // 52 | // Arguments and results are separated by spaces. The number and types of the 53 | // arguments correspond to the operations on a cache, for example "get" takes a 54 | // single key and returns a value and a bool, while "len" takes no arguments 55 | // and returns an int. ParseInsn will report an error if the arguments and 56 | // results do not match the opcode. 57 | // 58 | // As a special case, the empty string can be written as ”. 59 | // 60 | // Examples 61 | // 62 | // len = 0 63 | // get foo = bar true 64 | // get quux = '' false 65 | // has nonesuch = false 66 | // clear 67 | func Run(t *testing.T, c *cache.Cache[string, string], prgm ...string) { 68 | t.Helper() 69 | 70 | var insn []insn 71 | for i, p := range prgm { 72 | ins, err := parseInsn(p) 73 | if err != nil { 74 | t.Fatalf("Line %d: parse %q: %v", i+1, p, err) 75 | } 76 | insn = append(insn, ins) 77 | } 78 | 79 | for i, ins := range insn { 80 | if err := ins.eval(c); err != nil { 81 | t.Errorf("Line %d: %s: %v", i+1, ins, err) 82 | } 83 | } 84 | } 85 | 86 | func (in insn) eval(c *cache.Cache[string, string]) error { 87 | switch in.Op { 88 | case OpHas: 89 | got := c.Has(in.Key) 90 | if got != in.resOK { 91 | return fmt.Errorf("c.Has(%q): got %v, want %v", in.Key, got, in.resOK) 92 | } 93 | case OpGet: 94 | got, ok := c.Get(in.Key) 95 | if got != in.resV || ok != in.resOK { 96 | return fmt.Errorf("c.Get(%q): got (%q, %v), want (%q, %v)", in.Key, got, ok, in.resV, in.resOK) 97 | } 98 | case OpPut: 99 | if got, want := c.Put(in.Key, in.Value), in.resOK; got != want { 100 | return fmt.Errorf("c.Put(%q, %q): got %v, want %v", in.Key, in.Value, got, want) 101 | } 102 | case OpRemove: 103 | if got, want := c.Remove(in.Key), in.resOK; got != want { 104 | return fmt.Errorf("c.Remove(%q): got %v, want %v", in.Key, got, want) 105 | } 106 | case OpClear: 107 | c.Clear() // cannot fail 108 | return nil 109 | case OpLen: 110 | if got, want := c.Len(), int(in.resZ); got != want { 111 | return fmt.Errorf("c.Len(): got %d, want %d", got, want) 112 | } 113 | case OpSize: 114 | if got, want := c.Size(), in.resZ; got != want { 115 | return fmt.Errorf("c.Size(): got %d, want %d", got, want) 116 | } 117 | default: 118 | panic(fmt.Sprintf("eval: unknown opcode %q", in.Op)) 119 | } 120 | return nil 121 | } 122 | 123 | // parseInsn parses an instruction from a string format. 124 | func parseInsn(s string) (insn, error) { 125 | op, tail, _ := strings.Cut(s, "=") 126 | args := strings.Fields(op) 127 | resp := strings.Fields(tail) 128 | if len(args) == 0 { 129 | return insn{}, errors.New("missing opcode") 130 | } 131 | 132 | out := insn{ 133 | Op: Op(args[0]), 134 | text: strings.Join(args, " "), // for the String method 135 | } 136 | if len(resp) != 0 { 137 | out.text += " = " + strings.Join(resp, " ") 138 | } 139 | 140 | // Check argument counts. 141 | var narg, nres int 142 | switch out.Op { 143 | case "": 144 | return insn{}, errors.New("missing opcode") 145 | case OpGet: 146 | narg, nres = 1, 2 147 | case OpHas, OpRemove: 148 | narg, nres = 1, 1 149 | case OpPut: 150 | narg, nres = 2, 1 151 | case OpClear: 152 | case OpLen, OpSize: 153 | narg, nres = 0, 1 154 | default: 155 | return insn{}, fmt.Errorf("unknown opcode %q", args[0]) 156 | } 157 | if len(args) != narg+1 { 158 | return insn{}, fmt.Errorf("op %q has %d args, want %d", args[0], len(args)-1, narg) 159 | } 160 | if len(resp) != nres { 161 | return insn{}, fmt.Errorf("op %q has %d results, want %d", args[0], len(resp), nres) 162 | } 163 | 164 | // Check argument and result types. 165 | switch out.Op { 166 | case OpHas, OpGet, OpPut, OpRemove: 167 | out.Key = args[1] 168 | b, err := strconv.ParseBool(resp[len(resp)-1]) 169 | if err != nil { 170 | return insn{}, fmt.Errorf("op %q result: %w", out.Op, err) 171 | } 172 | out.resOK = b 173 | case OpLen, OpSize: 174 | v, err := strconv.ParseInt(resp[0], 10, 64) 175 | if err != nil { 176 | return insn{}, fmt.Errorf("op %q result: %w", out.Op, err) 177 | } 178 | out.resZ = v 179 | } 180 | if out.Op == OpGet { 181 | out.resV = resp[0] 182 | if out.resV == "''" { 183 | out.resV = "" // notation for empty 184 | } 185 | } 186 | if out.Op == OpPut { 187 | out.Value = args[2] 188 | } 189 | return out, nil 190 | } 191 | -------------------------------------------------------------------------------- /cache/lru.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | 7 | "github.com/creachadair/mds/heapq" 8 | ) 9 | 10 | // lruStore is an implementation of the [Store] interface. 11 | // Eviction chooses the least-recently accessed elements first. 12 | type lruStore[Key comparable, Value any] struct { 13 | present map[Key]int // :: Key → offset in access 14 | access *heapq.Queue[prioKey[Key, Value]] 15 | clock int64 16 | 17 | // A linked list is asymptotically better than a heap, but the heap avoids 18 | // all the pointer indirections, allocates less, and leaves less garbage. 19 | } 20 | 21 | type prioKey[Key comparable, Value any] struct { 22 | lastAccess int64 23 | key Key 24 | value Value 25 | } 26 | 27 | func comparePrio[Key comparable, Value any](a, b prioKey[Key, Value]) int { 28 | return cmp.Compare(a.lastAccess, b.lastAccess) // logical time order 29 | } 30 | 31 | // LRU constructs a [Config] with a cache store with the specified capacity 32 | // limit that manages entries with a least-recently used eviction policy. 33 | func LRU[Key comparable, Value any](limit int64) Config[Key, Value] { 34 | lru := &lruStore[Key, Value]{ 35 | present: make(map[Key]int), 36 | access: heapq.New(comparePrio[Key, Value]), 37 | } 38 | lru.access.Update(func(v prioKey[Key, Value], pos int) { 39 | lru.present[v.key] = pos 40 | }) 41 | return Config[Key, Value]{limit: limit, store: lru} 42 | } 43 | 44 | // Check implements part of the [Store] interface. 45 | func (c *lruStore[Key, Value]) Check(key Key) (Value, bool) { 46 | pos, ok := c.present[key] 47 | if !ok { 48 | var zero Value 49 | return zero, false 50 | } 51 | elt, ok := c.access.Peek(pos) 52 | return elt.value, ok 53 | } 54 | 55 | // Access implements part of the [Store] interface. 56 | func (c *lruStore[Key, Value]) Access(key Key) (Value, bool) { 57 | pos, ok := c.present[key] 58 | if !ok { 59 | var zero Value 60 | return zero, false 61 | } 62 | c.clock++ // this counts as an access 63 | 64 | // Remove the item at its existing priority, and re-add it as the most 65 | // recent access. Only the timestamp matters for order. 66 | out, _ := c.access.Remove(pos) // cannot fail 67 | out.lastAccess = c.clock 68 | c.access.Add(out) 69 | return out.value, true 70 | } 71 | 72 | // Store implements part of the [Store] interface. 73 | func (c *lruStore[Key, Value]) Store(key Key, val Value) { 74 | if _, ok := c.present[key]; ok { 75 | panic(fmt.Sprintf("lru store: unexpected key %v", key)) 76 | } 77 | 78 | c.clock++ 79 | pos := c.access.Add(prioKey[Key, Value]{ 80 | lastAccess: c.clock, 81 | key: key, 82 | value: val, 83 | }) 84 | c.present[key] = pos 85 | } 86 | 87 | // Remove implements part of the [Store] interface. 88 | func (c *lruStore[Key, _]) Remove(key Key) { 89 | pos, ok := c.present[key] 90 | if ok { 91 | c.access.Remove(pos) 92 | delete(c.present, key) 93 | } 94 | } 95 | 96 | // Evict implements part of the [Store] interface. 97 | func (c *lruStore[Key, Value]) Evict() (Key, Value) { 98 | out, ok := c.access.Pop() 99 | if !ok { 100 | panic("lru evict: no entries left") 101 | } 102 | delete(c.present, out.key) 103 | return out.key, out.value 104 | } 105 | -------------------------------------------------------------------------------- /compare/compare.go: -------------------------------------------------------------------------------- 1 | // Package compare contains support functions for comparison of values. 2 | // 3 | // # Comparison Functions 4 | // 5 | // For the purposes of this package, a comparison function takes two values A 6 | // and B of a type and reports their relative order, returning: 7 | // 8 | // -1 if A precedes B, 9 | // 0 if A and B are equivalent, 10 | // +1 if A follows B 11 | // 12 | // Comparison functions are expected to implement a strict weak ordering. 13 | // Unless otherwise noted, any negative value is accepted in place of -1, and 14 | // any positive value in place of 1. 15 | // 16 | // # Less Functions 17 | // 18 | // For the purposes of this package, a less function takes two values A and B 19 | // of a type and reports whether A precedes B in relative order. 20 | package compare 21 | 22 | import "time" 23 | 24 | // FromLessFunc converts a less function, which reports whether its first 25 | // argument precedes its second in an ordering relation, into a comparison 26 | // function on that same relation. 27 | func FromLessFunc[T any](less func(a, b T) bool) func(a, b T) int { 28 | return func(a, b T) int { 29 | if less(a, b) { 30 | return -1 31 | } else if less(b, a) { 32 | return 1 33 | } 34 | return 0 35 | } 36 | } 37 | 38 | // ToLessFunc converts a comparison function into a less function on the same 39 | // relation. 40 | func ToLessFunc[T any](cmp func(a, b T) int) func(a, b T) bool { 41 | return func(a, b T) bool { return cmp(a, b) < 0 } 42 | } 43 | 44 | // Time is a comparison function for time.Time values that orders earlier times 45 | // before later ones. 46 | func Time(a, b time.Time) int { return a.Compare(b) } 47 | 48 | // Reversed returns a comparison function that orders its elements in the 49 | // reverse of the ordering expressed by c. 50 | func Reversed[T any](c func(a, b T) int) func(a, b T) int { 51 | return func(a, b T) int { return -c(a, b) } 52 | } 53 | 54 | // Bool is a comparison function for bool values that orders false before true. 55 | func Bool(a, b bool) int { 56 | if a == b { 57 | return 0 58 | } else if a { 59 | return 1 60 | } 61 | return -1 62 | } 63 | -------------------------------------------------------------------------------- /compare/compare_test.go: -------------------------------------------------------------------------------- 1 | package compare_test 2 | 3 | import ( 4 | "cmp" 5 | "math/rand/v2" 6 | "slices" 7 | "testing" 8 | "time" 9 | 10 | "github.com/creachadair/mds/compare" 11 | ) 12 | 13 | func TestConversion(t *testing.T) { 14 | for _, less := range [](func(a, b int) bool){ 15 | func(a, b int) bool { return a < b }, 16 | func(a, b int) bool { return a > b }, 17 | } { 18 | cmp := compare.FromLessFunc(less) 19 | cless := compare.ToLessFunc(cmp) 20 | 21 | for range 1000 { 22 | m := rand.IntN(1000) - 500 23 | n := rand.IntN(1000) - 500 24 | 25 | mn, nm := less(m, n), less(n, m) 26 | if mn && nm { 27 | t.Fatalf("Invalid less function: %d < %d and %d < %d", m, n, n, m) 28 | } 29 | diff := cmp(m, n) 30 | switch { 31 | case mn: 32 | if diff >= 0 { 33 | t.Errorf("Compare %d %d: got %v, want ≥ 0", m, n, diff) 34 | } 35 | if !cless(m, n) { 36 | t.Errorf("Less %d %d: got false, want true", m, n) 37 | } 38 | case nm: 39 | if diff <= 0 { 40 | t.Errorf("Compare %d %d: got %v, want ≤ 0", m, n, diff) 41 | } 42 | if cless(m, n) { 43 | t.Errorf("Less %d %d: got true, want false", m, n) 44 | } 45 | default: 46 | if diff != 0 { 47 | t.Errorf("Compare %d %d: got %v, want 0", m, n, diff) 48 | } 49 | if cless(m, n) { 50 | t.Errorf("Less %d %d: got true, want false", m, n) 51 | } 52 | if cless(n, m) { 53 | t.Errorf("Less %d %d: got true, want false", n, m) 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | func TestTime(t *testing.T) { 61 | ptime := func(s string) time.Time { 62 | ts, err := time.Parse(time.RFC3339Nano, s) 63 | if err != nil { 64 | t.Fatalf("Parse time %q: %v", s, err) 65 | } 66 | return ts 67 | } 68 | tests := []struct { 69 | a, b string // RFC3339 70 | want int 71 | }{ 72 | {"1989-11-09T17:53:00Z", "1989-11-09T17:53:00Z", 0}, 73 | {"2009-11-10T19:00:00-04:00", "2009-11-10T23:00:00Z", 0}, 74 | {"1983-11-20T18:30:45-08:00", "1983-11-21T06:00:00+01:00", -1}, 75 | {"2022-01-31T12:00:00Z", "2021-01-31T12:00:00Z", 1}, 76 | } 77 | for _, tc := range tests { 78 | got := compare.Time(ptime(tc.a), ptime(tc.b)) 79 | if got != tc.want { 80 | t.Errorf("Compare %s ? %s: got %d, want %d", tc.a, tc.b, got, tc.want) 81 | } 82 | rev := compare.Time(ptime(tc.b), ptime(tc.a)) 83 | switch { 84 | case got < 0 && rev <= 0, 85 | got > 0 && rev >= 0, 86 | got == 0 && rev != 0: 87 | t.Errorf("Compare %s ? %s: strict weak order violation: %d / %d", tc.b, tc.a, got, rev) 88 | } 89 | } 90 | } 91 | 92 | func TestReversed(t *testing.T) { 93 | buf := make([]int, 37) 94 | for i := range buf { 95 | buf[i] = i 96 | } 97 | 98 | cz := cmp.Compare[int] 99 | rev := compare.Reversed(cz) 100 | 101 | slices.SortFunc(buf, rev) 102 | for i := range len(buf) - 1 { 103 | if buf[i] <= buf[i+1] { 104 | t.Errorf("Output disordered at %d: %d <= %d", i, buf[i], buf[i+1]) 105 | } 106 | } 107 | 108 | slices.SortFunc(buf, compare.Reversed(rev)) 109 | if !slices.IsSorted(buf) { 110 | t.Errorf("Reversed output is not sorted: %v", buf) 111 | } 112 | } 113 | 114 | func TestBool(t *testing.T) { 115 | tests := []struct { 116 | a, b bool 117 | want int 118 | }{ 119 | {false, false, 0}, 120 | {false, true, -1}, 121 | {true, false, 1}, 122 | {true, true, 0}, 123 | } 124 | for _, tc := range tests { 125 | if got := compare.Bool(tc.a, tc.b); got != tc.want { 126 | t.Errorf("Bool(%v, %v): got %v, want %v", tc.a, tc.b, got, tc.want) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /distinct/distinct.go: -------------------------------------------------------------------------------- 1 | // Package distinct implements the probabilistic distinct-elements counter 2 | // algorithm of Chakraborty, Vinodchandran, and Meel as described in the paper 3 | // "Distinct Elements in Streams" ([CVM]). 4 | // 5 | // [CVM]: https://arxiv.org/pdf/2301.10191 6 | package distinct 7 | 8 | import ( 9 | crand "crypto/rand" 10 | "fmt" 11 | "math" 12 | "math/bits" 13 | "math/rand/v2" 14 | 15 | "github.com/creachadair/mds/mapset" 16 | ) 17 | 18 | // A Counter estimates the number of distinct comparable elements that have 19 | // been passed to its Add method using the CVM algorithm. 20 | // 21 | // Add elements to a counter using [Counter.Add] method; use [Counter.Count] to 22 | // obtain the current estimate of the number of distinct elements observed. 23 | type Counter[T comparable] struct { 24 | buf mapset.Set[T] 25 | cap int // maximum allowed size of buf 26 | p uint64 // eviction probability (see below) 27 | rng rand.Source 28 | 29 | // To avoid the need for floating-point calculations during update, we 30 | // express the probability as a fixed-point threshold in 0..MaxUint64, where 31 | // 0 denotes probability 0 and ~0 denotes probability 1. 32 | } 33 | 34 | // NewCounter constructs a new empty distinct-elements counter using a buffer 35 | // of at most size elements for estimation. 36 | // 37 | // A newly-constructed counter does not pre-allocate the full buffer size. It 38 | // begins with a small buffer that grows as needed up to the limit. 39 | func NewCounter[T comparable](size int) *Counter[T] { 40 | var seed [32]byte 41 | if _, err := crand.Read(seed[:]); err != nil { 42 | panic(fmt.Sprintf("seed RNG: %v", err)) 43 | } 44 | return &Counter[T]{ 45 | buf: make(mapset.Set[T]), 46 | cap: size, 47 | p: math.MaxUint64, 48 | rng: rand.NewChaCha8(seed), 49 | } 50 | } 51 | 52 | // Len reports the number of elements currently buffered by c. 53 | func (c *Counter[T]) Len() int { return c.buf.Len() } 54 | 55 | // Reset resets c to its initial state, as if freshly constructed. 56 | // The internal buffer size limit remains unchanged. 57 | func (c *Counter[T]) Reset() { c.buf.Clear(); c.p = math.MaxUint64 } 58 | 59 | // Add adds v to the counter. 60 | func (c *Counter[T]) Add(v T) { 61 | if c.p < math.MaxUint64 && c.rng.Uint64() >= c.p { 62 | // The first check avoids spending source entropy unless we need to. 63 | // 64 | // TODO(creachadair): We could reuse a single roll multiple times if we 65 | // kept the pass count and masked off that many bits from a register. 66 | // But then we have more state, and I'm not sure it's worth it. 67 | c.buf.Remove(v) 68 | return 69 | } 70 | c.buf.Add(v) 71 | if c.buf.Len() >= c.cap { 72 | // Instead of flipping a coin for each element, grab blocks of 64 random 73 | // bits and use them directly, refilling only as needed. 74 | var nb, rnd uint64 75 | 76 | for elt := range c.buf { 77 | if nb == 0 { 78 | rnd = c.rng.Uint64() // refill 79 | nb = 64 80 | } 81 | if rnd&1 == 0 { 82 | c.buf.Remove(elt) 83 | } 84 | rnd >>= 1 85 | nb-- 86 | } 87 | c.p >>= 1 88 | } 89 | } 90 | 91 | // Count returns the current estimate of the number of distinct elements 92 | // observed by the counter. 93 | func (c *Counter[T]) Count() uint64 { 94 | // The estimate is |X| / p, where p = 1/2^k after k eviction passes. 95 | // To convert our fixed-point probability, note that: 96 | // 97 | // |X| / p == |X| * (1/p) == |X| * 2^k 98 | // 99 | // The number of leading zeroes of c.p records k, so we can do this all in 100 | // fixed-point arithmetic with no floating point conversion. 101 | p2k := uint64(1) << uint64(bits.LeadingZeros64(c.p)) 102 | return uint64(c.buf.Len()) * p2k 103 | } 104 | 105 | // BufferSize returns a buffer size sufficient to ensure that a counter using 106 | // this size will produce estimates within (1 ± ε) times the true count with 107 | // probability (1 - δ), assuming the expected total number of elements to be 108 | // counted is expSize. 109 | // 110 | // The suggested buffer size guarantees these constraints, but note that the 111 | // Chernoff bound estimate is very conservative. In practice, the actual 112 | // estimates will usually be much more accurate. Empirically, values of ε and δ 113 | // in the 0.05 range work well. 114 | func BufferSize(ε, δ float64, expSize int) int { 115 | if ε < 0 || ε > 1 { 116 | panic(fmt.Sprintf("error bound out of range: %v", ε)) 117 | } 118 | if δ < 0 || δ > 1 { 119 | panic(fmt.Sprintf("error rate out of range: %v", δ)) 120 | } 121 | if expSize <= 0 { 122 | panic(fmt.Sprintf("expected size must be positive: %d", expSize)) 123 | } 124 | 125 | v := math.Ceil((12 / (ε * ε)) * math.Log2((8*float64(expSize))/δ)) 126 | return int(v) 127 | } 128 | -------------------------------------------------------------------------------- /distinct/distinct_test.go: -------------------------------------------------------------------------------- 1 | package distinct_test 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "math" 7 | "testing" 8 | 9 | "math/rand/v2" 10 | 11 | "github.com/creachadair/mds/distinct" 12 | "github.com/creachadair/mds/mapset" 13 | ) 14 | 15 | var ( 16 | errRate = flag.Float64("error-rate", 0.06, "Error rate") 17 | failProb = flag.Float64("fail-probability", 0.02, "Failure probability") 18 | ) 19 | 20 | func fill(c *distinct.Counter[int], n int) mapset.Set[int] { 21 | actual := mapset.New[int]() 22 | for range n { 23 | r := rand.Int() 24 | actual.Add(r) 25 | c.Add(r) 26 | } 27 | return actual 28 | } 29 | 30 | func TestCounter(t *testing.T) { 31 | t.Run("Empty", func(t *testing.T) { 32 | // An empty counter should report no elements. 33 | c := distinct.NewCounter[int](100) 34 | if got := c.Count(); got != 0 { 35 | t.Errorf("Empty count: got %d, want 0", got) 36 | } 37 | }) 38 | 39 | t.Run("Small", func(t *testing.T) { 40 | // A counter that has seen fewer values than its buffer size should count 41 | // perfectly. 42 | c := distinct.NewCounter[int](100) 43 | want := len(fill(c, 50)) 44 | if got := c.Len(); got != want { 45 | t.Errorf("Small count: got %d, want %d", got, want) 46 | } 47 | }) 48 | 49 | t.Logf("Error rate: %g%%", 100**errRate) 50 | t.Logf("Failure probability: %g%%", 100**failProb) 51 | for _, tc := range []int{9_999, 100_000, 543_210, 1_000_000, 1_048_576} { 52 | name := fmt.Sprintf("Large/%d", tc) 53 | t.Run(name, func(t *testing.T) { 54 | size := distinct.BufferSize(*errRate, *failProb, tc) 55 | t.Logf("Buffer size estimate: %d", size) 56 | 57 | c := distinct.NewCounter[int](size) 58 | actual := fill(c, tc) 59 | 60 | t.Logf("Actual count: %d", actual.Len()) 61 | t.Logf("Estimated count: %d", c.Count()) 62 | t.Logf("Buffer size: %d", c.Len()) 63 | 64 | e := observedErrorRate(int(c.Count()), actual.Len()) 65 | t.Logf("Error: %.4g%%", 100*e) 66 | 67 | if math.Abs(e) > *errRate { 68 | t.Errorf("Error rate = %f, want ≤ %f", e, *errRate) 69 | } 70 | if c.Len() > size { 71 | t.Errorf("Buffer size is %d > %d", c.Len(), size) 72 | } 73 | 74 | // After counting, a reset should leave the buffer empty. 75 | c.Reset() 76 | if got := c.Len(); got != 0 { 77 | t.Errorf("After reset: buffer size is %d, want 0", got) 78 | } 79 | }) 80 | } 81 | 82 | t.Run("Saturate", func(t *testing.T) { 83 | // To achieve a ± 5% error rate for 1M inputs, we theoretically need 84 | // about 142K buffer slots. With 10K buffer slots the expected error rate 85 | // for 1M inputs is about ± 18.8%. The predicted bound is correct, but is 86 | // very conservative: With high probability the error will be much less, 87 | // even when we greatly exceed the predicted load. 88 | // 89 | // In several hundred runs with random inputs, this configuration did not 90 | // exceed 5% error, although it could have done so. 91 | 92 | c := distinct.NewCounter[int](10_000) 93 | var actual mapset.Set[int] 94 | var maxErr float64 95 | for i := 0; i < 1_000_000; i += 500 { 96 | actual.AddAll(fill(c, 500)) 97 | e := observedErrorRate(int(c.Count()), actual.Len()) 98 | if math.Abs(e) > math.Abs(maxErr) { 99 | maxErr = e 100 | t.Logf("At %d unique items, max error is %.4g%%", actual.Len(), 100*maxErr) 101 | } 102 | } 103 | t.Logf("Actual count: %d", actual.Len()) 104 | t.Logf("Estimated count: %d", c.Count()) 105 | t.Logf("Buffer size: %d", c.Len()) 106 | t.Logf("Max error: %.4g%%", 100*maxErr) 107 | }) 108 | } 109 | 110 | func observedErrorRate(got, want int) float64 { return float64(got-want) / float64(want) } 111 | -------------------------------------------------------------------------------- /distinct/example_test.go: -------------------------------------------------------------------------------- 1 | package distinct_test 2 | 3 | import ( 4 | "fmt" 5 | "math/rand/v2" 6 | "os" 7 | 8 | "github.com/creachadair/mds/distinct" 9 | "github.com/creachadair/mds/mapset" 10 | ) 11 | 12 | func Example() { 13 | // Suggest how big a buffer we need to have an estimate within ± 10% of the 14 | // true value with 95% probability, given we expect to see 60000 inputs. 15 | bufSize := distinct.BufferSize(0.1, 0.05, 60000) 16 | 17 | // Construct a counter with the specified buffer size limit. 18 | c := distinct.NewCounter[int](bufSize) 19 | 20 | // For demonstration purposes, keep track of the actual count. 21 | // This will generally be impractical for "real" workloads. 22 | var unique mapset.Set[int] 23 | 24 | // Observe some (50,000) random inputs... 25 | for range 50000 { 26 | r := rand.IntN(80000) 27 | c.Add(r) 28 | 29 | unique.Add(r) 30 | } 31 | 32 | fmt.Printf("Buffer limit: %d\n", bufSize) 33 | fmt.Fprintf(os.Stderr, "Unique: %d\n", unique.Len()) 34 | fmt.Fprintf(os.Stderr, "Estimate: %d\n", c.Count()) 35 | fmt.Fprintf(os.Stderr, "Buffer used: %d\n", c.Len()) 36 | 37 | // N.B.: Counter results are intentionally omitted here. The exact values 38 | // are not stable even if the RNG is fixed, because the counter uses map 39 | // iteration during update. 40 | 41 | // Output: 42 | // Buffer limit: 27834 43 | } 44 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/creachadair/mds 2 | 3 | go 1.23 4 | 5 | require github.com/google/go-cmp v0.6.0 6 | 7 | require ( 8 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 // indirect 9 | golang.org/x/mod v0.17.0 // indirect 10 | golang.org/x/sync v0.7.0 // indirect 11 | golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 // indirect 12 | honnef.co/go/tools v0.5.1 // indirect 13 | ) 14 | 15 | tool honnef.co/go/tools/staticcheck 16 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= 2 | github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= 3 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 5 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678 h1:1P7xPZEwZMoBoz0Yze5Nx2/4pxj6nw9ZqHWXqP0iRgQ= 6 | golang.org/x/exp/typeparams v0.0.0-20231108232855-2478ac86f678/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= 7 | golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= 8 | golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 9 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 10 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 11 | golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3 h1:SHq4Rl+B7WvyM4XODon1LXtP7gcG49+7Jubt1gWWswY= 12 | golang.org/x/tools v0.21.1-0.20240531212143-b6235391adb3/go.mod h1:bqv7PJ/TtlrzgJKhOAGdDUkUltQapRik/UEHubLVBWo= 13 | honnef.co/go/tools v0.5.1 h1:4bH5o3b5ZULQ4UrBmP+63W9r7qIkqJClEA9ko5YKx+I= 14 | honnef.co/go/tools v0.5.1/go.mod h1:e9irvo83WDG9/irijV44wr3tbhcFeRnfpVlRqVwpzMs= 15 | -------------------------------------------------------------------------------- /heapq/example_test.go: -------------------------------------------------------------------------------- 1 | package heapq_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/creachadair/mds/heapq" 7 | ) 8 | 9 | func ExampleNew() { 10 | q := heapq.New(intCompare) 11 | 12 | q.Add(8) 13 | q.Add(6) 14 | q.Add(7) 15 | q.Add(5) 16 | q.Add(3) 17 | q.Add(0) 18 | q.Add(9) 19 | 20 | fmt.Println("length before", q.Len()) 21 | fmt.Println(q.Pop()) 22 | fmt.Println(q.Pop()) 23 | fmt.Println(q.Pop()) 24 | fmt.Println("length after", q.Len()) 25 | fmt.Println("front", q.Front()) 26 | 27 | // Output: 28 | // length before 7 29 | // 0 true 30 | // 3 true 31 | // 5 true 32 | // length after 4 33 | // front 6 34 | } 35 | -------------------------------------------------------------------------------- /heapq/heapq.go: -------------------------------------------------------------------------------- 1 | // Package heapq implements a generic heap-structured priority queue. 2 | package heapq 3 | 4 | // A Queue is a heap-structured priority queue. The contents of a Queue are 5 | // partially ordered, and the minimum element is accessible in constant time. 6 | // Adding or removing an element has worst-case time complexity O(lg n). 7 | // 8 | // The order of elements in the Queue is determined by a comparison function 9 | // provided when the queue is constructed. 10 | type Queue[T any] struct { 11 | data []T 12 | cmp func(a, b T) int 13 | move func(T, int) 14 | } 15 | 16 | // nmove is a no-op move function used by default in a queue on which no update 17 | // function has been set. 18 | func nmove[T any](T, int) {} 19 | 20 | // New constructs an empty Queue with the given comparison function, where 21 | // cmp(a, b) must be <0 if a < b, =0 if a == b, and >0 if a > b. 22 | func New[T any](cmp func(a, b T) int) *Queue[T] { return &Queue[T]{cmp: cmp, move: nmove[T]} } 23 | 24 | // NewWithData constructs an empty Queue with the given comparison function 25 | // that uses the given slice as storage. This allows the caller to initialize 26 | // a heap with existing data without copying, or to preallocate storage. To 27 | // preallocate storage without any initial values, pass a slice with length 0 28 | // and the desired capacity. 29 | // 30 | // For example, to initialize a queue with fixed elements: 31 | // 32 | // q := heapq.NewWithData(cfunc, []string{"u", "v", "w", "x", "y"}) 33 | // 34 | // To initialize an empty queue with a pre-allocated buffer of n elements: 35 | // 36 | // q := heapq.NewWithData(cfunc, make([]string, 0, n)) 37 | // 38 | // The resulting queue takes ownership of the slice, and the caller should not 39 | // access the contents data after the call unless the queue will no longer be 40 | // used. 41 | func NewWithData[T any](cmp func(a, b T) int, data []T) *Queue[T] { 42 | q := &Queue[T]{data: data, cmp: cmp, move: nmove[T]} 43 | for i := len(q.data) / 2; i >= 0; i-- { 44 | q.pushDown(i) 45 | } 46 | return q 47 | } 48 | 49 | // Update sets u as the update function on q. This function is called whenever 50 | // an element of the queue is moved to a new position, giving the value and its 51 | // new position. If u == nil, an existing update function is removed. Update 52 | // returns q to allow chaining. 53 | // 54 | // Setting an update function makes q intrusive, allowing values in the queue 55 | // to keep track of their current offset in the queue as items are added and 56 | // removed. By default location information is not reported. 57 | func (q *Queue[T]) Update(u func(T, int)) *Queue[T] { 58 | if u == nil { 59 | q.move = nmove[T] 60 | } else { 61 | q.move = u 62 | } 63 | return q 64 | } 65 | 66 | // Len reports the number of elements in the queue. This is a constant-time operation. 67 | func (q *Queue[T]) Len() int { return len(q.data) } 68 | 69 | // IsEmpty reports whether the queue is empty. 70 | func (q *Queue[T]) IsEmpty() bool { return len(q.data) == 0 } 71 | 72 | // Front returns the frontmost element of the queue. If the queue is empty, it 73 | // returns a zero value. 74 | func (q *Queue[T]) Front() T { 75 | if len(q.data) == 0 { 76 | var zero T 77 | return zero 78 | } 79 | return q.data[0] 80 | } 81 | 82 | // Peek reports whether q has a value at offset n from the front of the queue, 83 | // and if so returns its value. Peek(0) returns the same value as Front. The 84 | // order of elements at offsets n > 0 is unspecified. 85 | // 86 | // Peek will panic if n < 0. 87 | func (q *Queue[T]) Peek(n int) (T, bool) { 88 | if n < 0 { 89 | panic("index out of range") 90 | } else if n >= len(q.data) { 91 | var zero T 92 | return zero, false 93 | } 94 | return q.data[n], true 95 | } 96 | 97 | // Pop reports whether the queue contains any elements, and if so removes and 98 | // returns the frontmost element. It returns a zero value if q is empty. 99 | func (q *Queue[T]) Pop() (T, bool) { 100 | if len(q.data) == 0 { 101 | var zero T 102 | return zero, false 103 | } 104 | return q.pop(0), true 105 | } 106 | 107 | // Add adds v to the queue. It returns the index in q where v is stored. 108 | func (q *Queue[T]) Add(v T) int { 109 | n := len(q.data) 110 | q.data = append(q.data, v) 111 | q.move(q.data[n], n) 112 | return q.pushUp(n) 113 | } 114 | 115 | // Remove reports whether q has a value at offset n from the front of the 116 | // queue, and if so removes and returns it. Remove(0) is equivalent to Pop(). 117 | // 118 | // Remove will panic if n < 0. 119 | func (q *Queue[T]) Remove(n int) (T, bool) { 120 | if n < 0 { 121 | panic("index out of range") 122 | } else if n >= len(q.data) { 123 | var zero T 124 | return zero, false 125 | } 126 | return q.pop(n), true 127 | } 128 | 129 | // Set replaces the contents of q with the specified values. Any previous 130 | // values in the queue are discarded. This operation takes time proportional to 131 | // len(vs) to restore heap order. Set returns q to allow chaining. 132 | func (q *Queue[T]) Set(vs []T) *Queue[T] { 133 | // Copy the values so we do not alias the original slice. 134 | // If the existing buffer already has enough space, reslice it; otherwise 135 | // allocate a fresh one. 136 | if cap(q.data) < len(vs) { 137 | q.data = make([]T, len(vs)) 138 | } else { 139 | q.data = q.data[:len(vs)] 140 | } 141 | copy(q.data, vs) 142 | for i := len(q.data) - 1; i >= 0; i-- { 143 | q.move(q.data[i], i) 144 | q.pushDown(i) 145 | } 146 | return q 147 | } 148 | 149 | // Reorder replaces the ordering function for q with a new function. This 150 | // operation takes time proportional to the length of the queue to restore the 151 | // (new) heap order. The queue retains the same elements. 152 | func (q *Queue[T]) Reorder(cmp func(a, b T) int) { 153 | q.cmp = cmp 154 | for i := len(q.data) / 2; i >= 0; i-- { 155 | q.pushDown(i) 156 | } 157 | } 158 | 159 | // Each is a range function that calls f with each value in q in heap order. 160 | // If f returns false, Each returns immediately. 161 | func (q *Queue[T]) Each(f func(T) bool) { 162 | for _, v := range q.data { 163 | if !f(v) { 164 | return 165 | } 166 | } 167 | } 168 | 169 | // Clear discards all the entries in q, leaving it empty. 170 | func (q *Queue[T]) Clear() { q.data = q.data[:0] } 171 | 172 | // pop removes and returns the value at index i of the heap, after restoring 173 | // heap order. Precondition: i < len(q.data). 174 | func (q *Queue[T]) pop(i int) T { 175 | out := q.data[i] 176 | n := len(q.data) - 1 177 | if n == 0 { 178 | q.data = q.data[:0] 179 | } else { 180 | q.data[i], q.data[n] = q.data[n], out 181 | q.move(q.data[i], i) // N.B. we do not report a move of out. 182 | q.data = q.data[:n] 183 | q.pushDown(i) 184 | } 185 | return out 186 | } 187 | 188 | // pushUp pushes the value at index i of the heap up until it is correctly 189 | // ordered relative to its parent, and returns the resulting heap index. 190 | func (q *Queue[T]) pushUp(i int) int { 191 | for i > 0 { 192 | par := i / 2 193 | if q.cmp(q.data[i], q.data[par]) >= 0 { 194 | break 195 | } 196 | q.swap(i, par) 197 | i = par 198 | } 199 | return i 200 | } 201 | 202 | // pushDown pushes the value at index i of the heap down until it is correctly 203 | // ordered relative to its children, and returns the resulting heap index. 204 | func (q *Queue[T]) pushDown(i int) int { 205 | lc := 2*i + 1 206 | for lc < len(q.data) { 207 | min := i 208 | if q.cmp(q.data[lc], q.data[min]) < 0 { 209 | min = lc 210 | } 211 | if rc := lc + 1; rc < len(q.data) && q.cmp(q.data[rc], q.data[min]) < 0 { 212 | min = rc 213 | } 214 | if min == i { 215 | break // no more work to do 216 | } 217 | q.swap(i, min) 218 | i, lc = min, 2*min+1 219 | } 220 | return i 221 | } 222 | 223 | // swap exchanges the elements at positions i and j of the heap, invoking the 224 | // update function as needed. 225 | func (q *Queue[T]) swap(i, j int) { 226 | q.data[i], q.data[j] = q.data[j], q.data[i] 227 | q.move(q.data[i], i) 228 | q.move(q.data[j], j) 229 | } 230 | 231 | // Sort reorders the contents of vs in-place using the heap-sort algorithm, in 232 | // non-decreasing order by the comparison function provided. 233 | func Sort[T any](cmp func(a, b T) int, vs []T) { 234 | if len(vs) < 2 { 235 | return 236 | } 237 | rcmp := func(a, b T) int { return -cmp(a, b) } 238 | q := NewWithData(rcmp, vs) 239 | for !q.IsEmpty() { 240 | q.Pop() 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /heapq/heapq_test.go: -------------------------------------------------------------------------------- 1 | package heapq_test 2 | 3 | import ( 4 | "cmp" 5 | "math/rand/v2" 6 | "sort" 7 | "testing" 8 | 9 | "github.com/creachadair/mds/compare" 10 | "github.com/creachadair/mds/heapq" 11 | "github.com/creachadair/mds/internal/mdtest" 12 | gocmp "github.com/google/go-cmp/cmp" 13 | ) 14 | 15 | var _ mdtest.Shared[any] = (*heapq.Queue[any])(nil) 16 | 17 | var intCompare = cmp.Compare[int] 18 | var revIntCompare = compare.Reversed(cmp.Compare[int]) 19 | 20 | func TestHeap(t *testing.T) { 21 | t.Run("New", func(t *testing.T) { 22 | runTests(t, heapq.New(intCompare)) 23 | }) 24 | t.Run("NewWithData", func(t *testing.T) { 25 | buf := make([]int, 0, 64) 26 | runTests(t, heapq.NewWithData(intCompare, buf)) 27 | }) 28 | } 29 | 30 | func runTests(t *testing.T, q *heapq.Queue[int]) { 31 | t.Helper() 32 | 33 | check := func(want ...int) { 34 | t.Helper() 35 | var got []int 36 | for v := range q.Each { 37 | got = append(got, v) 38 | } 39 | if diff := gocmp.Diff(want, got); diff != "" { 40 | t.Errorf("Queue contents (+want, -got):\n%s", diff) 41 | t.Logf("Got: %v", got) 42 | t.Logf("Want: %v", want) 43 | } 44 | if len(want) != 0 { 45 | if got := q.Front(); got != want[0] { 46 | t.Errorf("Front: got %v, want %v", got, want[0]) 47 | } 48 | } 49 | if got := q.Len(); got != len(want) { 50 | t.Errorf("Len: got %v, want %v", got, len(want)) 51 | } 52 | } 53 | checkAdd := func(v, want int) { 54 | if got := q.Add(v); got != want { 55 | t.Errorf("Add(%v): got %v, want %v", v, got, want) 56 | } 57 | } 58 | checkPop := func(want int, wantok bool) { 59 | got, ok := q.Pop() 60 | if got != want || ok != wantok { 61 | t.Errorf("Pop: got (%v, %v), want (%v, %v)", got, ok, want, wantok) 62 | } 63 | } 64 | checkRemove := func(n, want int, wantok bool) { 65 | got, ok := q.Remove(n) 66 | if got != want || ok != wantok { 67 | t.Errorf("Remove(%d): got (%v, %v), want (%v, %v)", n, got, ok, want, wantok) 68 | } 69 | } 70 | 71 | check() 72 | checkPop(0, false) 73 | 74 | checkAdd(10, 0) 75 | check(10) 76 | checkAdd(5, 0) 77 | check(5, 10) 78 | checkAdd(3, 0) 79 | check(3, 5, 10) 80 | checkAdd(4, 1) 81 | check(3, 4, 10, 5) 82 | checkPop(3, true) 83 | 84 | checkPop(4, true) 85 | checkPop(5, true) 86 | checkPop(10, true) 87 | checkPop(0, false) 88 | check() 89 | 90 | q.Set([]int{1, 2, 3, 4, 5}) 91 | check(1, 2, 3, 4, 5) 92 | checkPop(1, true) 93 | 94 | q.Set([]int{1, 2, 3, 4, 5, 6}) 95 | checkRemove(0, 1, true) 96 | checkRemove(2, 3, true) 97 | checkRemove(5, 0, false) 98 | 99 | q.Clear() 100 | check() 101 | 102 | q.Set([]int{15, 3, 9, 4, 8, 2, 11, 20, 11, 17, 1}) 103 | check(1, 3, 2, 4, 8, 9, 11, 20, 11, 17, 15) // constructed by hand 104 | if got := extract(q); !sort.IntsAreSorted(got) { 105 | t.Errorf("Queue contents are out of order: %v", got) 106 | } 107 | } 108 | 109 | func TestOrder(t *testing.T) { 110 | const inputSize = 5000 111 | const inputRange = 100000 112 | 113 | makeInput := func() []int { 114 | input := make([]int, inputSize) 115 | for i := range input { 116 | input[i] = rand.IntN(inputRange) - (inputRange / 2) 117 | } 118 | return input 119 | } 120 | 121 | t.Run("Ascending", func(t *testing.T) { 122 | q := heapq.New(intCompare) 123 | q.Set(makeInput()) 124 | if got := extract(q); !sort.IntsAreSorted(got) { 125 | t.Errorf("Queue contents are out of order: %v", got) 126 | } 127 | }) 128 | 129 | t.Run("Descending", func(t *testing.T) { 130 | q := heapq.New(revIntCompare) 131 | q.Set(makeInput()) 132 | got := extract(q) 133 | if !sort.IsSorted(sort.Reverse(sort.IntSlice(got))) { 134 | t.Errorf("Queue contents are out of order: %v", got) 135 | } 136 | }) 137 | 138 | t.Run("Reorder", func(t *testing.T) { 139 | q := heapq.New(intCompare) 140 | q.Set([]int{17, 3, 11, 2, 7, 5, 13}) 141 | if got, want := q.Front(), 2; got != want { 142 | t.Errorf("Front: got %v, want %v", got, want) 143 | } 144 | 145 | q.Reorder(revIntCompare) 146 | if got, want := q.Front(), 17; got != want { 147 | t.Errorf("Front: got %v, want %v", got, want) 148 | } 149 | 150 | got := extract(q) 151 | if !sort.IsSorted(sort.Reverse(sort.IntSlice(got))) { 152 | t.Errorf("Results are out of order: %v", got) 153 | } 154 | }) 155 | } 156 | 157 | func TestNewWithData(t *testing.T) { 158 | const bufSize = 100 // N.B. must be even, so we can fill halves 159 | 160 | // Preallocate a buffer and populate part of it with some data. 161 | buf := make([]int, 0, bufSize) 162 | 163 | var want []int 164 | for range bufSize / 2 { 165 | z := rand.IntN(500) - 250 166 | buf = append(buf, z) 167 | want = append(want, z) // keep track of what we added. 168 | } 169 | 170 | // Give buf over to the queue, then add more stuff so we can check that the 171 | // queue took over the array correctly. 172 | q := heapq.NewWithData(intCompare, buf) 173 | 174 | // Add some more stuff via the queue. 175 | for range bufSize / 2 { 176 | z := rand.IntN(500) - 250 177 | q.Add(z) 178 | want = append(want, z) 179 | } 180 | 181 | // Check that the queue used the same array. You are specifically NOT 182 | // supposed to do this, messing with the array outside the queue, but here 183 | // we need to check that the queue did the right thing. 184 | got := buf[:len(want)] 185 | sort.Ints(got) 186 | sort.Ints(want) 187 | 188 | if diff := gocmp.Diff(want, got); diff != "" { 189 | t.Errorf("Queue contents (+want, -got):\n%s", diff) 190 | } 191 | } 192 | 193 | func TestSort(t *testing.T) { 194 | longIn := make([]int, 50) 195 | for i := range longIn { 196 | longIn[i] = rand.IntN(1000) - 250 197 | } 198 | longOut := make([]int, len(longIn)) 199 | copy(longOut, longIn) 200 | sort.Ints(longOut) 201 | 202 | lt := func(a, b int) int { return a - b } 203 | gt := func(a, b int) int { return b - a } 204 | _, _ = lt, gt 205 | tests := []struct { 206 | name string 207 | cmp func(a, b int) int 208 | input, want []int 209 | }{ 210 | {"Nil", intCompare, nil, nil}, 211 | {"Empty", intCompare, []int{}, nil}, 212 | {"Single-LT", intCompare, []int{11}, []int{11}}, 213 | {"Single-GT", revIntCompare, []int{11}, []int{11}}, 214 | {"Ascend", intCompare, []int{9, 1, 4, 11}, []int{1, 4, 9, 11}}, 215 | {"Descend", revIntCompare, []int{9, 1, 4, 11}, []int{11, 9, 4, 1}}, 216 | {"Long", intCompare, longIn, longOut}, 217 | } 218 | for _, tc := range tests { 219 | t.Run(tc.name, func(t *testing.T) { 220 | in := append([]int(nil), tc.input...) 221 | heapq.Sort(tc.cmp, in) 222 | if diff := gocmp.Diff(tc.want, in); diff != "" { 223 | t.Errorf("Sort (-want, +got):\n%s", diff) 224 | } 225 | }) 226 | } 227 | } 228 | 229 | func TestUpdate(t *testing.T) { 230 | m := make(map[string]int) // tracks the offsets of strings in the queue 231 | up := func(s string, p int) { m[s] = p } // update the offsets map 232 | q := heapq.New(cmp.Compare[string]).Update(up) 233 | 234 | // Verify that all the elements know their current offset correctly. 235 | check := func() { 236 | for i := 0; i < q.Len(); i++ { 237 | s, _ := q.Peek(i) 238 | if m[s] != i { 239 | t.Errorf("At pos %d: %s is at %d instead", i, s, m[s]) 240 | } 241 | } 242 | } 243 | 244 | check() // empty 245 | 246 | // Check that Set assigns positions to the elements added. 247 | q.Set([]string{"m", "z", "t", "a", "k", "b"}) 248 | check() 249 | 250 | // Check that Add updates positions correctly. 251 | q.Add("c") 252 | check() 253 | 254 | // Check that we can add an element and remove it by its assigned position. 255 | q.Add("j") 256 | check() 257 | 258 | oldp := m["j"] 259 | t.Logf("Added j at pos=%d", oldp) 260 | q.Remove(oldp) 261 | check() 262 | 263 | // After removal, the element retains its last position. 264 | if m["j"] != oldp { 265 | t.Errorf("After Remove j: p=%d, want %d", m["j"], oldp) 266 | } 267 | 268 | var got []string 269 | for !q.IsEmpty() { 270 | s, _ := q.Pop() 271 | got = append(got, s) 272 | if m[s] != 0 { 273 | t.Errorf("Pop: got %q at p=%d, want p=0", s, m[s]) 274 | } 275 | 276 | } 277 | if diff := gocmp.Diff(got, []string{"a", "b", "c", "k", "m", "t", "z"}); diff != "" { 278 | t.Errorf("Values (-got, +want):\n%s", diff) 279 | } 280 | } 281 | 282 | func extract[T any](q *heapq.Queue[T]) []T { 283 | all := make([]T, 0, q.Len()) 284 | for !q.IsEmpty() { 285 | all = append(all, q.Front()) 286 | q.Pop() 287 | } 288 | return all 289 | } 290 | -------------------------------------------------------------------------------- /internal/mdtest/mdtest.go: -------------------------------------------------------------------------------- 1 | // Package mdtest includes some internal utilities for testing. 2 | package mdtest 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/google/go-cmp/cmp" 8 | "github.com/google/go-cmp/cmp/cmpopts" 9 | ) 10 | 11 | // Shared is the shared interface implemented by various types in this module. 12 | // It is defined here for use in interface satisfaction checks in tests. 13 | type Shared[T any] interface { 14 | Clear() 15 | Peek(int) (T, bool) 16 | Each(func(T) bool) 17 | IsEmpty() bool 18 | Len() int 19 | } 20 | 21 | // Eacher is the subset of Shared provided by iterable elements. 22 | type Eacher[T any] interface { 23 | Each(func(T) bool) 24 | Len() int 25 | } 26 | 27 | // CheckContents verifies that s contains the specified elements in order, or 28 | // reports an error to t. 29 | func CheckContents[T any](t *testing.T, s Eacher[T], want []T) { 30 | t.Helper() 31 | var got []T 32 | for v := range s.Each { 33 | got = append(got, v) 34 | } 35 | if diff := cmp.Diff(want, got, cmpopts.EquateEmpty()); diff != "" { 36 | t.Errorf("Wrong contents (-got, +want):\n%s", diff) 37 | } 38 | if n := s.Len(); n != len(got) || n != len(want) { 39 | t.Errorf("Wrong length: got %d, want %d == %d", n, len(got), len(want)) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /mapset/example_test.go: -------------------------------------------------------------------------------- 1 | package mapset_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/creachadair/mds/mapset" 8 | ) 9 | 10 | func Example() { 11 | s := mapset.New(strings.Fields("a man a plan")...) 12 | 13 | // Add individual elements. 14 | s.Add("panama", "canal") 15 | 16 | // Add the contents of another set. 17 | t := mapset.New("plan", "for", "the", "future") 18 | s.AddAll(t) 19 | 20 | // Remove items and convert to a slice. 21 | elts := s.Remove("a", "an", "the", "for").Slice() 22 | 23 | // Clone and make other changes. 24 | u := s.Clone().Remove("future", "plans") 25 | 26 | // Do some basic comparisons. 27 | fmt.Println("t intersects u:", t.Intersects(u)) 28 | fmt.Println("t equals u:", t.Equals(u)) 29 | fmt.Println() 30 | 31 | // The slice is unordered, so impose some discipline. 32 | fmt.Println(strings.Join(elts, "\n")) 33 | // Unordered output: 34 | // t intersects u: true 35 | // t equals u: false 36 | // 37 | // canal 38 | // future 39 | // man 40 | // panama 41 | // plan 42 | } 43 | 44 | func ExampleKeys() { 45 | s := mapset.Keys(map[string]int{ 46 | "apple": 1, 47 | "pear": 2, 48 | "plum": 3, 49 | "cherry": 4, 50 | }) 51 | 52 | fmt.Println(strings.Join(s.Slice(), "\n")) 53 | // Unordered output: 54 | // apple 55 | // cherry 56 | // pear 57 | // plum 58 | } 59 | 60 | func ExampleValues() { 61 | s := mapset.Values(map[string]int{ 62 | "apple": 5, 63 | "pear": 4, 64 | "plum": 4, 65 | "cherry": 6, 66 | }) 67 | 68 | for _, v := range s.Slice() { 69 | fmt.Println(v) 70 | } 71 | // Unordered output: 72 | // 4 73 | // 5 74 | // 6 75 | } 76 | -------------------------------------------------------------------------------- /mapset/mapset.go: -------------------------------------------------------------------------------- 1 | // Package mapset implements a basic set type using a built-in map. 2 | // 3 | // The Set type is a thin wrapper on a built-in Go map, so a Set is not safe 4 | // for concurrent use without external synchronization. 5 | package mapset 6 | 7 | import ( 8 | "iter" 9 | "maps" 10 | ) 11 | 12 | // A Set represents a set of distinct values. It is implemented via the 13 | // built-in map type, and the underlying map can also be used directly to add 14 | // and remove items and to iterate the contents. 15 | type Set[T comparable] map[T]struct{} 16 | 17 | // New constructs a set of the specified items. The result is never nil, even 18 | // if no items are provided. 19 | func New[T comparable](items ...T) Set[T] { 20 | m := make(Set[T], len(items)) 21 | return m.add(items) 22 | } 23 | 24 | // NewSize constructs a new empty set preallocated to have space for n items. 25 | func NewSize[T comparable](n int) Set[T] { return make(Set[T], n) } 26 | 27 | // IsEmpty reports whether s is empty. 28 | func (s Set[T]) IsEmpty() bool { return len(s) == 0 } 29 | 30 | // Len reports the number of elements in s. 31 | func (s Set[T]) Len() int { return len(s) } 32 | 33 | // Clear removes all elements from s and returns s. 34 | func (s Set[T]) Clear() Set[T] { clear(s); return s } 35 | 36 | // Clone returns a new set with the same contents as s. 37 | // The value returned is never nil. 38 | func (s Set[T]) Clone() Set[T] { 39 | if s == nil { 40 | return make(Set[T]) 41 | } 42 | // N.B. maps.Clone uses a runtime API internally so it should generally 43 | // always be more efficient than an explicit copy. 44 | return maps.Clone(s) 45 | } 46 | 47 | // Has reports whether t is present in the set. 48 | func (s Set[T]) Has(t T) bool { _, ok := s[t]; return ok } 49 | 50 | // Add adds the specified items to the set and returns s. 51 | func (s *Set[T]) Add(items ...T) Set[T] { 52 | if *s == nil { 53 | *s = make(Set[T], len(items)) 54 | } 55 | return (*s).add(items) 56 | } 57 | 58 | func (s Set[T]) add(items []T) Set[T] { 59 | for _, item := range items { 60 | s[item] = struct{}{} 61 | } 62 | return s 63 | } 64 | 65 | // AddAll adds all the elements of set t to s and returns s. 66 | func (s *Set[T]) AddAll(t Set[T]) Set[T] { 67 | if *s == nil { 68 | *s = t.Clone() 69 | return *s 70 | } 71 | for item := range t { 72 | (*s)[item] = struct{}{} 73 | } 74 | return *s 75 | } 76 | 77 | // Remove removes the specified items from the set and returns s. 78 | func (s Set[T]) Remove(items ...T) Set[T] { 79 | for _, item := range items { 80 | if len(s) == 0 { 81 | break 82 | } 83 | delete(s, item) 84 | } 85 | return s 86 | } 87 | 88 | // RemoveAll removes all the elements of set t from s and returns s. 89 | func (s Set[T]) RemoveAll(t Set[T]) Set[T] { 90 | for item := range t { 91 | if len(s) == 0 { 92 | break 93 | } 94 | delete(s, item) 95 | } 96 | return s 97 | } 98 | 99 | // Pop removes and returns an arbitrary element of s, if s is non-empty. 100 | // If s is empty, it returns a zero value. 101 | func (s Set[T]) Pop() T { 102 | for item := range s { 103 | delete(s, item) 104 | return item 105 | } 106 | var zero T 107 | return zero 108 | } 109 | 110 | // Intersects reports whether s and t share any elements in common. 111 | func (s Set[T]) Intersects(t Set[T]) bool { 112 | lo, hi := s, t 113 | if len(s) > len(t) { 114 | lo, hi = hi, lo 115 | } 116 | for item := range lo { 117 | if hi.Has(item) { 118 | return true 119 | } 120 | } 121 | return false 122 | } 123 | 124 | // HasAll reports whether s contains all the elements of ts. 125 | // It is semantically equivalent to ts.IsSubset(s), but does not construct an 126 | // intermediate set. It returns true if len(ts) == 0. 127 | func (s Set[T]) HasAll(ts ...T) bool { 128 | if len(s) == 0 { 129 | return len(ts) == 0 130 | } 131 | for _, t := range ts { 132 | if !s.Has(t) { 133 | return false 134 | } 135 | } 136 | return true 137 | } 138 | 139 | // HasAny reports whether s contains any element of ts. 140 | // It is semantically equivalent to ts.Intersects(s), but does not construct an 141 | // intermediate set. It returns false if len(ts) == 0. 142 | func (s Set[T]) HasAny(ts ...T) bool { 143 | if len(s) == 0 { 144 | return false 145 | } 146 | for _, t := range ts { 147 | if s.Has(t) { 148 | return true 149 | } 150 | } 151 | return false 152 | } 153 | 154 | // IsSubset reports whether s is a subset of t. 155 | func (s Set[T]) IsSubset(t Set[T]) bool { 156 | if len(s) == 0 { 157 | return true 158 | } else if len(s) > len(t) { 159 | return false 160 | } 161 | for item := range s { 162 | if !t.Has(item) { 163 | return false 164 | } 165 | } 166 | return true 167 | } 168 | 169 | // Equals reports whether s and t contain exactly the same elements. 170 | func (s Set[T]) Equals(t Set[T]) bool { 171 | if len(s) != len(t) { 172 | return false 173 | } 174 | for item := range s { 175 | if !t.Has(item) { 176 | return false 177 | } 178 | } 179 | return true 180 | } 181 | 182 | // Append appends the elements of s to the specified slice in arbitrary order, 183 | // and returns the resulting slice. If cap(vs) ≥ len(s) this will not allocate. 184 | func (s Set[T]) Append(vs []T) []T { 185 | if len(s) == 0 { 186 | return vs 187 | } 188 | for item := range s { 189 | vs = append(vs, item) 190 | } 191 | return vs 192 | } 193 | 194 | // Slice returns a slice of the contents of s in arbitrary order. 195 | // It is a shorthand for Append. 196 | func (s Set[T]) Slice() []T { 197 | if len(s) == 0 { 198 | return nil 199 | } 200 | return s.Append(make([]T, 0, len(s))) 201 | } 202 | 203 | // Intersect constructs a new set containing the intersection of the specified 204 | // sets. The result is never nil, even if the given sets are empty. 205 | func Intersect[T comparable](ss ...Set[T]) Set[T] { 206 | if len(ss) == 0 { 207 | return make(Set[T]) 208 | } 209 | min := ss[0] 210 | for _, s := range ss[1:] { 211 | if len(s) < len(min) { 212 | min = s 213 | } 214 | } 215 | 216 | out := make(Set[T], len(min)) 217 | nextElt: 218 | for v := range min { 219 | for _, s := range ss { 220 | if !s.Has(v) { 221 | continue nextElt 222 | } 223 | } 224 | out.Add(v) 225 | } 226 | 227 | return out 228 | } 229 | 230 | // Range constructs a new Set containing the values of it. 231 | func Range[T comparable](it iter.Seq[T]) Set[T] { 232 | out := make(Set[T]) 233 | for v := range it { 234 | out.Add(v) 235 | } 236 | return out 237 | } 238 | 239 | // Keys constructs a new Set containing the keys of m. The result is never 240 | // nil, even if m is empty. 241 | func Keys[T comparable, U any](m map[T]U) Set[T] { 242 | out := make(Set[T], len(m)) 243 | for key := range m { 244 | out.Add(key) 245 | } 246 | return out 247 | } 248 | 249 | // Values constructs a new Set containing the values of m. The result is never 250 | // nil, even if m is empty. 251 | func Values[T, U comparable](m map[T]U) Set[U] { 252 | out := make(Set[U]) 253 | for _, val := range m { 254 | out.Add(val) 255 | } 256 | return out 257 | } 258 | -------------------------------------------------------------------------------- /mbits/mbits.go: -------------------------------------------------------------------------------- 1 | // Package mbits provides functions for manipulating bits and bytes. 2 | package mbits 3 | 4 | import ( 5 | "unsafe" 6 | ) 7 | 8 | // Zero sets the contents of data to zero and returns len(data). 9 | func Zero(data []byte) int { clear(data); return len(data) } 10 | 11 | // LeadingZeroes reports the number of leading zero bytes at the beginning of data. 12 | func LeadingZeroes(data []byte) int { 13 | n := len(data) 14 | m := n &^ 7 // end of full 64-bit chunks spanned by data 15 | 16 | var i int 17 | for ; i < m; i += 8 { 18 | v := *(*uint64)(unsafe.Pointer(&data[i])) 19 | if v != 0 { 20 | // Count zeroes at the front of v. 21 | for data[i] == 0 { 22 | i++ 23 | } 24 | return i 25 | } 26 | } 27 | 28 | // Count however many zeroes are left. 29 | for i < n && data[i] == 0 { 30 | i++ 31 | } 32 | return i 33 | } 34 | 35 | // TrailingZeroes reports the number of trailing zero bytes at the end of data. 36 | func TrailingZeroes(data []byte) int { 37 | n := len(data) 38 | 39 | // Find the start of the tail of data comprising only full-width chunks. 40 | // 41 | // | < 8 | ... 8 ... | ... 8 ... | . . . | ... 8 ... | 42 | // ^m ^n 43 | // 44 | // Walk backward through these looking for a non-zero. 45 | m := n - n&^7 46 | 47 | i, nz := n-8, 0 48 | for ; i >= m; i -= 8 { 49 | v := *(*uint64)(unsafe.Pointer(&data[i])) 50 | if v != 0 { 51 | // Count zeroes at the end of v. 52 | for data[i+7] == 0 { 53 | i-- 54 | nz++ 55 | } 56 | return nz 57 | } 58 | nz += 8 59 | } 60 | 61 | // Count zeroes left at the tail of the ragged block at the front of data. 62 | for m--; m >= 0 && data[m] == 0; m-- { 63 | nz++ 64 | } 65 | return nz 66 | } 67 | -------------------------------------------------------------------------------- /mbits/mbits_test.go: -------------------------------------------------------------------------------- 1 | package mbits_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/creachadair/mds/mbits" 9 | ) 10 | 11 | func isZero(data []byte) bool { 12 | for _, b := range data { 13 | if b != 0 { 14 | return false 15 | } 16 | } 17 | return true 18 | } 19 | 20 | func TestZero(t *testing.T) { 21 | for _, s := range []string{ 22 | "", 23 | "\x00", 24 | "\x00\x00\x00\x00\x00\x00\x00", 25 | "abcd\x00\x00efghij\x00jklmnopqrstuvwxyz", 26 | "abcdefgh", 27 | "abcdefgh1", 28 | "abcdefgh12", 29 | "abcdefgh123", 30 | "abcdefgh1234", 31 | "abcdefgh12345", 32 | "abcdefgh123456", 33 | "abcdefgh1234567", 34 | "abcdefgh12345678", 35 | "abcdefgh123456789", 36 | strings.Repeat("\x00", 1000), 37 | strings.Repeat("\xff", 1000), 38 | strings.Repeat("\x00\xff\x01", 1003), 39 | } { 40 | in := []byte(s) 41 | mbits.Zero(in) 42 | if !isZero(in) { 43 | t.Errorf("Zero %q did not work", s) 44 | } 45 | } 46 | } 47 | 48 | func TestLeadingZeroes(t *testing.T) { 49 | for _, nb := range []int{5, 16, 43, 100, 128} { 50 | t.Run(fmt.Sprintf("Buf%d", nb), func(t *testing.T) { 51 | buf := make([]byte, nb) 52 | if got := mbits.LeadingZeroes(buf); got != nb { 53 | t.Errorf("Got %d leading zeroes, want %d", got, nb) 54 | } 55 | 56 | // Test every possible offset. 57 | for i := range len(buf) { 58 | buf[i] = 1 59 | if got := mbits.LeadingZeroes(buf); got != i { 60 | t.Errorf("Got %d leading zeroes, want %d", got, i) 61 | } 62 | buf[i] = 0 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func TestTrailingZeroes(t *testing.T) { 69 | for _, nb := range []int{5, 16, 43, 100, 128} { 70 | t.Run(fmt.Sprintf("Buf%d", nb), func(t *testing.T) { 71 | buf := make([]byte, nb) 72 | if got := mbits.TrailingZeroes(buf); got != nb { 73 | t.Errorf("Got %d trailing zeroes, want %d", got, nb) 74 | } 75 | 76 | // Test every possible offset. 77 | for i := range len(buf) { 78 | pos := len(buf) - i - 1 79 | buf[pos] = 1 80 | if got := mbits.TrailingZeroes(buf); got != i { 81 | t.Errorf("Got %d trailing zeroes, want %d", got, i) 82 | } 83 | buf[pos] = 0 84 | } 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /mdiff/example_test.go: -------------------------------------------------------------------------------- 1 | package mdiff_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | "github.com/creachadair/mds/mdiff" 11 | ) 12 | 13 | func ExampleNormal() { 14 | diff := mdiff.New( 15 | []string{"I", "saw", "three", "mice", "running", "away"}, 16 | []string{"three", "blind", "mice", "ran", "home"}, 17 | ) 18 | 19 | diff.Format(os.Stdout, mdiff.Normal, nil) 20 | 21 | // Output: 22 | // 23 | // 1,2d0 24 | // < I 25 | // < saw 26 | // 3a2 27 | // > blind 28 | // 5,6c4,5 29 | // < running 30 | // < away 31 | // --- 32 | // > ran 33 | // > home 34 | } 35 | 36 | func ExampleContext() { 37 | diff := mdiff.New( 38 | []string{"I", "saw", "three", "mice", "running", "away"}, 39 | []string{"three", "blind", "mice", "ran", "home"}, 40 | ).AddContext(3).Unify() 41 | 42 | ts := time.Date(2024, 3, 18, 22, 30, 35, 0, time.UTC) 43 | diff.Format(os.Stdout, mdiff.Context, &mdiff.FileInfo{ 44 | Left: "old", LeftTime: ts, 45 | Right: "new", RightTime: ts.Add(3 * time.Second), 46 | TimeFormat: time.ANSIC, 47 | }) 48 | 49 | // Output: 50 | // 51 | // *** old Mon Mar 18 22:30:35 2024 52 | // --- new Mon Mar 18 22:30:38 2024 53 | // *************** 54 | // *** 1,6 **** 55 | // - I 56 | // - saw 57 | // three 58 | // mice 59 | // ! running 60 | // ! away 61 | // --- 1,5 ---- 62 | // three 63 | // + blind 64 | // mice 65 | // ! ran 66 | // ! home 67 | 68 | } 69 | 70 | func ExampleUnified() { 71 | diff := mdiff.New( 72 | []string{"I", "saw", "three", "mice", "running", "away"}, 73 | []string{"three", "blind", "mice", "ran", "home"}, 74 | ).AddContext(3).Unify() 75 | 76 | diff.Format(os.Stdout, mdiff.Unified, nil) // nil means "no header" 77 | 78 | // Output: 79 | // 80 | // @@ -1,6 +1,5 @@ 81 | // -I 82 | // -saw 83 | // three 84 | // +blind 85 | // mice 86 | // -running 87 | // -away 88 | // +ran 89 | // +home 90 | } 91 | 92 | func ExampleRead() { 93 | const textDiff = `1,2d0 94 | < I 95 | < saw 96 | 3a2 97 | > blind 98 | 5,6c4,5 99 | < running 100 | < away 101 | --- 102 | > ran 103 | > home` 104 | 105 | p, err := mdiff.Read(strings.NewReader(textDiff)) 106 | if err != nil { 107 | log.Fatalf("Read: %v", err) 108 | } 109 | printChunks(p.Chunks) 110 | 111 | // Output: 112 | // 113 | // Chunk 1: left 1:3, right 1:1 114 | // edit 1.1: -[I saw] 115 | // Chunk 2: left 4:4, right 2:3 116 | // edit 2.1: +[blind] 117 | // Chunk 3: left 5:7, right 4:6 118 | // edit 3.1: ![running away:ran home] 119 | } 120 | 121 | func ExampleReadUnified() { 122 | const textDiff = `@@ -1,3 +1 @@ 123 | -I 124 | -saw 125 | three 126 | @@ -3,2 +1,3 @@ 127 | three 128 | +blind 129 | mice 130 | @@ -4,3 +3,3 @@ 131 | mice 132 | -running 133 | -away 134 | +ran 135 | +home` 136 | 137 | p, err := mdiff.ReadUnified(strings.NewReader(textDiff)) 138 | if err != nil { 139 | log.Fatalf("ReadUnified: %v", err) 140 | } 141 | printChunks(p.Chunks) 142 | 143 | // Output: 144 | // 145 | // Chunk 1: left 1:4, right 1:1 146 | // edit 1.1: -[I saw] 147 | // edit 1.2: =[three] 148 | // Chunk 2: left 3:5, right 1:4 149 | // edit 2.1: =[three] 150 | // edit 2.2: +[blind] 151 | // edit 2.3: =[mice] 152 | // Chunk 3: left 4:7, right 3:6 153 | // edit 3.1: =[mice] 154 | // edit 3.2: -[running away] 155 | // edit 3.3: +[ran home] 156 | } 157 | 158 | func printChunks(cs []*mdiff.Chunk) { 159 | for i, c := range cs { 160 | fmt.Printf("Chunk %d: left %d:%d, right %d:%d\n", 161 | i+1, c.LStart, c.LEnd, c.RStart, c.REnd) 162 | for j, e := range c.Edits { 163 | fmt.Printf(" edit %d.%d: %v\n", i+1, j+1, e) 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /mdiff/format.go: -------------------------------------------------------------------------------- 1 | package mdiff 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "time" 9 | 10 | "github.com/creachadair/mds/slice" 11 | ) 12 | 13 | // FormatFunc is a function that renders diff chunks as text to an io.Writer. 14 | // 15 | // A FormatFunc should accept a nil info pointer, and should skip or supply 16 | // default values for missing fields. 17 | type FormatFunc func(w io.Writer, ch []*Chunk, fi *FileInfo) error 18 | 19 | // TimeFormat is the default format string used to render timestamps in context 20 | // and unified diff outputs. It is based on the RFC 2822 time format. 21 | const TimeFormat = "2006-01-02 15:04:05.999999 -0700" 22 | 23 | // FileInfo specifies file metadata to use when formatting a diff. 24 | type FileInfo struct { 25 | // Left is the filename to use for the left-hand input. 26 | Left string 27 | 28 | // Right is the filename to use for the right-hand argument. 29 | Right string 30 | 31 | // LeftTime is the timestamp to use for the left-hand input. 32 | LeftTime time.Time 33 | 34 | // RightTime is the timestamp to use for the right-hand input. 35 | RightTime time.Time 36 | 37 | // TimeFormat specifies the time format to use for timestamps. 38 | // Any format string accepted by time.Format is permitted. 39 | // If omitted, it uses the TimeFormat constant. 40 | TimeFormat string 41 | } 42 | 43 | // Unified is a [FormatFunc] that renders ch in the [unified diff] format 44 | // introduced by GNU diff. If fi == nil, the file header is omitted. 45 | // 46 | // [unified diff]: https://www.gnu.org/software/diffutils/manual/html_node/Unified-Format.html 47 | func Unified(w io.Writer, ch []*Chunk, fi *FileInfo) error { 48 | if len(ch) == 0 { 49 | return nil 50 | } 51 | if fi != nil { 52 | fmtFileHeader(w, "--- ", cmp.Or(fi.Left, "a"), fi.LeftTime, cmp.Or(fi.TimeFormat, TimeFormat)) 53 | fmtFileHeader(w, "+++ ", cmp.Or(fi.Right, "b"), fi.RightTime, cmp.Or(fi.TimeFormat, TimeFormat)) 54 | } 55 | for _, c := range ch { 56 | fmt.Fprintln(w, "@@", uspan("-", c.LStart, c.LEnd), uspan("+", c.RStart, c.REnd), "@@") 57 | for _, e := range c.Edits { 58 | switch e.Op { 59 | case slice.OpDrop: 60 | writeLines(w, "-", e.X) 61 | case slice.OpEmit: 62 | writeLines(w, " ", e.X) 63 | case slice.OpCopy: 64 | writeLines(w, "+", e.Y) 65 | case slice.OpReplace: 66 | writeLines(w, "-", e.X) 67 | writeLines(w, "+", e.Y) 68 | } 69 | } 70 | } 71 | return nil 72 | } 73 | 74 | func fmtFileHeader(w io.Writer, prefix, name string, ts time.Time, tfmt string) { 75 | fmt.Fprint(w, prefix, name) 76 | if !ts.IsZero() { 77 | fmt.Fprint(w, "\t", ts.Format(tfmt)) 78 | } 79 | fmt.Fprintln(w) 80 | } 81 | 82 | // Context is a [FormatFunc] that renders ch in the [context diff] format 83 | // introduced by BSD diff. If fi == nil, the file header is omitted. 84 | // 85 | // [context diff]: https://www.gnu.org/software/diffutils/manual/html_node/Context-Format.html 86 | func Context(w io.Writer, ch []*Chunk, fi *FileInfo) error { 87 | if len(ch) == 0 { 88 | return nil 89 | } 90 | if fi != nil { 91 | fmtFileHeader(w, "*** ", cmp.Or(fi.Left, "a"), fi.LeftTime, cmp.Or(fi.TimeFormat, TimeFormat)) 92 | fmtFileHeader(w, "--- ", cmp.Or(fi.Right, "b"), fi.RightTime, cmp.Or(fi.TimeFormat, TimeFormat)) 93 | } 94 | for _, c := range ch { 95 | // Why 15 stars? I can't say. Berkeley just liked it better that way. 96 | fmt.Fprintln(w, "***************") 97 | fmt.Fprintf(w, "*** %s ****\n", dspan(c.LStart, c.LEnd)) 98 | if hasRelevantEdits(c.Edits, slice.OpDrop) { 99 | for _, e := range c.Edits { 100 | switch e.Op { 101 | case slice.OpDrop: 102 | writeLines(w, "- ", e.X) 103 | case slice.OpEmit: 104 | writeLines(w, " ", e.X) 105 | case slice.OpReplace: 106 | writeLines(w, "! ", e.X) 107 | } 108 | } 109 | } 110 | fmt.Fprintf(w, "--- %s ----\n", dspan(c.RStart, c.REnd)) 111 | if hasRelevantEdits(c.Edits, slice.OpCopy) { 112 | for _, e := range c.Edits { 113 | switch e.Op { 114 | case slice.OpCopy: 115 | writeLines(w, "+ ", e.Y) 116 | case slice.OpEmit: 117 | writeLines(w, " ", e.X) 118 | case slice.OpReplace: 119 | writeLines(w, "! ", e.Y) 120 | } 121 | } 122 | } 123 | } 124 | return nil 125 | } 126 | 127 | // Normal is a [FormatFunc] that renders ch in the "normal" [Unix diff] format. 128 | // This format does not include a file header, so the FileInfo is ignored. 129 | // 130 | // [Unix diff]: https://www.gnu.org/software/diffutils/manual/html_node/Detailed-Normal.html 131 | func Normal(w io.Writer, ch []*Chunk, _ *FileInfo) error { 132 | for _, c := range ch { 133 | lpos, rpos := c.LStart, c.RStart 134 | for _, e := range c.Edits { 135 | switch e.Op { 136 | case slice.OpDrop: 137 | // Diff considers deletions to happen AFTER the previous line rather 138 | // than on the current one. 139 | fmt.Fprintf(w, "%sd%d\n", dspan(lpos, lpos+len(e.X)), rpos-1) 140 | writeLines(w, "< ", e.X) 141 | lpos += len(e.X) 142 | 143 | case slice.OpEmit: 144 | lpos += len(e.X) 145 | rpos += len(e.X) 146 | 147 | case slice.OpCopy: 148 | // Diff considers insertions to happen AFTER the previons line rather 149 | // than on the current one. 150 | fmt.Fprintf(w, "%da%s\n", lpos-1, dspan(rpos, rpos+len(e.Y))) 151 | writeLines(w, "> ", e.Y) 152 | rpos += len(e.Y) 153 | 154 | case slice.OpReplace: 155 | fmt.Fprintf(w, "%sc%s\n", dspan(lpos, lpos+len(e.X)), dspan(rpos, rpos+len(e.Y))) 156 | writeLines(w, "< ", e.X) 157 | fmt.Fprintln(w, "---") 158 | writeLines(w, "> ", e.Y) 159 | lpos += len(e.X) 160 | rpos += len(e.Y) 161 | } 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | // dspan formats the range start, end as a diff span. 168 | func dspan(start, end int) string { 169 | if end-start == 1 { 170 | return strconv.Itoa(start) 171 | } 172 | return fmt.Sprintf("%d,%d", start, end-1) 173 | } 174 | 175 | // uspan formats the range start, end as a unified diff span. 176 | func uspan(side string, start, end int) string { 177 | if end-start == 1 { 178 | return side + strconv.Itoa(start) 179 | } 180 | return fmt.Sprintf("%s%d,%d", side, start, end-start) 181 | } 182 | 183 | func writeLines(w io.Writer, pfx string, lines []string) { 184 | for _, line := range lines { 185 | fmt.Fprint(w, pfx, line, "\n") 186 | } 187 | } 188 | 189 | // hasRelevantEdits reports whether es contains at least one edit with either 190 | // the specified opcode or slice.OpReplace. 191 | func hasRelevantEdits(es []Edit, op slice.EditOp) bool { 192 | for _, e := range es { 193 | if e.Op == op || e.Op == slice.OpReplace { 194 | return true 195 | } 196 | } 197 | return false 198 | } 199 | -------------------------------------------------------------------------------- /mdiff/testdata/cdiff.txt: -------------------------------------------------------------------------------- 1 | *** testdata/lhs.txt Sat Mar 16 18:53:15 2024 2 | --- testdata/rhs.txt Sat Mar 16 18:53:15 2024 3 | *************** 4 | *** 1,7 **** 5 | - The Way that can be told of is not the eternal Way; 6 | - The name that can be named is not the eternal name. 7 | The Nameless is the origin of Heaven and Earth; 8 | ! The Named is the mother of all things. 9 | Therefore let there always be non-being, 10 | so we may see their subtlety, 11 | And let there always be being, 12 | --- 1,6 ---- 13 | The Nameless is the origin of Heaven and Earth; 14 | ! The named is the mother of all things. 15 | ! 16 | Therefore let there always be non-being, 17 | so we may see their subtlety, 18 | And let there always be being, 19 | *************** 20 | *** 9,11 **** 21 | --- 8,13 ---- 22 | The two are the same, 23 | But after they are produced, 24 | they have different names. 25 | + They both may be called deep and profound. 26 | + Deeper and more profound, 27 | + The door of all subtleties! 28 | -------------------------------------------------------------------------------- /mdiff/testdata/gdiff.txt: -------------------------------------------------------------------------------- 1 | diff --git a/mdiff/testdata/lhs.txt b/mdiff/testdata/lhs.txt 2 | index 635ef2c..5af88a8 100644 3 | --- a/mdiff/testdata/lhs.txt 4 | +++ b/mdiff/testdata/lhs.txt 5 | @@ -1,7 +1,6 @@ 6 | -The Way that can be told of is not the eternal Way; 7 | -The name that can be named is not the eternal name. 8 | The Nameless is the origin of Heaven and Earth; 9 | -The Named is the mother of all things. 10 | +The named is the mother of all things. 11 | + 12 | Therefore let there always be non-being, 13 | so we may see their subtlety, 14 | And let there always be being, 15 | @@ -9,3 +8,6 @@ And let there always be being, 16 | The two are the same, 17 | But after they are produced, 18 | they have different names. 19 | +They both may be called deep and profound. 20 | +Deeper and more profound, 21 | +The door of all subtleties! 22 | diff --git a/mdiff/testdata/rhs.txt b/mdiff/testdata/rhs.txt 23 | index 5af88a8..635ef2c 100644 24 | --- a/mdiff/testdata/rhs.txt 25 | +++ b/mdiff/testdata/rhs.txt 26 | @@ -1,6 +1,7 @@ 27 | +The Way that can be told of is not the eternal Way; 28 | +The name that can be named is not the eternal name. 29 | The Nameless is the origin of Heaven and Earth; 30 | -The named is the mother of all things. 31 | - 32 | +The Named is the mother of all things. 33 | Therefore let there always be non-being, 34 | so we may see their subtlety, 35 | And let there always be being, 36 | @@ -8,6 +9,3 @@ And let there always be being, 37 | The two are the same, 38 | But after they are produced, 39 | they have different names. 40 | -They both may be called deep and profound. 41 | -Deeper and more profound, 42 | -The door of all subtleties! 43 | -------------------------------------------------------------------------------- /mdiff/testdata/lhs.txt: -------------------------------------------------------------------------------- 1 | The Way that can be told of is not the eternal Way; 2 | The name that can be named is not the eternal name. 3 | The Nameless is the origin of Heaven and Earth; 4 | The Named is the mother of all things. 5 | Therefore let there always be non-being, 6 | so we may see their subtlety, 7 | And let there always be being, 8 | so we may see their outcome. 9 | The two are the same, 10 | But after they are produced, 11 | they have different names. 12 | -------------------------------------------------------------------------------- /mdiff/testdata/odiff.txt: -------------------------------------------------------------------------------- 1 | 1,2d0 2 | < The Way that can be told of is not the eternal Way; 3 | < The name that can be named is not the eternal name. 4 | 4c2,3 5 | < The Named is the mother of all things. 6 | --- 7 | > The named is the mother of all things. 8 | > 9 | 11a11,13 10 | > They both may be called deep and profound. 11 | > Deeper and more profound, 12 | > The door of all subtleties! 13 | -------------------------------------------------------------------------------- /mdiff/testdata/rhs.txt: -------------------------------------------------------------------------------- 1 | The Nameless is the origin of Heaven and Earth; 2 | The named is the mother of all things. 3 | 4 | Therefore let there always be non-being, 5 | so we may see their subtlety, 6 | And let there always be being, 7 | so we may see their outcome. 8 | The two are the same, 9 | But after they are produced, 10 | they have different names. 11 | They both may be called deep and profound. 12 | Deeper and more profound, 13 | The door of all subtleties! 14 | -------------------------------------------------------------------------------- /mdiff/testdata/udiff.txt: -------------------------------------------------------------------------------- 1 | --- testdata/lhs.txt 2024-03-16 17:47:40.12345 +0000 2 | +++ testdata/rhs.txt 2024-03-16 17:47:40.12345 +0000 3 | @@ -1,7 +1,6 @@ 4 | -The Way that can be told of is not the eternal Way; 5 | -The name that can be named is not the eternal name. 6 | The Nameless is the origin of Heaven and Earth; 7 | -The Named is the mother of all things. 8 | +The named is the mother of all things. 9 | + 10 | Therefore let there always be non-being, 11 | so we may see their subtlety, 12 | And let there always be being, 13 | @@ -9,3 +8,6 @@ 14 | The two are the same, 15 | But after they are produced, 16 | they have different names. 17 | +They both may be called deep and profound. 18 | +Deeper and more profound, 19 | +The door of all subtleties! 20 | -------------------------------------------------------------------------------- /mlink/example_test.go: -------------------------------------------------------------------------------- 1 | package mlink_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/creachadair/mds/mlink" 8 | ) 9 | 10 | func ExampleList() { 11 | var lst mlink.List[string] 12 | 13 | lst.At(0).Add(strings.Fields("A is for Amy who fell down the stairs")...) 14 | 15 | // Find the first element of the list matching a predicate. 16 | name := lst.Find(func(s string) bool { 17 | return s == "Amy" 18 | }) 19 | 20 | name.Set("Amélie") // change the value 21 | name.Next() // move to the next element 22 | name.Next() // ...and again 23 | name.Truncate() // discard the rest of the list 24 | name.Set("ran") // add a new last element 25 | name.Push("far") // push a new element 26 | 27 | // Add a new element at the end. 28 | lst.End().Add("away") 29 | 30 | // Remove the element we previously pushed. 31 | name.Remove() 32 | 33 | // Print out everything in the list. 34 | for s := range lst.Each { 35 | fmt.Print(" ", s) 36 | } 37 | fmt.Println() 38 | 39 | // Calculate the length of the list. 40 | fmt.Println(lst.Len(), "items") 41 | 42 | // Output: 43 | // A is for Amélie who ran away 44 | // 7 items 45 | } 46 | 47 | func ExampleList_Find() { 48 | lst := mlink.NewList[int]() 49 | lst.At(0).Add(2, 4, 6, 7, 8, 10) 50 | 51 | if odd := lst.Find(isOdd); !odd.AtEnd() { 52 | fmt.Printf("Found %d\n", odd.Get()) 53 | odd.Remove() 54 | } 55 | if odd := lst.Find(isOdd); odd.AtEnd() { 56 | fmt.Println("no more odds") 57 | } 58 | // Output: 59 | // Found 7 60 | // no more odds 61 | } 62 | 63 | func isOdd(z int) bool { return z%2 == 1 } 64 | 65 | func ExampleCursor_Next() { 66 | var lst mlink.List[string] 67 | lst.At(0).Add("apples", "pears", "plums", "cherries") 68 | 69 | cur := lst.At(0) 70 | for !cur.AtEnd() { 71 | fmt.Print(cur.Get()) 72 | if cur.Next() { 73 | fmt.Print(", ") 74 | } 75 | } 76 | fmt.Println() 77 | // Output: 78 | // apples, pears, plums, cherries 79 | } 80 | -------------------------------------------------------------------------------- /mlink/list.go: -------------------------------------------------------------------------------- 1 | package mlink 2 | 3 | // A List is a singly-linked ordered list. A zero value is ready for use. 4 | // 5 | // The methods of a List value do not allow direct modification of the list. 6 | // To insert and update entries in the list, use the At, Find, Last, or End 7 | // methods to obtain a Cursor to a location in the list. A cursor can be used 8 | // to insert, update, and delete elements of the list. 9 | type List[T any] struct { 10 | first entry[T] // sentinel; first.link points to the real first element 11 | } 12 | 13 | // NewList returns a new empty list. 14 | func NewList[T any]() *List[T] { return new(List[T]) } 15 | 16 | // IsEmpty reports whether lst is empty. 17 | func (lst *List[T]) IsEmpty() bool { return lst.first.link == nil } 18 | 19 | // Clear discards all the values in lst, leaving it empty. Calling Clear 20 | // invalidates all cursors to the list. 21 | func (lst *List[T]) Clear() { lst.first.link.invalidate(); lst.first.link = nil } 22 | 23 | // Peek reports whether lst has a value at offset n from the front of the list, 24 | // and if so returns its value. 25 | // 26 | // This method takes time proportional to n. Peek will panic if n < 0. 27 | func (lst *List[T]) Peek(n int) (T, bool) { 28 | cur := lst.At(n) 29 | return cur.Get(), !cur.AtEnd() 30 | } 31 | 32 | // Each is a range function that calls f with each value in lst in order from 33 | // first to last. If f returns false, Each returns immediately. 34 | func (lst *List[T]) Each(f func(T) bool) { 35 | for cur := lst.cfirst(); !cur.AtEnd(); cur.Next() { 36 | if !f(cur.Get()) { 37 | return 38 | } 39 | } 40 | } 41 | 42 | // Len reports the number of elements in lst. This method takes time proportional 43 | // to the length of the list. 44 | func (lst *List[T]) Len() (n int) { 45 | for range lst.Each { 46 | n++ 47 | } 48 | return 49 | } 50 | 51 | // At returns a cursor to the element at index n ≥ 0 in the list. If n is 52 | // greater than or equal to n.Len(), At returns a cursor to the end of the list 53 | // (equivalent to End). 54 | // 55 | // At will panic if n < 0. 56 | func (lst *List[T]) At(n int) *Cursor[T] { 57 | if n < 0 { 58 | panic("index out of range") 59 | } 60 | 61 | cur := lst.cfirst() 62 | for ; !cur.AtEnd(); cur.Next() { 63 | if n == 0 { 64 | break 65 | } 66 | n-- 67 | } 68 | return &cur 69 | } 70 | 71 | // Last returns a cursor to the last element of the list. If lst is empty, it 72 | // returns a cursor to the end of the list (equivalent to End). 73 | // This method takes time proportional to the length of the list. 74 | func (lst *List[T]) Last() *Cursor[T] { 75 | cur := lst.cfirst() 76 | if !cur.AtEnd() { 77 | for cur.pred.link.link != nil { 78 | cur.Next() 79 | } 80 | } 81 | return &cur 82 | } 83 | 84 | // End returns a cursor to the position just past the end of the list. 85 | // This method takes time proportional to the length of the list. 86 | func (lst *List[T]) End() *Cursor[T] { c := lst.Last(); c.Next(); return c } 87 | 88 | // Find returns a cursor to the first element of the list for which f returns 89 | // true. If no such element is found, the resulting cursor is at the end of the 90 | // list. 91 | func (lst *List[T]) Find(f func(T) bool) *Cursor[T] { 92 | cur := lst.cfirst() 93 | for !cur.AtEnd() { 94 | if f(cur.Get()) { 95 | break 96 | } 97 | cur.Next() 98 | } 99 | return &cur 100 | } 101 | 102 | func (lst *List[T]) cfirst() Cursor[T] { return Cursor[T]{pred: &lst.first} } 103 | 104 | // A Cursor represents a location in a list. A nil *Cursor is not valid, and 105 | // operations on it will panic. Through a valid cursor, the caller can add, 106 | // modify, or remove elements, and navigate forward through the list. 107 | // 108 | // Multiple cursors into the same list are fine, but note that modifying the 109 | // list through one cursor may invalidate others. 110 | type Cursor[T any] struct { 111 | // pred is the entry in its list whose link points to the target. This 112 | // permits a cursor to delete the element it points to from the list. 113 | // Invariant: pred != nil 114 | pred *entry[T] 115 | } 116 | 117 | // Get returns the value at c's location. If c is at the end of the list, Get 118 | // returns a zero value. 119 | func (c *Cursor[T]) Get() T { 120 | if c.AtEnd() { 121 | var zero T 122 | return zero 123 | } 124 | return c.pred.checkValid().link.X 125 | } 126 | 127 | // Set replaces the value at c's location. If c is at the end of the list, 128 | // calling Set is equivalent to calling Push. 129 | // 130 | // Before: 131 | // 132 | // [1, 2, 3] 133 | // ^--- c 134 | // 135 | // After c.Set(9) 136 | // 137 | // [1, 9, 3] 138 | // ^--- c 139 | func (c *Cursor[T]) Set(v T) { 140 | if c.AtEnd() { 141 | c.pred.link = &entry[T]{X: v} 142 | // N.B.: c is now no longer AtEnd 143 | } else { 144 | c.pred.checkValid().link.X = v 145 | } 146 | } 147 | 148 | // AtEnd reports whether c is at the end of its list. 149 | func (c *Cursor[T]) AtEnd() bool { return c.pred.checkValid().link == nil } 150 | 151 | // Next advances c to the next position in the list (if possible) and reports 152 | // whether the resulting position is at the end of the list. If c was already 153 | // at the end its position is unchanged. 154 | func (c *Cursor[T]) Next() bool { 155 | if c.AtEnd() { 156 | return false 157 | } 158 | c.pred = c.pred.link 159 | return !c.AtEnd() 160 | } 161 | 162 | // Push inserts a new value into the list at c's location. After insertion, c 163 | // points to the newly-added item and the previous value is now at c.Next(). 164 | // 165 | // Before: 166 | // 167 | // [1, 2, 3] 168 | // ^--- c 169 | // 170 | // After c.Push(4): 171 | // 172 | // [4, 1, 2, 3] 173 | // ^--- c 174 | func (c *Cursor[T]) Push(v T) { 175 | added := &entry[T]{X: v, link: c.pred.checkValid().link} 176 | c.pred.link = added 177 | } 178 | 179 | // Add inserts one or more new values into the list at c's location. After 180 | // insertion, c points to the original item, now in the location after the 181 | // newly-added values. This is a shorthand for Push followed by Next. 182 | // 183 | // Before: 184 | // 185 | // [1, 2, 3] 186 | // ^--- c 187 | // 188 | // After c.Add(4): 189 | // 190 | // [4, 1, 2, 3] 191 | // ^--- c 192 | func (c *Cursor[T]) Add(vs ...T) { 193 | for _, v := range vs { 194 | c.Push(v) 195 | c.Next() 196 | } 197 | } 198 | 199 | // Remove removes and returns the element at c's location from the list. If c 200 | // is at the end of the list, Remove does nothing and returns a zero value. 201 | // 202 | // After removal, c is still valid and points the element after the one that 203 | // was removed, or the end of the list. 204 | // 205 | // Successfully removing an element invalidates any cursors to the location 206 | // after the element that was removed. 207 | // 208 | // Before: 209 | // 210 | // [1, 2, 3, 4] 211 | // ^--- c 212 | // 213 | // After c.Remove() 214 | // 215 | // [1, 3, 4] 216 | // ^--- c 217 | func (c *Cursor[T]) Remove() T { 218 | if c.AtEnd() { 219 | var zero T 220 | return zero 221 | } 222 | 223 | // Detach the discarded entry from its neighbor so that any cursors pointing 224 | // to that entry will be AtEnd, and changes made through them will not 225 | // affect the remaining list. 226 | val := c.pred.link.X 227 | next := c.pred.link.link 228 | c.pred.link.link = c.pred.link // invalidate the outgoing (but not all) 229 | c.pred.link = next // the successor of the removed element 230 | return val 231 | } 232 | 233 | // Truncate removes all the elements of the list at and after c's location. 234 | // After calling Truncate, c is at the end of the remaining list. If c was 235 | // already at the end of the list, Truncate does nothing. After truncation, c 236 | // remains valid. 237 | // 238 | // Truncate invalidates any cursors to locations after c in the list. 239 | // 240 | // Before: 241 | // 242 | // [1, 2, 3, 4] 243 | // ^--- c 244 | // 245 | // After c.Truncate(): 246 | // 247 | // [1, 2] * 248 | // ^--- c (c.AtEnd() == true) 249 | func (c *Cursor[T]) Truncate() { c.pred.link.invalidate(); c.pred.link = nil } 250 | -------------------------------------------------------------------------------- /mlink/list_test.go: -------------------------------------------------------------------------------- 1 | package mlink_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/internal/mdtest" 7 | "github.com/creachadair/mds/mlink" 8 | "github.com/creachadair/mds/mtest" 9 | ) 10 | 11 | func eq(z int) func(int) bool { 12 | return func(n int) bool { return n == z } 13 | } 14 | 15 | func TestList(t *testing.T) { 16 | lst := mlink.NewList[int]() 17 | checkList := func(want ...int) { t.Helper(); mdtest.CheckContents(t, lst, want) } 18 | advance := func(c *mlink.Cursor[int], wants ...int) { 19 | t.Helper() 20 | for _, want := range wants { 21 | c.Next() 22 | if got := c.Get(); got != want { 23 | t.Errorf("Get: got %v, want %v", got, want) 24 | } 25 | } 26 | } 27 | checkAt := func(c *mlink.Cursor[int], want int) { 28 | t.Helper() 29 | if got := c.Get(); got != want { 30 | t.Errorf("Get: got %v, want %v", got, want) 31 | } 32 | } 33 | 34 | // A new list is initially empty. 35 | checkList() 36 | 37 | if !lst.At(0).AtEnd() { 38 | t.Error("At(0): should be at end for empty list") 39 | } 40 | if !lst.Last().AtEnd() { 41 | t.Error("Last: should be at end for empty list") 42 | } 43 | 44 | // Add advances after insertion. 45 | first := lst.At(0) 46 | first.Add(1, 2) 47 | checkList(1, 2) 48 | 49 | // Push does not advance after insertion. 50 | first.Push(3) 51 | first.Push(4) 52 | checkList(1, 2, 4, 3) 53 | if got, want := first.Remove(), 4; got != want { 54 | t.Errorf("Remove: got %v, want %v", got, want) 55 | } 56 | checkList(1, 2, 3) 57 | 58 | // Check adding at the end. 59 | lst.End().Add(4) 60 | checkList(1, 2, 3, 4) 61 | 62 | // At and Last should work. 63 | checkAt(lst.At(0), 1) 64 | checkAt(lst.At(1), 2) 65 | checkAt(lst.At(2), 3) 66 | checkAt(lst.At(3), 4) 67 | checkAt(lst.Last(), 4) 68 | 69 | // At past the end of the list should pin to the end. 70 | if e := lst.At(1000); !e.AtEnd() { 71 | t.Error("At(big) should go to the end of the list") 72 | } 73 | // Peek past the end should report failure and a zero. 74 | if v, ok := lst.Peek(1000); ok || v != 0 { 75 | t.Errorf("Peek(big): got (%v, %v), want (0, false)", v, ok) 76 | } 77 | 78 | // Exercise navigation with a cursor. 79 | c := lst.At(0) 80 | checkAt(c, 1) 81 | advance(c, 2) 82 | checkAt(c, 2) 83 | 84 | if got, want := c.Remove(), 2; got != want { 85 | t.Errorf("Remove: got %v, want %v", got, want) 86 | } 87 | checkList(1, 3, 4) 88 | checkAt(c, 3) 89 | 90 | // Add at the ends of the list. 91 | lst.End().Add(5) 92 | lst.At(0).Add(6) 93 | checkList(6, 1, 3, 4, 5) 94 | 95 | // The cursor should still be valid, and see the changes. 96 | advance(c, 4) 97 | 98 | // Add in the middle of the list. 99 | c.Push(7) 100 | checkList(6, 1, 3, 7, 4, 5) 101 | 102 | // Exercise moving in a list. 103 | c = lst.At(0) 104 | checkAt(c, 6) 105 | advance(c, 1, 3, 7) 106 | 107 | cn := lst.Find(eq(4)) // grab a cursor after where we are about to truncate 108 | checkAt(cn, 4) 109 | 110 | c.Truncate() 111 | checkList(6, 1, 3) 112 | if !c.AtEnd() { 113 | t.Error("Cursor should be at the end") 114 | } 115 | 116 | // Truncation invalidates a cursor after the cut point. 117 | mtest.MustPanic(t, func() { cn.Get() }) 118 | 119 | // Push at the end does the needful. 120 | lst.End().Push(9) 121 | checkList(6, 1, 3, 9) 122 | checkAt(c, 9) // c sees the new value 123 | 124 | // Setting the last element doesn't add any mass. 125 | lst.Last().Set(10) 126 | checkList(6, 1, 3, 10) 127 | checkAt(c, 10) // c sees the new value 128 | 129 | // Setting elsewhere works too. 130 | lst.At(0).Set(11) 131 | checkList(11, 1, 3, 10) 132 | 133 | // Setting at the end pushes a new item. 134 | tail := lst.End() 135 | tail.Set(12) 136 | checkAt(tail, 12) // tail is no longer at the end 137 | checkList(11, 1, 3, 10, 12) 138 | checkAt(c, 10) // c hasn't moved 139 | 140 | // Finding things. 141 | checkAt(lst.Find(eq(11)), 11) // first 142 | checkAt(lst.Find(eq(3)), 3) // middle 143 | checkAt(lst.Find(eq(12)), 12) // last 144 | if q := lst.Find(eq(-999)); !q.AtEnd() { // missing 145 | t.Errorf("Find: got %v, wanted no result", q.Get()) 146 | } 147 | 148 | // Remove an item, and verify that a cursor to the following item is 149 | // correctly invalidated. 150 | d := lst.Find(eq(12)) 151 | checkAt(d, 12) 152 | 153 | if got, want := c.Remove(), 10; got != want { 154 | t.Errorf("Remove: got %v, want %v", got, want) 155 | } 156 | checkList(11, 1, 3, 12) 157 | 158 | // Removing invalidates a cursor to the next item. 159 | mtest.MustPanic(t, func() { d.Get() }) 160 | 161 | // We can remove the first and last elements. 162 | if got, want := lst.At(0).Remove(), 11; got != want { 163 | t.Errorf("Remove: got %v, want %v", got, want) 164 | } 165 | checkList(1, 3, 12) 166 | if got, want := lst.Last().Remove(), 12; got != want { 167 | t.Errorf("Remove: got %v, want %v", got, want) 168 | } 169 | checkList(1, 3) 170 | 171 | lst.Clear() 172 | checkList() 173 | } 174 | 175 | func mustPanic(f func()) func(*testing.T) { 176 | return func(t *testing.T) { 177 | t.Helper() 178 | mtest.MustPanic(t, f) 179 | } 180 | } 181 | 182 | func TestPanics(t *testing.T) { 183 | var lst mlink.List[bool] 184 | 185 | t.Run("At(-1)", mustPanic(func() { 186 | lst.At(-1) 187 | })) 188 | t.Run("Peek(-1)", mustPanic(func() { 189 | lst.Peek(-1) 190 | })) 191 | t.Run("NilCursor", mustPanic(func() { 192 | var nc *mlink.Cursor[bool] 193 | nc.Get() 194 | })) 195 | } 196 | -------------------------------------------------------------------------------- /mlink/mlink.go: -------------------------------------------------------------------------------- 1 | // Package mlink implements basic linked container data structures. 2 | // 3 | // The types defined here are not safe for concurrent use by multiple 4 | // goroutines without external synchronization. 5 | package mlink 6 | 7 | // An entry is a singly-linked value container. 8 | type entry[T any] struct { 9 | X T 10 | link *entry[T] 11 | } 12 | 13 | // invalidate makes e and all its successor entries point to themselves, as a 14 | // flag that they are detached from their original list and are invalid. 15 | func (e *entry[T]) invalidate() { 16 | for e != nil { 17 | next := e.link 18 | e.link = e 19 | e = next 20 | } 21 | } 22 | 23 | // checkValid panics if e is an invalid entry, otherwise it returns e. 24 | func (e *entry[T]) checkValid() *entry[T] { 25 | if e.link == e { 26 | panic("invalid cursor") 27 | } 28 | return e 29 | } 30 | -------------------------------------------------------------------------------- /mlink/mlink_test.go: -------------------------------------------------------------------------------- 1 | package mlink_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/internal/mdtest" 7 | "github.com/creachadair/mds/mlink" 8 | ) 9 | 10 | var ( 11 | _ mdtest.Shared[any] = (*mlink.Queue[any])(nil) 12 | _ mdtest.Shared[any] = (*mlink.List[any])(nil) 13 | ) 14 | 15 | func TestQueue(t *testing.T) { 16 | var q mlink.Queue[int] 17 | check := func(want ...int) { mdtest.CheckContents(t, &q, want) } 18 | 19 | // Front and Pop of an empty queue report no value. 20 | if v := q.Front(); v != 0 { 21 | t.Errorf("Front: got %v, want 0", v) 22 | } 23 | if v, ok := q.Pop(); ok { 24 | t.Errorf("Pop: got (%v, %v), want (0, false)", v, ok) 25 | } 26 | 27 | check() 28 | if !q.IsEmpty() { 29 | t.Error("IsEmpty is incorrectly false") 30 | } 31 | if n := q.Len(); n != 0 { 32 | t.Errorf("Len: got %d, want 0", n) 33 | } 34 | 35 | q.Add(1) 36 | if q.IsEmpty() { 37 | t.Error("IsEmpty is incorrectly true") 38 | } 39 | check(1) 40 | 41 | q.Add(2) 42 | check(1, 2) 43 | 44 | q.Add(3) 45 | check(1, 2, 3) 46 | if n := q.Len(); n != 3 { 47 | t.Errorf("Len: got %d, want 3", n) 48 | } 49 | 50 | front := q.Front() 51 | if front != 1 { 52 | t.Errorf("Front: got %v, want 1", front) 53 | } 54 | if v, ok := q.Peek(0); !ok || v != front { 55 | t.Errorf("Peek(0): got (%v, %v), want (%v, true)", v, ok, front) 56 | } 57 | if v, ok := q.Peek(1); !ok || v != 2 { 58 | t.Errorf("Peek(1): got (%v, %v), want (2, true)", v, ok) 59 | } 60 | if v, ok := q.Peek(10); ok { 61 | t.Errorf("Peek(10): got (%v, %v), want (0, false)", v, ok) 62 | } 63 | 64 | if v, ok := q.Pop(); !ok || v != front { 65 | t.Errorf("Pop: got (%v, %v), want (%v, true)", v, ok, front) 66 | } 67 | check(2, 3) 68 | 69 | q.Clear() 70 | check() 71 | } 72 | -------------------------------------------------------------------------------- /mlink/queue.go: -------------------------------------------------------------------------------- 1 | package mlink 2 | 3 | // A Queue is a linked first-in, first out sequence of values. A zero value is 4 | // ready for use. 5 | type Queue[T any] struct { 6 | list List[T] 7 | back Cursor[T] 8 | size int 9 | } 10 | 11 | // NewQueue returns a new empty FIFO queue. 12 | func NewQueue[T any]() *Queue[T] { 13 | q := new(Queue[T]) 14 | q.back = q.list.cfirst() 15 | return q 16 | } 17 | 18 | // Add adds v to the end of q. 19 | func (q *Queue[T]) Add(v T) { 20 | if q.back.pred == nil { 21 | q.back = q.list.cfirst() 22 | } 23 | q.back.Add(v) 24 | q.size++ 25 | } 26 | 27 | // IsEmpty reports whether q is empty. 28 | func (q *Queue[T]) IsEmpty() bool { return q.list.IsEmpty() } 29 | 30 | // Clear discards all the values in q, leaving it empty. 31 | func (q *Queue[T]) Clear() { q.list.Clear(); q.back = q.list.cfirst(); q.size = 0 } 32 | 33 | // Front returns the frontmost (oldest) element of the queue. If the queue is 34 | // empty, it returns a zero value. 35 | func (q *Queue[T]) Front() T { v, _ := q.list.Peek(0); return v } 36 | 37 | // Peek reports whether q has a value at offset n from the front of the queue, 38 | // and if so returns its value. Peek(0) returns the same value as Front. 39 | // 40 | // Peek will panic if n < 0. 41 | func (q *Queue[T]) Peek(n int) (T, bool) { return q.list.Peek(n) } 42 | 43 | // Pop reports whether q is non-empty, and if so removes and returns its 44 | // frontmost (oldest) value. 45 | func (q *Queue[T]) Pop() (T, bool) { 46 | cur := q.list.cfirst() 47 | out := cur.Get() 48 | if cur.AtEnd() { 49 | return out, false 50 | } 51 | cur.Remove() 52 | q.size-- 53 | if q.list.IsEmpty() { 54 | q.back = q.list.cfirst() 55 | } 56 | return out, true 57 | } 58 | 59 | // Each is a range function that calls f with each value in q, in order from 60 | // oldest to newest. If f returns false, Each returns immediately. 61 | func (q *Queue[T]) Each(f func(T) bool) { q.list.Each(f) } 62 | 63 | // Len reports the number of elements in q. This is a constant-time operation. 64 | func (q *Queue[T]) Len() int { return q.size } 65 | -------------------------------------------------------------------------------- /mstr/mstr.go: -------------------------------------------------------------------------------- 1 | // Package mstr defines utility functions for strings. 2 | package mstr 3 | 4 | import ( 5 | "cmp" 6 | "strings" 7 | ) 8 | 9 | // Trunc returns a prefix of s having length no greater than n bytes. If s 10 | // exceeds this length, it is truncated at a point ≤ n so that the result does 11 | // not end in a partial UTF-8 encoding. Trunc does not verify that s is valid 12 | // UTF-8, but if it is the result will remain valid after truncation. 13 | func Trunc[String ~string | ~[]byte](s String, n int) String { 14 | if n >= len(s) { 15 | return s 16 | } 17 | 18 | // Back up until we find the beginning of a UTF-8 encoding. 19 | for n > 0 && s[n-1]&0xc0 == 0x80 { // 0b10... is a continuation byte 20 | n-- 21 | } 22 | 23 | // If we're at the beginning of a multi-byte encoding, back up one more to 24 | // skip it. It's possible the value was already complete, but it's simpler 25 | // if we only have to check in one direction. 26 | // 27 | // Otherwise, we have a single-byte code (0b00... or 0b01...). 28 | if n > 0 && s[n-1]&0xc0 == 0xc0 { // 0b11... starts a multibyte encoding 29 | n-- 30 | } 31 | return s[:n] 32 | } 33 | 34 | // Lines splits its argument on newlines. It is a convenience function for 35 | // [strings.Split], except that it returns empty if s == "" and treats a 36 | // trailing newline as the end of the file rather than an empty line. 37 | func Lines(s string) []string { 38 | if s == "" { 39 | return nil 40 | } 41 | return strings.Split(strings.TrimSuffix(s, "\n"), "\n") 42 | } 43 | 44 | // Split splits its argument on sep. It is a convenience function for 45 | // [strings.Split], except that it returns empty if s == "". 46 | func Split(s, sep string) []string { 47 | if s == "" { 48 | return nil 49 | } 50 | return strings.Split(s, sep) 51 | } 52 | 53 | // CompareNatural compares its arguments lexicographically, but treats runs of 54 | // decimal digits as the spellings of natural numbers and compares their values 55 | // instead of the individual digits. 56 | // 57 | // For example, "a2b" is after "a12b" under ordinary lexicographic comparison, 58 | // but before under CompareNatural, because 2 < 12. However, if one argument 59 | // has digits and the other has non-digits at that position (see for example 60 | // "a" vs. "12") the comparison falls back to lexicographic. 61 | // 62 | // CompareNatural returns -1 if a < b, 0 if a == b, and +1 if a > b. 63 | func CompareNatural(a, b string) int { 64 | for a != "" && b != "" { 65 | va, ra, aok := parseInt(a) 66 | vb, rb, bok := parseInt(b) 67 | 68 | if aok && bok { 69 | // Both begin with runs of digits, compare them numerically. 70 | if c := cmp.Compare(va, vb); c != 0 { 71 | return c 72 | } 73 | a, b = ra, rb 74 | continue 75 | } else if aok != bok { 76 | // One begins with digits, the other does not. 77 | // They cannot be equal, so compare them lexicographically. 78 | return cmp.Compare(a, b) 79 | } 80 | 81 | // Neither begins with digits. Compare runs of non-digits. 82 | pa, ra := parseStr(a) 83 | pb, rb := parseStr(b) 84 | if c := cmp.Compare(pa, pb); c != 0 { 85 | return c 86 | } 87 | a, b = ra, rb 88 | } 89 | return cmp.Compare(a, b) 90 | } 91 | 92 | // parseInt reports whether s begins with a run of one or more decimal digits, 93 | // and if so returns the value of that run, along with the unconsumed tail of 94 | // the string. 95 | func parseInt(s string) (int, string, bool) { 96 | var i, v int 97 | for i < len(s) && isDigit(s[i]) { 98 | v = (v * 10) + int(s[i]-'0') 99 | i++ 100 | } 101 | return v, s[i:], i > 0 102 | } 103 | 104 | // parseStr returns the longest prefix of s not containing decimal digits, 105 | // along with the remaining suffix of s. 106 | func parseStr(s string) (pfx, sfx string) { 107 | var i int 108 | for i < len(s) && !isDigit(s[i]) { 109 | i++ 110 | } 111 | return s[:i], s[i:] 112 | } 113 | 114 | func isDigit(b byte) bool { return b >= '0' && b <= '9' } 115 | -------------------------------------------------------------------------------- /mstr/mstr_test.go: -------------------------------------------------------------------------------- 1 | package mstr_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/mstr" 7 | gocmp "github.com/google/go-cmp/cmp" 8 | ) 9 | 10 | func TestTrunc(t *testing.T) { 11 | tests := []struct { 12 | input string 13 | size int 14 | want string 15 | }{ 16 | {"", 0, ""}, // n == length 17 | {"", 1000, ""}, // n > length 18 | {"abc", 4, "abc"}, // n > length 19 | {"abc", 3, "abc"}, // n == length 20 | {"abcdefg", 4, "abcd"}, // n < length, safe 21 | {"abcdefg", 0, ""}, // n < length, safe 22 | {"abc\U0001f60a", 3, "abc"}, // n < length, at boundary 23 | {"abc\U0001f60a", 4, "abc"}, // n < length, mid-rune 24 | {"abc\U0001f60a", 5, "abc"}, // n < length, mid-rune 25 | {"abc\U0001f60a", 6, "abc"}, // n < length, mid-rune 26 | {"abc\U0001f60axxx", 7, "abc"}, // n < length, cut multibyte 27 | {"abc\U0001f60axxx", 8, "abc\U0001f60ax"}, // n < length, keep multibyte 28 | } 29 | 30 | for _, tc := range tests { 31 | t.Logf("Input %q len=%d n=%d", tc.input, len(tc.input), tc.size) 32 | got := mstr.Trunc(tc.input, tc.size) 33 | if got != tc.want { 34 | t.Errorf("Trunc(%q, %d) [string]: got %q, want %q", tc.input, tc.size, got, tc.want) 35 | } 36 | if got := mstr.Trunc([]byte(tc.input), tc.size); string(got) != tc.want { 37 | t.Errorf("Trunc(%q, %d) [bytes]: got %q, want %q", tc.input, tc.size, got, tc.want) 38 | } 39 | } 40 | } 41 | 42 | func TestLines(t *testing.T) { 43 | tests := []struct { 44 | input string 45 | want []string 46 | }{ 47 | {"", nil}, 48 | {" ", []string{" "}}, 49 | {"\n", []string{""}}, 50 | {"\n ", []string{"", " "}}, 51 | {"a\n", []string{"a"}}, 52 | {"\na\n", []string{"", "a"}}, 53 | {"a\nb\n", []string{"a", "b"}}, 54 | {"a\nb", []string{"a", "b"}}, 55 | {"\n\n\n", []string{"", "", ""}}, 56 | {"\n\nq", []string{"", "", "q"}}, 57 | {"\n\nq\n", []string{"", "", "q"}}, 58 | {"a b\nc\n\n", []string{"a b", "c", ""}}, 59 | {"a b\nc\n\nd\n", []string{"a b", "c", "", "d"}}, 60 | } 61 | for _, tc := range tests { 62 | if diff := gocmp.Diff(mstr.Lines(tc.input), tc.want); diff != "" { 63 | t.Errorf("Lines %q (-got, +want):\n%s", tc.input, diff) 64 | } 65 | } 66 | } 67 | 68 | func TestSplit(t *testing.T) { 69 | tests := []struct { 70 | input, sep string 71 | want []string 72 | }{ 73 | {"", "x", nil}, 74 | {"y", "x", []string{"y"}}, 75 | {"x", "x", []string{"", ""}}, 76 | {"ax", "x", []string{"a", ""}}, 77 | {"xa", "x", []string{"", "a"}}, 78 | {"axbxc", "x", []string{"a", "b", "c"}}, 79 | {"axxc", "x", []string{"a", "", "c"}}, 80 | {"a,b,c,,d", ",", []string{"a", "b", "c", "", "d"}}, 81 | } 82 | for _, tc := range tests { 83 | if diff := gocmp.Diff(mstr.Split(tc.input, tc.sep), tc.want); diff != "" { 84 | t.Errorf("Split %q on %q (-got, +want):\n%s", tc.input, tc.sep, diff) 85 | } 86 | } 87 | } 88 | 89 | func TestCompareNatural(t *testing.T) { 90 | tests := []struct { 91 | a, b string 92 | want int 93 | }{ 94 | {"", "", 0}, 95 | 96 | // Non-empty vs. empty with non-digits and digits. 97 | {"x", "", 1}, 98 | {"", "x", -1}, 99 | {"0", "", 1}, 100 | {"", "0", -1}, 101 | 102 | // Leading zeroes do not change the value. 103 | {"1", "1", 0}, 104 | {"01", "1", 0}, 105 | {"1", "01", 0}, 106 | 107 | // Mixed values. 108 | {"a1", "a1", 0}, 109 | {"a2", "a1", 1}, 110 | {"a1", "a2", -1}, 111 | {"6c", "06c", 0}, 112 | {"06c", "6c", 0}, 113 | {"5c", "06c", -1}, 114 | {"07c", "6c", 1}, 115 | 116 | // Multi-digit numeric runs. 117 | {"a2b", "a25b", -1}, 118 | {"a12b", "a2", 1}, 119 | {"a25b", "a21b", 1}, 120 | {"a025b", "a25b", 0}, 121 | 122 | // Non-matching types compare lexicographically. 123 | // Note it is not possible for these to be equal. 124 | {"123", "a", -1}, // because 'a' > '1' 125 | {"123", ".", 1}, // because '.' < '1' 126 | {"12c9", "12cv", -1}, // because 'v' > '9' 127 | 128 | // Normal lexicographic comparison, without digits. 129 | {"a-b-c", "a-b-c", 0}, 130 | {"a-b-c", "a-b-d", -1}, 131 | {"a-b-c-d", "a-b-d", -1}, 132 | {"a-q", "a-b-c", 1}, 133 | {"a-q-c", "a-b-c", 1}, 134 | 135 | // Complicated cases ("v" indicates the point of divergence). 136 | // v v 137 | {"test1-143a19", "test01-143b13", -1}, 138 | // v v 139 | {"test5-143a21", "test04-999", 1}, 140 | // v v 'w' > '9' 141 | {"test5-word-5", "test5-999-5", 1}, 142 | } 143 | for _, tc := range tests { 144 | if got := mstr.CompareNatural(tc.a, tc.b); got != tc.want { 145 | t.Errorf("Compare(%q, %q): got %v, want %v", tc.a, tc.b, got, tc.want) 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /mtest/mtest.go: -------------------------------------------------------------------------------- 1 | // Package mtest is a support library for writing tests. 2 | package mtest 3 | 4 | // TB is the subset of the testing.TB interface used by this package. 5 | type TB interface { 6 | Cleanup(func()) 7 | Fatalf(string, ...any) 8 | Helper() 9 | } 10 | 11 | // MustPanic executes a function f that is expected to panic. 12 | // If it does so, MustPanic returns the value recovered from the 13 | // panic. Otherwise, it logs a fatal error in t. 14 | func MustPanic(t TB, f func()) any { 15 | t.Helper() 16 | return MustPanicf(t, f, "expected panic was not observed") 17 | } 18 | 19 | // MustPanicf executes a function f that is expected to panic. If it does so, 20 | // MustPanicf returns the value recovered from the panic. Otherwise it logs a 21 | // fatal error in t. 22 | func MustPanicf(t TB, f func(), msg string, args ...any) (val any) { 23 | t.Helper() 24 | defer func() { val = recover() }() 25 | f() 26 | t.Fatalf(msg, args...) 27 | return 28 | } 29 | 30 | // Swap replaces the target of p with v, and restores the original value when 31 | // the governing test exits. It returns the original value. 32 | func Swap[T any](t TB, p *T, v T) T { 33 | save := *p 34 | *p = v 35 | t.Cleanup(func() { *p = save }) 36 | return save 37 | } 38 | -------------------------------------------------------------------------------- /mtest/mtest_test.go: -------------------------------------------------------------------------------- 1 | package mtest_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/creachadair/mds/mtest" 8 | ) 9 | 10 | // testStub implements the mtest.TB interface as a capturing shim to verify 11 | // that test failures are reported properly. 12 | type testStub struct { 13 | failed bool 14 | text string 15 | } 16 | 17 | func (t *testStub) Fatal(args ...any) { 18 | t.failed = true 19 | t.text = fmt.Sprint(args...) 20 | } 21 | 22 | func (t *testStub) Fatalf(msg string, args ...any) { 23 | t.failed = true 24 | t.text = fmt.Sprintf(msg, args...) 25 | } 26 | 27 | func (*testStub) Helper() {} 28 | func (*testStub) Cleanup(func()) {} 29 | 30 | func TestMustPanic(t *testing.T) { 31 | t.Run("OK", func(t *testing.T) { 32 | v := mtest.MustPanic(t, func() { panic("pass") }) 33 | t.Logf("Panic reported: %v", v) 34 | }) 35 | 36 | t.Run("Fail", func(t *testing.T) { 37 | var s testStub 38 | v := mtest.MustPanic(&s, func() {}) 39 | if !s.failed { 40 | t.Error("Test did not fail as expected") 41 | } 42 | if s.text == "" { 43 | t.Error("Failure did not log a message") 44 | } 45 | if v != nil { 46 | t.Errorf("Unexpected panic value: %v", v) 47 | } 48 | }) 49 | } 50 | 51 | func TestMustPanicf(t *testing.T) { 52 | t.Run("OK", func(t *testing.T) { 53 | v := mtest.MustPanicf(t, func() { panic("pass") }, "bad things") 54 | t.Logf("Panic reported: %v", v) 55 | }) 56 | 57 | t.Run("Fail", func(t *testing.T) { 58 | var s testStub 59 | v := mtest.MustPanicf(&s, func() {}, "bad: %d", 11) 60 | if !s.failed { 61 | t.Error("Test did not fail as expected") 62 | } 63 | if s.text != "bad: 11" { 64 | t.Errorf("Wrong message: got %q, want bad: 11", s.text) 65 | } 66 | if v != nil { 67 | t.Errorf("Unexpected panic value: %v", v) 68 | } 69 | }) 70 | } 71 | 72 | func TestSwap(t *testing.T) { 73 | testValue := "original" 74 | 75 | t.Run("Swapped", func(t *testing.T) { 76 | old := mtest.Swap(t, &testValue, "replacement") 77 | 78 | if old != "original" { 79 | t.Errorf("Old value is %q, want original", old) 80 | } 81 | if testValue != "replacement" { 82 | t.Errorf("Test value is %q, want replacement", testValue) 83 | } 84 | }) 85 | 86 | t.Run("NoSwap", func(t *testing.T) { 87 | if testValue != "original" { 88 | t.Errorf("Test value is %q, want original", testValue) 89 | } 90 | }) 91 | 92 | if testValue != "original" { 93 | t.Errorf("Test value after is %q, want original", testValue) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /omap/example_test.go: -------------------------------------------------------------------------------- 1 | package omap_test 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/creachadair/mds/omap" 9 | ) 10 | 11 | const input = ` 12 | the thousand injuries of fortunato i had borne as i best could but when he 13 | ventured upon insult i vowed revenge you who so well know the nature of my soul 14 | will not suppose however that i gave utterance to a threat at length i would be 15 | avenged this was a point definitely settled but the very definitiveness with 16 | which it was resolved precluded the idea of risk i must not only punish but 17 | punish with impunity a wrong is unredressed when retribution overtakes its 18 | redresser it is equally unredressed when the avenger fails to make himself felt 19 | as such to him who has done the wrong it must be understood that neither by 20 | word nor deed had i given fortunato cause to doubt my good will i continued as 21 | was my wont to smile in his face and he did not perceive that my smile now was 22 | at the thought of his immolation 23 | ` 24 | 25 | func ExampleMap() { 26 | // Construct a map on a naturally ordered key (string). 27 | m := omap.New[string, int]() 28 | for _, w := range strings.Fields(input) { 29 | m.Set(w, m.Get(w)+1) 30 | } 31 | 32 | // Construct a map with an explicit ordering function. 33 | c := omap.NewFunc[int, string](cmp.Compare) 34 | 35 | // Traverse a map in key order. 36 | for it := m.First(); it.IsValid(); it.Next() { 37 | c.Set(it.Value(), it.Key()) 38 | } 39 | 40 | // Traverse a map in reverse order. 41 | for it := c.Last(); it.IsValid(); it.Prev() { 42 | fmt.Println(it.Value(), it.Key()) 43 | if it.Key() <= 3 { 44 | break 45 | } 46 | } 47 | 48 | // Output: 49 | // 50 | // i 8 51 | // the 7 52 | // to 5 53 | // was 4 54 | // when 3 55 | } 56 | -------------------------------------------------------------------------------- /omap/omap.go: -------------------------------------------------------------------------------- 1 | // Package omap implements a map-like collection on ordered keys. 2 | // 3 | // # Basic Operations 4 | // 5 | // Create an empty map with New or NewFunc. A zero-valued Map is ready for use 6 | // as a read-only empty map, but it will panic if modified. 7 | // 8 | // m := omap.New[string, int]() 9 | // 10 | // Add items using Set and remove items using Delete: 11 | // 12 | // m.Set("apple", 1) 13 | // m.Delete("pear") 14 | // 15 | // Look up items using Get and GetOK: 16 | // 17 | // v := m.Get(key) // returns a zero value if key not found 18 | // v, ok := m.GetOK(key) // ok indicates whether key was found 19 | // 20 | // Report the number of elements in the map using Len. 21 | // 22 | // # Iterating in Order 23 | // 24 | // The elements of a map can be traversed in order using an iterator. 25 | // Construct an iterator for m by calling First or Last. The IsValid 26 | // method reports whether the iterator has an element available, and 27 | // the Next and Prev methods advance or retract the iterator: 28 | // 29 | // for it := m.First(); it.IsValid(); it.Next() { 30 | // doThingsWith(it.Key(), it.Value()) 31 | // } 32 | // 33 | // Use the Seek method to seek to a particular point in the order. Seek 34 | // returns an iterator at the first item greater than or equal to the specified 35 | // key: 36 | // 37 | // for it := m.Seek("cherry"); it.IsValid(); it.Next() { 38 | // doThingsWith(it.Key(), it.Value()) 39 | // } 40 | // 41 | // Note that it is not safe to modify the map while iterating it. If you 42 | // modify a map while iterating it, you will need to re-synchronize any 43 | // iterators after the edits, e.g., 44 | // 45 | // for it := m.First(); it.IsValid(); { 46 | // if key := it.Key(); shouldDelete(key) { 47 | // m.Delete(key) 48 | // it.Seek(key) // update the iterator 49 | // } else { 50 | // it.Next() 51 | // } 52 | // } 53 | package omap 54 | 55 | import ( 56 | "cmp" 57 | "fmt" 58 | "strings" 59 | 60 | "github.com/creachadair/mds/stree" 61 | ) 62 | 63 | // A Map represents a mapping over arbitrary key and value types. It supports 64 | // efficient insertion, deletion and lookup, and also allows keys to be 65 | // traversed in order. 66 | // 67 | // A zero Map behaves as an empty read-only map, and Clear, Delete, Get, Keys, 68 | // Len, First, and Last will work without error; however, calling Set on a zero 69 | // Map will panic. 70 | type Map[T, U any] struct { 71 | m *stree.Tree[stree.KV[T, U]] 72 | } 73 | 74 | // New constructs a new empty Map using the natural comparison order for an 75 | // ordered key type. Copies of the map share storage. 76 | func New[T cmp.Ordered, U any]() Map[T, U] { return NewFunc[T, U](cmp.Compare) } 77 | 78 | // NewFunc constructs a new empty Map using cf to compare keys. If cf == nil, 79 | // NewFunc will panic. Copies of the map share storage. 80 | func NewFunc[T, U any](cf func(a, b T) int) Map[T, U] { 81 | type kv = stree.KV[T, U] 82 | return Map[T, U]{m: stree.New(250, kv{}.Compare(cf))} 83 | } 84 | 85 | // String returns a string representation of the contents of m. 86 | func (m Map[T, U]) String() string { 87 | if m.m == nil { 88 | return `omap[]` 89 | } 90 | var sb strings.Builder 91 | sb.WriteString("omap[") 92 | 93 | sp := "%v:%v" 94 | for it := m.First(); it.IsValid(); it.Next() { 95 | fmt.Fprintf(&sb, sp, it.Key(), it.Value()) 96 | sp = " %v:%v" 97 | } 98 | sb.WriteString("]") 99 | return sb.String() 100 | } 101 | 102 | // Len reports the number of key-value pairs in m. 103 | // This operation is constant-time. 104 | func (m Map[T, U]) Len() int { 105 | if m.m == nil { 106 | return 0 107 | } 108 | return m.m.Len() 109 | } 110 | 111 | // Get returns the value associated with key in m if it is present, or returns 112 | // a zero value. To check for presence, use GetOK. 113 | func (m Map[T, U]) Get(key T) U { u, _ := m.GetOK(key); return u } 114 | 115 | // GetOK reports whether key is present in m, and if so returns the value 116 | // associated with it, or otherwise a zero value. 117 | // 118 | // This operation takes O(lg n) time for a map with n elements. 119 | func (m Map[T, U]) GetOK(key T) (U, bool) { 120 | if m.m != nil { 121 | kv, ok := m.m.Get(stree.KV[T, U]{Key: key}) 122 | if ok { 123 | return kv.Value, true 124 | } 125 | } 126 | var zero U 127 | return zero, false 128 | } 129 | 130 | // Set adds or replaces the value associated with key in m, and reports whether 131 | // the key was new (true) or updated (false). 132 | // 133 | // This operation takes amortized O(lg n) time for a map with n elements. 134 | func (m Map[T, U]) Set(key T, value U) bool { 135 | return m.m.Replace(stree.KV[T, U]{Key: key, Value: value}) 136 | } 137 | 138 | // Delete deletes the specified key from m, and reports whether it was present. 139 | // 140 | // This operation takes amortized O(lg n) time for a map with n elements. 141 | func (m Map[T, U]) Delete(key T) bool { 142 | if m.m == nil { 143 | return false 144 | } 145 | return m.m.Remove(stree.KV[T, U]{Key: key}) 146 | } 147 | 148 | // Clear deletes all the elements from m, leaving it empty. 149 | // 150 | // This operation is constant-time. 151 | func (m Map[T, U]) Clear() { 152 | if m.m != nil { 153 | m.m.Clear() 154 | } 155 | } 156 | 157 | // Keys returns a slice of all the keys in m, in order. 158 | func (m Map[T, U]) Keys() []T { 159 | if m.m == nil || m.m.Len() == 0 { 160 | return nil 161 | } 162 | out := make([]T, 0, m.Len()) 163 | for kv := range m.m.Inorder { 164 | out = append(out, kv.Key) 165 | } 166 | return out 167 | } 168 | 169 | // First returns an iterator to the first entry of the map, if any. 170 | func (m Map[T, U]) First() *Iter[T, U] { 171 | it := &Iter[T, U]{m: m.m} 172 | if m.m != nil { 173 | it.c = m.m.Root().Min() 174 | } 175 | return it 176 | } 177 | 178 | // Last returns an iterator to the last entry of the map, if any. 179 | func (m Map[T, U]) Last() *Iter[T, U] { 180 | it := &Iter[T, U]{m: m.m} 181 | if m.m != nil { 182 | it.c = m.m.Root().Max() 183 | } 184 | return it 185 | } 186 | 187 | // Seek returns an iterator to the first entry of the map whose key is greater 188 | // than or equal to key, if any. 189 | func (m Map[T, U]) Seek(key T) *Iter[T, U] { return m.First().Seek(key) } 190 | 191 | // An Iter is an iterator for a Map. 192 | type Iter[T, U any] struct { 193 | m *stree.Tree[stree.KV[T, U]] 194 | c *stree.Cursor[stree.KV[T, U]] 195 | } 196 | 197 | // IsValid reports whether it is pointing at an element of its map. 198 | func (it *Iter[T, U]) IsValid() bool { return it.c.Valid() } 199 | 200 | // Next advances it to the next element in the map, if any. 201 | func (it *Iter[T, U]) Next() *Iter[T, U] { it.c.Next(); return it } 202 | 203 | // Prev advances it to the previous element in the map, if any. 204 | func (it *Iter[T, U]) Prev() *Iter[T, U] { it.c.Prev(); return it } 205 | 206 | // Key returns the current key, or a zero key if it is invalid. 207 | func (it *Iter[T, U]) Key() T { return it.c.Key().Key } 208 | 209 | // Value returns the current value, or a zero value if it is invalid. 210 | func (it *Iter[T, U]) Value() U { return it.c.Key().Value } 211 | 212 | // Seek advances it to the first key greater than or equal to key. 213 | // If no such key exists, it becomes invalid. 214 | func (it *Iter[T, U]) Seek(key T) *Iter[T, U] { 215 | it.c = nil 216 | if it.m != nil { 217 | for kv := range it.m.InorderAfter(stree.KV[T, U]{Key: key}) { 218 | it.c = it.m.Cursor(kv) 219 | break 220 | } 221 | } 222 | return it 223 | } 224 | -------------------------------------------------------------------------------- /omap/omap_test.go: -------------------------------------------------------------------------------- 1 | package omap_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/mtest" 7 | "github.com/creachadair/mds/omap" 8 | gocmp "github.com/google/go-cmp/cmp" 9 | ) 10 | 11 | func TestMap(t *testing.T) { 12 | m := omap.New[string, int]() 13 | checkGet := func(key string, want int) { 14 | t.Helper() 15 | v := m.Get(key) 16 | if v != want { 17 | t.Errorf("Get %q: got %d, want %d", key, v, want) 18 | } 19 | } 20 | checkLen := func(want int) { 21 | t.Helper() 22 | if n := m.Len(); n != want { 23 | t.Errorf("Len: got %d, want %d", n, want) 24 | } 25 | } 26 | 27 | checkLen(0) 28 | 29 | m.Set("apple", 1) 30 | m.Set("pear", 2) 31 | m.Set("plum", 3) 32 | m.Set("cherry", 4) 33 | 34 | checkLen(4) 35 | 36 | checkGet("apple", 1) 37 | checkGet("pear", 2) 38 | checkGet("plum", 3) 39 | checkGet("cherry", 4) 40 | checkGet("dog", 0) // i.e., not found 41 | 42 | m.Set("plum", 100) 43 | checkGet("plum", 100) 44 | 45 | // Note we want the string to properly reflect the map ordering. 46 | if got, want := m.String(), `omap[apple:1 cherry:4 pear:2 plum:100]`; got != want { 47 | t.Errorf("String:\n got: %q\nwant: %q", got, want) 48 | } 49 | 50 | var got []string 51 | for it := m.First(); it.IsValid(); it.Next() { 52 | got = append(got, it.Key()) 53 | } 54 | if diff := gocmp.Diff(got, []string{"apple", "cherry", "pear", "plum"}); diff != "" { 55 | t.Errorf("Iter (-got, +want):\n%s", diff) 56 | } 57 | if diff := gocmp.Diff(m.Keys(), []string{"apple", "cherry", "pear", "plum"}); diff != "" { 58 | t.Errorf("Keys (-got, +want):\n%s", diff) 59 | } 60 | 61 | got = got[:0] 62 | for it := m.Seek("dog"); it.IsValid(); it.Next() { 63 | got = append(got, it.Key()) 64 | } 65 | if diff := gocmp.Diff(got, []string{"pear", "plum"}); diff != "" { 66 | t.Errorf("Seek dog (-got, +want):\n%s", diff) 67 | } 68 | 69 | if m.Delete("dog") { 70 | t.Error("Delete(dog) incorrectly reported true") 71 | } 72 | checkLen(4) 73 | 74 | if !m.Delete("pear") { 75 | t.Error("Delete(pear) incorrectly reported false") 76 | } 77 | checkGet("pear", 0) 78 | checkLen(3) 79 | 80 | m.Clear() 81 | checkLen(0) 82 | } 83 | 84 | func TestZero(t *testing.T) { 85 | var zero omap.Map[string, string] 86 | 87 | if zero.Len() != 0 { 88 | t.Errorf("Len is %d, want 0", zero.Len()) 89 | } 90 | if v, ok := zero.GetOK("whatever"); ok || v != "" { 91 | t.Errorf(`Get whatever: got (%q, %v), want ("", false)`, v, ok) 92 | } 93 | if zero.Delete("whatever") { 94 | t.Error("Delete(whatever) incorrectly reported true") 95 | } 96 | if it := zero.First(); it.IsValid() { 97 | t.Errorf("Iter zero: unexected key %q=%q", it.Key(), it.Value()) 98 | } 99 | if it := zero.First().Seek("whatever"); it.IsValid() { 100 | t.Errorf("Seek(whatever): unexected key %q=%q", it.Key(), it.Value()) 101 | } 102 | zero.Clear() // don't panic 103 | 104 | mtest.MustPanicf(t, func() { zero.Set("bad", "mojo") }, 105 | "Set on a zero map should panic") 106 | } 107 | 108 | func TestIterEdit(t *testing.T) { 109 | m := omap.New[string, int]() 110 | 111 | m.Set("a", 1) 112 | m.Set("b", 2) 113 | m.Set("c", 3) 114 | m.Set("d", 4) 115 | m.Set("e", 5) 116 | 117 | var got []string 118 | for it := m.First(); it.IsValid(); { 119 | key := it.Key() 120 | if key == "b" || key == "d" { 121 | m.Delete(key) 122 | it.Seek(key) 123 | } else { 124 | got = append(got, key) 125 | it.Next() 126 | } 127 | } 128 | if diff := gocmp.Diff(got, []string{"a", "c", "e"}); diff != "" { 129 | t.Errorf("Result (-got, +want):\n%s", diff) 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /queue/queue.go: -------------------------------------------------------------------------------- 1 | // Package queue implements an array-based FIFO queue. 2 | package queue 3 | 4 | import ( 5 | "slices" 6 | 7 | "github.com/creachadair/mds/slice" 8 | ) 9 | 10 | // Queue is an array-based queue of values. A zero Queue is ready for use. 11 | // Items can be added and removed at either end of the queue. Use Add and Pop 12 | // for last-in, first-out semantics; use Push and PopLast for first-in, 13 | // first-out semantics. 14 | // 15 | // Add, Push, Pop, and PopLast operations take amortized O(1) time and storage. 16 | // All other operations on a Queue are constant time and space. 17 | type Queue[T any] struct { 18 | vs []T 19 | head int 20 | n int 21 | } 22 | 23 | // New constructs a new empty queue. 24 | func New[T any]() *Queue[T] { return new(Queue[T]) } 25 | 26 | // NewSize constructs a new empty queue with storage pre-allocated for n items. 27 | // The queue will automatically grow beyond the initial size as needed. 28 | func NewSize[T any](n int) *Queue[T] { return &Queue[T]{vs: make([]T, n)} } 29 | 30 | // Add adds v to the end (tail) of q. 31 | func (q *Queue[T]) Add(v T) { 32 | if q.n < len(q.vs) { 33 | // We have spaces left in the buffer. 34 | pos := q.head + q.n 35 | if pos >= len(q.vs) { 36 | pos -= len(q.vs) 37 | } 38 | q.vs[pos] = v 39 | q.n++ 40 | return 41 | } 42 | 43 | if q.head > 0 { 44 | // Shift the existing items to initial position so that the append below 45 | // can handle extending the buffer. This costs O(1) space, O(n) time; but 46 | // we amortize this against the allocation we're (probably) going to do. 47 | slice.Rotate(q.vs, -q.head) 48 | q.head = 0 49 | } 50 | 51 | // The buffer is in the initial regime, head == 0. 52 | w := append(q.vs, v) 53 | q.vs = w[:cap(w)] 54 | q.n++ 55 | } 56 | 57 | // Push adds v to the front (head) of q. 58 | func (q *Queue[T]) Push(v T) { 59 | if q.n < len(q.vs) { 60 | // We have spaces left in the buffer. 61 | pos := q.head - 1 62 | if pos < 0 { 63 | pos = len(q.vs) - 1 64 | } 65 | q.vs[pos] = v 66 | q.head = pos 67 | q.n++ 68 | return 69 | } 70 | 71 | if q.head > 0 { 72 | slice.Rotate(q.vs, -q.head) // as in Add 73 | q.head = 0 74 | } 75 | 76 | // N.B. This append does not put v in the correct location, we just need a 77 | // value to trigger the reallocation. 78 | w := append(q.vs, v) 79 | q.vs = w[:cap(w)] 80 | q.head = len(q.vs) - 1 81 | q.vs[q.head] = v 82 | q.n++ 83 | } 84 | 85 | // IsEmpty reports whether q is empty. 86 | func (q *Queue[T]) IsEmpty() bool { return q.n == 0 } 87 | 88 | // Len reports the number of entries in q. 89 | func (q *Queue[T]) Len() int { return q.n } 90 | 91 | // Clear discards all the values in q, leaving it empty. 92 | func (q *Queue[T]) Clear() { q.vs, q.head, q.n = nil, 0, 0 } 93 | 94 | // Front returns the frontmost (oldest) element of q. If q is empty, Front 95 | // returns a zero value. 96 | func (q *Queue[T]) Front() T { 97 | if q.n == 0 { 98 | var zero T 99 | return zero 100 | } 101 | return q.vs[q.head] 102 | } 103 | 104 | // Peek reports whether q has a value at offset n from the front of the queue, 105 | // and if so returns its value. Peek(0) returns the same value as Front. 106 | // Negative offsets count forward from the end of the queue. 107 | func (q *Queue[T]) Peek(n int) (T, bool) { 108 | if n < 0 { 109 | n += q.n 110 | } 111 | if n < 0 || n >= q.n { 112 | var zero T 113 | return zero, false 114 | } 115 | p := (q.head + n) % len(q.vs) 116 | return q.vs[p], true 117 | } 118 | 119 | // Pop reports whether q is non-empty, and if so removes and returns its 120 | // frontmost (oldest) value. If q is empty, Pop returns a zero value. 121 | func (q *Queue[T]) Pop() (T, bool) { 122 | if q.n == 0 { 123 | var zero T 124 | return zero, false 125 | } 126 | out := q.vs[q.head] 127 | q.n-- 128 | if q.n == 0 { 129 | q.head = 0 // reset to initial conditions 130 | } else { 131 | q.head = (q.head + 1) % len(q.vs) 132 | } 133 | return out, true 134 | } 135 | 136 | // PopLast reports whether q is non-empty, and if so removes and returns its 137 | // rearmost (newest) value. If q is empty, PopLast returns a zero value. 138 | func (q *Queue[T]) PopLast() (T, bool) { 139 | if q.n == 0 { 140 | var zero T 141 | return zero, false 142 | } 143 | pos := q.head + q.n - 1 144 | if pos >= len(q.vs) { 145 | pos -= len(q.vs) 146 | } 147 | out := q.vs[pos] 148 | q.n-- 149 | if q.n == 0 { 150 | q.head = 0 // reset to initial conditions 151 | } 152 | return out, true 153 | } 154 | 155 | // Each is a range function that calls f with each value, in q, in order from 156 | // oldest to newest. If f returns false, Each returns immediately. 157 | func (q *Queue[T]) Each(f func(T) bool) { 158 | cur := q.head 159 | for range q.n { 160 | if !f(q.vs[cur]) { 161 | return 162 | } 163 | cur = (cur + 1) % len(q.vs) 164 | } 165 | } 166 | 167 | // Slice returns a slice of the values of q in order from oldest to newest. 168 | // If q is empty, Slice returns nil. 169 | func (q *Queue[T]) Slice() []T { return q.Append(nil) } 170 | 171 | // Append appends the values of q to ts in order from oldest to newest, and 172 | // returns the resulting slice. 173 | func (q *Queue[T]) Append(ts []T) []T { 174 | buf := slices.Grow(ts, q.n) 175 | cur := q.head 176 | for range q.n { 177 | buf = append(buf, q.vs[cur]) 178 | cur = (cur + 1) % len(q.vs) 179 | } 180 | return buf 181 | } 182 | 183 | /* 184 | A queue is an expanding ring buffer with amortized O(1) access. 185 | 186 | The queue tracks a buffer (buf) and two values, the head (H) is the offset of 187 | the oldest item in the queue (if any), and the length (n) is the number of 188 | queue entries. 189 | 190 | Initially the queue is empty, n = 0 and H = 0. 191 | 192 | As long as there is unused space, n < len(buf), we can add to the queue by 193 | simply bumping the length and storing the item in the next unused slot. 194 | 195 | When items are removed from the queue, H moves forward, leaving spaces at the 196 | beginning of the ring: 197 | 198 | * * * d e f g h i 199 | - - - - - - - - - 200 | H 201 | 202 | In this regime, a new item (j) wraps around and consumes an empty slot: 203 | 204 | j * * d e f g h i 205 | - - - - - - - - - 206 | ^ H 207 | 208 | If the queue is empty after removing an item (n = 0, we can reset to the 209 | initial condition by setting H = 0, since it no longer matters where H is 210 | when there are no values. 211 | 212 | Once the buffer fills (n = len(buf)), there are two cases to consider: In the 213 | simple case, when H == 0, new items are appended to the end, extending the 214 | buffer (and the append function handles amortized allocation for us): 215 | 216 | a b c d e f g 217 | - - - - - - - 218 | H ^ next 219 | 220 | On the other hand, if H > 0, we cannot append directly to the end of buf, 221 | because that will put it out of order with respect to the offsets < H. To 222 | fix this, we rotate the contents of buf forward so that H = 0 again, at which 223 | point we can now safely append again: 224 | 225 | 1. Before insert, the buffer is full with H > 0: 226 | 227 | j k l d e f g h i 228 | - - - - - - - - - 229 | H 230 | 231 | 2. Rotate the elements down to offset 0. This can be done in O(n) time 232 | in-place by chasing the cycles of the rotation: 233 | 234 | < < < rotate 235 | 236 | d e f g h i j k l 237 | - - - - - - - - - 238 | H 239 | 240 | At this point we are back in the initial regime. We can append m to grow buf: 241 | 242 | d e f g h i j k l m 243 | - - - - - - - - - - 244 | H 245 | */ 246 | -------------------------------------------------------------------------------- /queue/queue_test.go: -------------------------------------------------------------------------------- 1 | package queue_test 2 | 3 | import ( 4 | "flag" 5 | "math/rand/v2" 6 | "testing" 7 | 8 | "github.com/creachadair/mds/internal/mdtest" 9 | "github.com/creachadair/mds/queue" 10 | ) 11 | 12 | var _ mdtest.Shared[any] = (*queue.Queue[any])(nil) 13 | 14 | func TestQueue(t *testing.T) { 15 | var q queue.Queue[int] 16 | check := func(want ...int) { t.Helper(); mdtest.CheckContents(t, &q, want) } 17 | 18 | // Front and Pop of an empty queue report no value. 19 | if v := q.Front(); v != 0 { 20 | t.Errorf("Front: got %v, want 0", v) 21 | } 22 | if v, ok := q.Pop(); ok { 23 | t.Errorf("Pop: got (%v, %v), want (0, false)", v, ok) 24 | } 25 | 26 | check() 27 | if !q.IsEmpty() { 28 | t.Error("IsEmpty is incorrectly false") 29 | } 30 | if n := q.Len(); n != 0 { 31 | t.Errorf("Len: got %d, want 0", n) 32 | } 33 | 34 | q.Add(1) 35 | if q.IsEmpty() { 36 | t.Error("IsEmpty is incorrectly true") 37 | } 38 | check(1) 39 | 40 | q.Add(2) 41 | check(1, 2) 42 | 43 | q.Add(3) 44 | check(1, 2, 3) 45 | if n := q.Len(); n != 3 { 46 | t.Errorf("Len: got %d, want 3", n) 47 | } 48 | 49 | front := q.Front() 50 | if front != 1 { 51 | t.Errorf("Front: got %v, want 1", front) 52 | } 53 | 54 | // Make sure we can peek all the locations, both positive and negative. 55 | for i, want := range []int{1, 2, 3} { 56 | if v, ok := q.Peek(i); !ok || v != want { 57 | t.Errorf("Peek(%d): got (%v, %v), want (%v, true)", i, v, ok, want) 58 | } 59 | } 60 | for i, want := range []int{3, 2, 1} { 61 | pos := -(i + 1) 62 | if v, ok := q.Peek(pos); !ok || v != want { 63 | t.Errorf("Peek(%d): got (%v, %v), want (%v, true)", pos, v, ok, want) 64 | } 65 | } 66 | 67 | // Peek off the end (in either direction) should return 0, false. 68 | for _, pos := range []int{-10, -4, 5, 9} { 69 | if v, ok := q.Peek(pos); ok || v != 0 { 70 | t.Errorf("Peek(%d): got (%v, %v), want (0, false)", pos, v, ok) 71 | } 72 | } 73 | 74 | // Pop should work in order. 75 | for i, want := range []int{1, 2, 3} { 76 | if v, ok := q.Pop(); !ok || v != want { 77 | t.Errorf("Pop %d: got (%v, %v), want (%v, true)", i+1, v, ok, want) 78 | } 79 | } 80 | check() 81 | 82 | q.Add(2) 83 | q.Add(3) 84 | q.Push(1) 85 | check(1, 2, 3) 86 | 87 | q.Add(4) 88 | check(1, 2, 3, 4) 89 | 90 | q.Push(0) 91 | check(0, 1, 2, 3, 4) 92 | 93 | // PopLast should work in reverse order. 94 | for _, want := range []int{4, 3, 2} { 95 | if v, ok := q.PopLast(); !ok || v != want { 96 | t.Errorf("PopLast: got (%v, %v), want (%v, true)", v, ok, want) 97 | } 98 | } 99 | check(0, 1) 100 | 101 | q.Clear() 102 | check() 103 | 104 | q.Push(25) 105 | check(25) 106 | } 107 | 108 | var doDebug = flag.Bool("debug", false, "Enable debug logging") 109 | 110 | func TestQueueRandom(t *testing.T) { 111 | var q queue.Queue[int] 112 | 113 | debug := func(msg string, args ...any) { 114 | if *doDebug { 115 | t.Logf(msg, args...) 116 | } 117 | } 118 | 119 | // The "has" slice is an "awful" queue that grows indefinitely with use, but 120 | // serves to confirm that the real implementation gets the right order. 121 | var has []int 122 | var stats struct { 123 | MaxLen int 124 | NumAdd int 125 | NumPop int 126 | NumClear int 127 | } 128 | get := func(z int) int { 129 | if z < 0 || z >= len(has) { 130 | return -1 131 | } 132 | return has[z] 133 | } 134 | 135 | // Run a bunch of operations at random on the q, and verify that we get the 136 | // right values out of its methods. 137 | const ( 138 | doAdd = 45 139 | doPop = doAdd + 45 140 | doPeek = doPop + 3 141 | doFront = doPeek + 3 142 | doLen = doFront + 3 143 | doClear = doLen + 1 144 | 145 | doTotal = doClear 146 | ) 147 | 148 | for range 5000 { 149 | if len(has) > stats.MaxLen { 150 | stats.MaxLen = len(has) 151 | } 152 | mdtest.CheckContents(t, &q, has) 153 | switch op := rand.IntN(doTotal); { 154 | case op < doAdd: 155 | stats.NumAdd++ 156 | r := rand.IntN(1000) 157 | has = append(has, r) 158 | debug("Add(%d)", r) 159 | q.Add(r) 160 | case op < doPop: 161 | stats.NumPop++ 162 | debug("Pop exp=%d", get(0)) 163 | got, ok := q.Pop() 164 | if len(has) == 0 { 165 | if ok { 166 | t.Errorf("Pop: got (%v, %v), want (0, false)", got, ok) 167 | } 168 | continue 169 | } 170 | want := has[0] 171 | has = has[1:] 172 | if !ok || got != want { 173 | t.Errorf("Pop: got (%v, %v), want (%v, true)", got, ok, want) 174 | } 175 | case op < doLen: 176 | debug("Len n=%d", len(has)) 177 | if got := q.Len(); got != len(has) { 178 | t.Errorf("Len: got %d, want %d", got, len(has)) 179 | } 180 | case op < doFront: 181 | debug("Front exp=%d", get(0)) 182 | if got := q.Front(); len(has) != 0 && got != has[0] { 183 | t.Errorf("Front: got %d, want %d", got, has[0]) 184 | } 185 | case op < doPeek: 186 | if len(has) != 0 { 187 | r := rand.IntN(len(has)) 188 | debug("Peek(%d) exp=%d", r, has[r]) 189 | if got, ok := q.Peek(r); !ok || got != has[r] { 190 | t.Errorf("Peek(%d): got (%d, %v), want (%d, true)", r, got, ok, has[r]) 191 | } 192 | } 193 | case op < doClear: 194 | stats.NumClear++ 195 | debug("Clear n=%d", len(has)) 196 | has = has[:0] 197 | q.Clear() 198 | default: 199 | panic("unexpected") 200 | } 201 | } 202 | t.Logf("Queue at exit (n=%d): %v", q.Len(), q.Slice()) 203 | t.Logf("Stats: %+v", stats) 204 | } 205 | -------------------------------------------------------------------------------- /ring/example_test.go: -------------------------------------------------------------------------------- 1 | package ring_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/creachadair/mds/ring" 7 | ) 8 | 9 | func ExampleRing() { 10 | r := ring.Of("time", "flies", "like", "an", "arrow") 11 | 12 | // Set the value of an existing element. 13 | r.Value = "fruit" 14 | 15 | // Splice new elements into a ring. 16 | s := r.At(2).Join(ring.Of("a", "banana")) 17 | 18 | // Splice existing elements out of a ring. 19 | s.Prev().Join(r) 20 | 21 | // Iterate over the elements of a ring. 22 | for s := range r.Each { 23 | fmt.Println(s) 24 | } 25 | 26 | // Output: 27 | // fruit 28 | // flies 29 | // like 30 | // a 31 | // banana 32 | } 33 | -------------------------------------------------------------------------------- /ring/ring.go: -------------------------------------------------------------------------------- 1 | // Package ring implements a doubly-linked circular chain of data items, 2 | // called a "ring". 3 | package ring 4 | 5 | import ( 6 | "fmt" 7 | ) 8 | 9 | // A Ring is a doubly-linked circular chain of data items. There is no 10 | // designated beginning or end of a ring; each element is a valid entry point 11 | // for the entire ring. A ring with no elements is represented as nil. 12 | type Ring[T any] struct { 13 | Value T 14 | 15 | prev, next *Ring[T] 16 | } 17 | 18 | func (r *Ring[T]) ptr(p *Ring[T]) string { 19 | if p == nil { 20 | return "-" 21 | } else if p == r { 22 | return "@" 23 | } else { 24 | return "*" 25 | } 26 | } 27 | 28 | func (r *Ring[T]) String() string { 29 | if r == nil { 30 | return "Ring(empty)" 31 | } 32 | return fmt.Sprintf("Ring(%v, %v%v)", r.Value, r.ptr(r.prev), r.ptr(r.next)) 33 | } 34 | 35 | // New constructs a new ring with n zero-valued elements. 36 | // If n ≤ 0, New returns nil. 37 | func New[T any](n int) *Ring[T] { 38 | if n <= 0 { 39 | return nil 40 | } 41 | r := newRing[T]() 42 | for n > 1 { 43 | elt := newRing[T]() 44 | elt.next = r.next 45 | r.next.prev = elt 46 | elt.prev = r 47 | r.next = elt 48 | n-- 49 | } 50 | return r 51 | } 52 | 53 | // Of constructs a new ring containing the given elements. 54 | func Of[T any](vs ...T) *Ring[T] { 55 | r := New[T](len(vs)) 56 | cur := r 57 | for _, v := range vs { 58 | cur.Value = v 59 | cur = cur.Next() 60 | } 61 | return r 62 | } 63 | 64 | // Join splices ring s into a non-empty ring r. There are two cases: 65 | // 66 | // If r and s belong to different rings, [r1 ... rn] and [s1 ... sm], the 67 | // elements of s are spliced in after r1 and the resulting ring is: 68 | // 69 | // [r1 s1 ... sm r2 ... rn] 70 | // ^^^^^^^^^ 71 | // 72 | // In this case Join returns the ring [r2 ... rn r1 ... sm]. 73 | // 74 | // If r and s belong to the same ring, [r1 r2 ... ri s1 ... sm ... rn], then 75 | // the loop of the ring from r2 ... ri is spliced out of r and the resulting 76 | // ring is: 77 | // 78 | // [r1 s1 ... sm ... rn] 79 | // 80 | // In this case Join returns the ring [r2 ... ri] that was spliced out. This 81 | // may be empty (nil) if there were no elements between r1 and s1. 82 | func (r *Ring[T]) Join(s *Ring[T]) *Ring[T] { 83 | if r == s || r.next == s { 84 | return nil // same ring, no elements between r and s to remove 85 | } 86 | rnext, sprev := r.next, s.prev 87 | 88 | r.next = s // successor of r is now s 89 | s.prev = r // predecessor of s is now r 90 | sprev.next = rnext // successor of s end is now rnext 91 | rnext.prev = sprev // predecessor of rnext is now s end 92 | return rnext 93 | } 94 | 95 | // Pop detaches r from its ring, leaving it linked only to itself. 96 | // It returns r to permit method chaining. 97 | func (r *Ring[T]) Pop() *Ring[T] { 98 | if r != nil && r.prev != r { 99 | rprev, rnext := r.prev, r.next 100 | rprev.next = r.next 101 | rnext.prev = r.prev 102 | r.prev = r 103 | r.next = r 104 | } 105 | return r 106 | } 107 | 108 | // Next returns the successor of r (which may be r itself). 109 | // This will panic if r == nil. 110 | func (r *Ring[T]) Next() *Ring[T] { return r.next } 111 | 112 | // Prev returns the predecessor of r (which may be r itself). 113 | // This will panic if r == nil. 114 | func (r *Ring[T]) Prev() *Ring[T] { return r.prev } 115 | 116 | // At returns the entry at offset n from r. Negative values of n are 117 | // permitted, and r.At(0) == r. If r == nil or the absolute value of n is 118 | // greater than the length of the ring, At returns nil. 119 | func (r *Ring[T]) At(n int) *Ring[T] { 120 | if r == nil { 121 | return nil 122 | } 123 | 124 | next := (*Ring[T]).Next 125 | if n < 0 { 126 | n = -n 127 | next = (*Ring[T]).Prev 128 | } 129 | 130 | cur := r 131 | for n > 0 { 132 | cur = next(cur) 133 | if cur == r { 134 | return nil 135 | } 136 | n-- 137 | } 138 | return cur 139 | } 140 | 141 | // Peek reports whether the ring has a value at offset n from r, and if so 142 | // returns its value. Negative values of n are permitted. If the absolute value 143 | // of n is greater than the length of the ring, Peek reports a zero value. 144 | func (r *Ring[T]) Peek(n int) (T, bool) { 145 | cur := r.At(n) 146 | if cur == nil { 147 | var zero T 148 | return zero, false 149 | } 150 | return cur.Value, true 151 | } 152 | 153 | // Each is a range function that calls f with each value of r in circular 154 | // order. If f returns false, Each returns immediately. 155 | func (r *Ring[T]) Each(f func(v T) bool) { 156 | scan(r, func(cur *Ring[T]) bool { return f(cur.Value) }) 157 | } 158 | 159 | // Len reports the number of elements in r. If r == nil, Len is 0. 160 | // This operation takes time proportional to the size of the ring. 161 | func (r *Ring[T]) Len() int { 162 | if r == nil { 163 | return 0 164 | } 165 | var n int 166 | scan(r, func(*Ring[T]) bool { n++; return true }) 167 | return n 168 | } 169 | 170 | // IsEmpty reports whether r is the empty ring. 171 | func (r *Ring[T]) IsEmpty() bool { return r == nil } 172 | 173 | func scan[T any](r *Ring[T], f func(*Ring[T]) bool) { 174 | if r == nil { 175 | return 176 | } 177 | 178 | cur := r 179 | for f(cur) { 180 | if cur.next == r { 181 | return 182 | } 183 | cur = cur.next 184 | } 185 | } 186 | 187 | func newRing[T any]() *Ring[T] { r := new(Ring[T]); r.next = r; r.prev = r; return r } 188 | -------------------------------------------------------------------------------- /ring/ring_test.go: -------------------------------------------------------------------------------- 1 | package ring_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/internal/mdtest" 7 | "github.com/creachadair/mds/ring" 8 | ) 9 | 10 | func rc[T any](t *testing.T, r *ring.Ring[T], want ...T) { 11 | mdtest.CheckContents(t, r, want) 12 | } 13 | 14 | func TestRing(t *testing.T) { 15 | t.Run("Initialize", func(t *testing.T) { 16 | rc[int](t, nil) 17 | rc(t, ring.New[int](0)) 18 | rc(t, ring.Of[int]()) 19 | rc(t, ring.New[int](4), 0, 0, 0, 0) 20 | rc(t, ring.Of(0), 0) 21 | }) 22 | 23 | t.Run("Joining", func(t *testing.T) { 24 | r := ring.Of(0) 25 | r.Join(ring.Of(1, 2, 3)) 26 | rc(t, r, 0, 1, 2, 3) 27 | 28 | // Joining r to itself should do nothing. 29 | r.Join(r) 30 | rc(t, r, 0, 1, 2, 3) 31 | 32 | // Test adding items to various places in the ring. 33 | r.Next().Join(ring.Of(4, 5, 6)) 34 | rc(t, r, 0, 1, 4, 5, 6, 2, 3) 35 | r.Prev().Join(ring.Of(7)) 36 | rc(t, r, 0, 1, 4, 5, 6, 2, 3, 7) 37 | }) 38 | 39 | t.Run("Popping", func(t *testing.T) { 40 | rc(t, ring.Of(1).Pop(), 1) 41 | q := ring.Of(2, 3, 5, 7, 11) 42 | rc(t, q, 2, 3, 5, 7, 11) 43 | q.Next().Pop() 44 | rc(t, q, 2, 5, 7, 11) 45 | q.Prev().Pop() 46 | rc(t, q, 2, 5, 7) 47 | }) 48 | 49 | t.Run("Circularity", func(t *testing.T) { 50 | r := ring.Of(1, 3, 5, 7, 9) 51 | rc(t, r, 1, 3, 5, 7, 9) 52 | rc(t, r.Next().Next(), 5, 7, 9, 1, 3) 53 | rc(t, r.Prev(), 9, 1, 3, 5, 7) 54 | r.Next().Join(r.Prev()) 55 | rc(t, r, 1, 3, 9) 56 | }) 57 | 58 | t.Run("SplicingIn", func(t *testing.T) { 59 | s := ring.Of(10, 20, 30) 60 | rc(t, s, 10, 20, 30) 61 | r := ring.Of(1, 2, 3, 4, 5, 6) 62 | 63 | x := r.Next().Join(s) 64 | rc(t, r, 1, 2, 10, 20, 30, 3, 4, 5, 6) 65 | // ^- r ^- s ^- x 66 | rc(t, s, 10, 20, 30, 3, 4, 5, 6, 1, 2) 67 | // ^- s ^- x ^- r 68 | rc(t, x, 3, 4, 5, 6, 1, 2, 10, 20, 30) 69 | // ^- x ^- r ^- s 70 | }) 71 | 72 | t.Run("SplicingOut", func(t *testing.T) { 73 | r := ring.Of(1, 20, 30, 40, 5, 6) 74 | tail := r.At(4) 75 | rc(t, r.Join(tail), 20, 30, 40) // just the excised part 76 | rc(t, r, 1, 5, 6) 77 | 78 | rc(t, r.Join(r.Next())) // nothing was removed 79 | rc(t, r, 1, 5, 6) 80 | 81 | q := ring.Of("fat", "cats", "get", "dizzy", "after", "eating", "beans") 82 | s := q.At(2).Join(q.Prev()) 83 | 84 | rc(t, q, "fat", "cats", "get", "beans") 85 | rc(t, s, "dizzy", "after", "eating") 86 | }) 87 | 88 | t.Run("Peek", func(t *testing.T) { 89 | r := ring.Of("kingdom", "phylum", "class", "order", "family", "genus", "species") 90 | checkPeek := func(n int, want string, wantok bool) { 91 | t.Helper() 92 | got, ok := r.Peek(n) 93 | if got != want || ok != wantok { 94 | t.Errorf("Peek(%d): got (%v, %v), want (%v, %v)", n, got, ok, want, wantok) 95 | } 96 | } 97 | 98 | checkPeek(0, "kingdom", true) 99 | checkPeek(-2, "genus", true) 100 | checkPeek(3, "order", true) 101 | checkPeek(6, "species", true) 102 | checkPeek(7, "", false) 103 | checkPeek(-10, "", false) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /shell/bench_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Michael J. Fromberger 2 | 3 | package shell_test 4 | 5 | import ( 6 | "bytes" 7 | "fmt" 8 | "math" 9 | "math/rand" 10 | "strings" 11 | "testing" 12 | 13 | "github.com/creachadair/mds/shell" 14 | ) 15 | 16 | var input string 17 | 18 | // Generate a long random string with balanced quotations for perf testing. 19 | func init() { 20 | var buf bytes.Buffer 21 | 22 | src := rand.NewSource(12345) 23 | r := rand.New(src) 24 | 25 | const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789 \t\n\n\n" 26 | pick := func(f float64) byte { 27 | pos := math.Ceil(f*float64(len(alphabet))) - 1 28 | return alphabet[int(pos)] 29 | } 30 | 31 | const inputLen = 100000 32 | var quote struct { 33 | q byte 34 | n int 35 | } 36 | for range inputLen { 37 | if quote.n == 0 { 38 | q := r.Float64() 39 | if q < .1 { 40 | quote.q = '"' 41 | quote.n = r.Intn(256) 42 | buf.WriteByte('"') 43 | continue 44 | } else if q < .15 { 45 | quote.q = '\'' 46 | quote.n = r.Intn(256) 47 | buf.WriteByte('\'') 48 | continue 49 | } 50 | } 51 | buf.WriteByte(pick(r.Float64())) 52 | if quote.n > 0 { 53 | quote.n-- 54 | if quote.n == 0 { 55 | buf.WriteByte(quote.q) 56 | } 57 | } 58 | } 59 | input = buf.String() 60 | } 61 | 62 | func BenchmarkSplit(b *testing.B) { 63 | var lens []int 64 | for i := 1; i < len(input); i *= 4 { 65 | lens = append(lens, i) 66 | } 67 | lens = append(lens, len(input)) 68 | 69 | s := shell.NewScanner(nil) 70 | b.ResetTimer() 71 | for _, n := range lens { 72 | b.Run(fmt.Sprintf("len_%d", n), func(b *testing.B) { 73 | for range b.N { 74 | s.Reset(strings.NewReader(input[:n])) 75 | s.Each(ignore) 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func BenchmarkQuote(b *testing.B) { 82 | const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789 \t\n\n\n" 83 | src := rand.NewSource(67890) 84 | r := rand.New(src) 85 | 86 | var buf bytes.Buffer 87 | for range 100000 { 88 | switch v := r.Float64(); { 89 | case v < 0.5: 90 | buf.WriteByte('\'') 91 | case v < 0.1: 92 | buf.WriteByte('"') 93 | case v < 0.15: 94 | buf.WriteByte('\\') 95 | default: 96 | pos := math.Ceil(r.Float64()*float64(len(alphabet))) - 1 97 | buf.WriteByte(alphabet[int(pos)]) 98 | } 99 | } 100 | 101 | input := buf.String() 102 | parts, _ := shell.Split(input) 103 | b.Logf("Input length: %d bytes, %d tokens", len(input), len(parts)) 104 | 105 | b.Run("Quote", func(b *testing.B) { 106 | for range b.N { 107 | shell.Quote(input) 108 | } 109 | }) 110 | b.Run("Join", func(b *testing.B) { 111 | for range b.N { 112 | shell.Join(parts) 113 | } 114 | }) 115 | } 116 | 117 | func ignore(string) bool { return true } 118 | -------------------------------------------------------------------------------- /shell/internal_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Michael J. Fromberger 2 | 3 | package shell 4 | 5 | import ( 6 | "fmt" 7 | "testing" 8 | ) 9 | 10 | func TestQuote(t *testing.T) { 11 | type testCase struct{ in, want string } 12 | tests := []testCase{ 13 | {"", "''"}, // empty is special 14 | {"abc", "abc"}, // nothing to quote 15 | {"--flag", "--flag"}, // " 16 | {"'abc", `\'abc`}, // single quote only 17 | {"abc'", `abc\'`}, // " 18 | {`shan't`, `shan\'t`}, // " 19 | {"--flag=value", `'--flag=value'`}, 20 | {"a b\tc", "'a b\tc'"}, 21 | {`a"b"c`, `'a"b"c'`}, 22 | {`'''`, `\'\'\'`}, 23 | {`\`, `'\'`}, 24 | {`'a=b`, `\''a=b'`}, // quotes and other stuff 25 | {`a='b`, `'a='\''b'`}, // " 26 | {`a=b'`, `'a=b'\'`}, // " 27 | } 28 | // Verify that all the designated special characters get quoted. 29 | for _, c := range shouldQuote + mustQuote { 30 | tests = append(tests, testCase{ 31 | in: string(c), 32 | want: fmt.Sprintf(`'%c'`, c), 33 | }) 34 | } 35 | 36 | for _, test := range tests { 37 | got := Quote(test.in) 38 | if got != test.want { 39 | t.Errorf("Quote %q: got %q, want %q", test.in, got, test.want) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /shell/shell_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2015, Michael J. Fromberger 2 | 3 | package shell_test 4 | 5 | import ( 6 | "fmt" 7 | "io" 8 | "log" 9 | "strings" 10 | "testing" 11 | 12 | "github.com/creachadair/mds/shell" 13 | "github.com/google/go-cmp/cmp" 14 | ) 15 | 16 | func TestSplit(t *testing.T) { 17 | tests := []struct { 18 | in string 19 | want []string 20 | ok bool 21 | }{ 22 | // Variations of empty input yield an empty split. 23 | {"", nil, true}, 24 | {" ", nil, true}, 25 | {"\t", nil, true}, 26 | {"\n ", nil, true}, 27 | 28 | // Various escape sequences work properly. 29 | {`\ `, []string{" "}, true}, 30 | {`a\ `, []string{"a "}, true}, 31 | {`\\a`, []string{`\a`}, true}, 32 | {`"a\"b"`, []string{`a"b`}, true}, 33 | {`'\'`, []string{"\\"}, true}, 34 | 35 | // Leading and trailing whitespace are discarded correctly. 36 | {"a", []string{"a"}, true}, 37 | {" a", []string{"a"}, true}, 38 | {"a\n", []string{"a"}, true}, 39 | 40 | // Escaped newlines are magic in the correct ways. 41 | {"a\\\nb", []string{"ab"}, true}, 42 | {"a \\\n b\tc", []string{"a", "b", "c"}, true}, 43 | 44 | // Various splits with and without quotes. Quoted whitespace is 45 | // preserved. 46 | {"a b c", []string{"a", "b", "c"}, true}, 47 | {`a 'b c'`, []string{"a", "b c"}, true}, 48 | {"\"a\nb\"cd e'f'", []string{"a\nbcd", "ef"}, true}, 49 | {"'\n \t '", []string{"\n \t "}, true}, 50 | 51 | // Quoted empty strings are preserved in various places. 52 | {"''", []string{""}, true}, 53 | {"a ''", []string{"a", ""}, true}, 54 | {" a \"\" b ", []string{"a", "", "b"}, true}, 55 | {"'' a", []string{"", "a"}, true}, 56 | 57 | // Unbalanced quotation marks and escapes are detected. 58 | {"\\", []string{""}, false}, // escape without a target 59 | {"'", []string{""}, false}, // unclosed single 60 | {`"`, []string{""}, false}, // unclosed double 61 | {`'\''`, []string{`\`}, false}, // unclosed connected double 62 | {`"\\" '`, []string{`\`, ``}, false}, // unclosed separate single 63 | {"a 'b c", []string{"a", "b c"}, false}, 64 | {`a "b c`, []string{"a", "b c"}, false}, 65 | {`a "b \"`, []string{"a", `b "`}, false}, 66 | } 67 | for _, test := range tests { 68 | got, ok := shell.Split(test.in) 69 | if ok != test.ok { 70 | t.Errorf("Split %#q: got valid=%v, want %v", test.in, ok, test.ok) 71 | } 72 | if diff := cmp.Diff(test.want, got); diff != "" { 73 | t.Errorf("Split %#q: (-want, +got)\n%s", test.in, diff) 74 | } 75 | } 76 | } 77 | 78 | func TestScannerSplit(t *testing.T) { 79 | tests := []struct { 80 | in string 81 | want, rest []string 82 | }{ 83 | {"", nil, nil}, 84 | {" ", nil, nil}, 85 | {"--", nil, nil}, 86 | {"a -- b", []string{"a"}, []string{"b"}}, 87 | {"a b c -- d -- e ", []string{"a", "b", "c"}, []string{"d", "--", "e"}}, 88 | {`"a b c --" -- "d "`, []string{"a b c --"}, []string{"d "}}, 89 | {` -- "foo`, nil, []string{"foo"}}, // unterminated 90 | {"cmd -flag -- arg1 arg2", []string{"cmd", "-flag"}, []string{"arg1", "arg2"}}, 91 | } 92 | for _, test := range tests { 93 | t.Logf("Scanner split input: %q", test.in) 94 | 95 | s := shell.NewScanner(strings.NewReader(test.in)) 96 | var got, rest []string 97 | for s.Next() { 98 | if s.Text() == "--" { 99 | rest = s.Split() 100 | break 101 | } 102 | got = append(got, s.Text()) 103 | } 104 | 105 | if s.Err() != io.EOF { 106 | t.Errorf("Unexpected scan error: %v", s.Err()) 107 | } 108 | 109 | if diff := cmp.Diff(test.want, got); diff != "" { 110 | t.Errorf("Scanner split prefix: (-want, +got)\n%s", diff) 111 | } 112 | if diff := cmp.Diff(test.rest, rest); diff != "" { 113 | t.Errorf("Scanner split suffix: (-want, +got)\n%s", diff) 114 | } 115 | } 116 | } 117 | 118 | func TestRoundTrip(t *testing.T) { 119 | tests := [][]string{ 120 | nil, 121 | {"a"}, 122 | {"a "}, 123 | {"a", "b", "c"}, 124 | {"a", "b c"}, 125 | {"--flag=value"}, 126 | {"m='$USER'", "nop+", "$$"}, 127 | {`"a" b `, "c"}, 128 | {"odd's", "bodkins", "x'", "x''", "x\"\"", "$x':y"}, 129 | {"a=b", "--foo", "${bar}", `\$`}, 130 | {"cat", "a${b}.txt", "|", "tee", "capture", "2>", "/dev/null"}, 131 | } 132 | for _, test := range tests { 133 | s := shell.Join(test) 134 | t.Logf("Join %#q = %v", test, s) 135 | got, ok := shell.Split(s) 136 | if !ok { 137 | t.Errorf("Split %+q: should be valid, but is not", s) 138 | } 139 | if diff := cmp.Diff(test, got); diff != "" { 140 | t.Errorf("Split %+q: (-want, +got)\n%s", s, diff) 141 | } 142 | } 143 | } 144 | 145 | func ExampleScanner() { 146 | const input = `a "free range" exploration of soi\ disant novelties` 147 | s := shell.NewScanner(strings.NewReader(input)) 148 | sum, count := 0, 0 149 | for tok := range s.Each { 150 | count++ 151 | sum += len(tok) 152 | } 153 | fmt.Println(len(input), count, sum, s.Complete(), s.Err()) 154 | // Output: 51 6 43 true EOF 155 | } 156 | 157 | func ExampleScanner_Rest() { 158 | const input = `things 'and stuff' %end% all the remaining stuff` 159 | s := shell.NewScanner(strings.NewReader(input)) 160 | for tok := range s.Each { 161 | if tok == "%end%" { 162 | fmt.Print("found marker; ") 163 | break 164 | } 165 | } 166 | rest, err := io.ReadAll(s.Rest()) 167 | if err != nil { 168 | log.Fatal(err) 169 | } 170 | fmt.Println(string(rest)) 171 | // Output: found marker; all the remaining stuff 172 | } 173 | 174 | func ExampleScanner_Each() { 175 | const input = `a\ b 'c d' "e f's g" stop "go directly to jail"` 176 | s := shell.NewScanner(strings.NewReader(input)) 177 | for tok := range s.Each { 178 | fmt.Println(tok) 179 | if tok == "stop" { 180 | break 181 | } 182 | } 183 | if err := s.Err(); err != nil { 184 | log.Fatal(err) 185 | } 186 | // Output: 187 | // a b 188 | // c d 189 | // e f's g 190 | // stop 191 | } 192 | 193 | func ExampleScanner_Split() { 194 | const input = `cmd -flag=t -- foo bar baz` 195 | 196 | s := shell.NewScanner(strings.NewReader(input)) 197 | for s.Next() { 198 | if s.Text() == "--" { 199 | fmt.Println("** Args:", strings.Join(s.Split(), ", ")) 200 | } else { 201 | fmt.Println(s.Text()) 202 | } 203 | } 204 | // Output: 205 | // cmd 206 | // -flag=t 207 | // ** Args: foo, bar, baz 208 | } 209 | -------------------------------------------------------------------------------- /slice/bench_test.go: -------------------------------------------------------------------------------- 1 | package slice_test 2 | 3 | import ( 4 | "cmp" 5 | "flag" 6 | "fmt" 7 | "math/rand/v2" 8 | "testing" 9 | 10 | "github.com/creachadair/mds/slice" 11 | ) 12 | 13 | var ( 14 | lhsSize = flag.Int("lhs", 500, "LHS input size (number of elements)") 15 | rhsSize = flag.Int("rhs", 500, "RHS input size (number of elements)") 16 | 17 | lisSize = flag.Int("lis", 0, "LIS input size (number of elements)") 18 | // lisCountCmps is optional because it requires inserting some 19 | // accounting in the algorithm's inner loop, so while you get a 20 | // number that's independent of the nonlinear log(n) time factor, 21 | // the uncompensated numbers get thrown off. 22 | lisCountCmps = flag.Bool("lis-count-cmp", false, "report ns/compare stats") 23 | ) 24 | 25 | func BenchmarkEdit(b *testing.B) { 26 | lhs := make([]int, *lhsSize) 27 | for i := range lhs { 28 | lhs[i] = rand.IntN(1000000) 29 | } 30 | rhs := make([]int, *rhsSize) 31 | for i := range rhs { 32 | rhs[i] = rand.IntN(10000000) 33 | } 34 | 35 | b.Run("LCS", func(b *testing.B) { 36 | b.ReportAllocs() 37 | for range b.N { 38 | _ = slice.LCS(lhs, rhs) 39 | } 40 | }) 41 | b.Run("EditScript", func(b *testing.B) { 42 | b.ReportAllocs() 43 | for range b.N { 44 | _ = slice.EditScript(lhs, rhs) 45 | } 46 | }) 47 | } 48 | 49 | func BenchmarkLNDSFunc(b *testing.B) { 50 | buckets := []int{100, 1000, 20000} 51 | if *lisSize != 0 { 52 | buckets = []int{*lisSize} 53 | } 54 | 55 | for _, bucket := range buckets { 56 | b.Run(fmt.Sprint("items=", bucket), func(b *testing.B) { 57 | input := randomInts(bucket) 58 | 59 | var comparisons uint64 60 | cmpFn := cmp.Compare[int] 61 | if *lisCountCmps { 62 | cmpFn = func(a, b int) int { 63 | comparisons++ 64 | return cmp.Compare(a, b) 65 | } 66 | } 67 | 68 | b.ReportAllocs() 69 | for range b.N { 70 | _ = slice.LNDSFunc(input, cmpFn) 71 | } 72 | 73 | if *lisCountCmps { 74 | perCmp := float64(b.Elapsed().Nanoseconds()) / float64(comparisons) 75 | b.ReportMetric(perCmp, "ns/cmp") 76 | } 77 | }) 78 | } 79 | } 80 | 81 | func BenchmarkLISFunc(b *testing.B) { 82 | buckets := []int{100, 1000, 20000} 83 | if *lisSize != 0 { 84 | buckets = []int{*lisSize} 85 | } 86 | 87 | for _, bucket := range buckets { 88 | b.Run(fmt.Sprint("items=", bucket), func(b *testing.B) { 89 | input := randomInts(bucket) 90 | 91 | var comparisons uint64 92 | cmpFn := cmp.Compare[int] 93 | if *lisCountCmps { 94 | cmpFn = func(a, b int) int { 95 | comparisons++ 96 | return cmp.Compare(a, b) 97 | } 98 | } 99 | 100 | b.ReportAllocs() 101 | for range b.N { 102 | _ = slice.LISFunc(input, cmpFn) 103 | } 104 | 105 | if *lisCountCmps { 106 | perCmp := float64(b.Elapsed().Nanoseconds()) / float64(comparisons) 107 | b.ReportMetric(perCmp, "ns/cmp") 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /slice/edit.go: -------------------------------------------------------------------------------- 1 | package slice 2 | 3 | import ( 4 | "fmt" 5 | "slices" 6 | ) 7 | 8 | // LCS computes a longest common subsequence of as and bs. 9 | // 10 | // This implementation takes Θ(mn) time and O(P·min(m, n)) space for inputs of 11 | // length m = len(as) and n = len(bs) and longest subsequence length P. 12 | func LCS[T comparable, Slice ~[]T](as, bs Slice) Slice { return LCSFunc(as, bs, equal) } 13 | 14 | // LCSFunc computes a longest common subsequence of as and bs, using 15 | // eq to compare elements. 16 | // 17 | // This implementation takes Θ(mn) time and O(P·min(m, n)) space for inputs of 18 | // length m = len(as) and n = len(bs) and longest subsequence length P. 19 | func LCSFunc[T any, Slice ~[]T](as, bs Slice, eq func(a, b T) bool) Slice { 20 | if len(as) == 0 || len(bs) == 0 { 21 | return nil 22 | } 23 | 24 | // We maintain two rows of the optimization matrix, (p)revious and 25 | // (c)urrent. Rows are positions in bs and columns are positions in as. 26 | // The rows are extended by one position to get rid of the special case at 27 | // the beginning of the sequence (we use 1..n instead of 0..n-1). 28 | 29 | // Size the buffers based on the smaller input, since order does not matter. 30 | // This lets us use less storage with no time penalty. 31 | if len(bs) < len(as) { 32 | as, bs = bs, as 33 | } 34 | 35 | type seq struct { 36 | i int // offset into as 37 | n int // length of path 38 | prev *seq // previous element 39 | } 40 | p := make([]*seq, len(as)+1) 41 | c := make([]*seq, len(as)+1) 42 | 43 | var zero seq // sentinel; not written 44 | for i := range p { 45 | p[i] = &zero 46 | c[i] = &zero 47 | } 48 | 49 | // Fill the rows top to bottom, left to right, since the optimization 50 | // recurrence needs the previous element in the same row, and the same and 51 | // previous elements in the previous row. 52 | for j := 1; j <= len(bs); j++ { 53 | p, c = c, p // swap the double buffer 54 | 55 | // Fill the current row. 56 | for i := 1; i <= len(as); i++ { 57 | if eq(as[i-1], bs[j-1]) { 58 | c[i] = &seq{i - 1, p[i-1].n + 1, p[i-1]} 59 | } else if c[i-1].n >= p[i].n { 60 | c[i] = c[i-1] 61 | } else { 62 | c[i] = p[i] 63 | } 64 | } 65 | } 66 | out := make(Slice, 0, c[len(as)].n) 67 | for p := c[len(as)]; p.n > 0; p = p.prev { 68 | out = append(out, as[p.i]) 69 | } 70 | slices.Reverse(out) 71 | return out 72 | } 73 | 74 | // EditOp is the opcode of an edit sequence instruction. 75 | type EditOp byte 76 | 77 | const ( 78 | OpDrop EditOp = '-' // Drop items from lhs 79 | OpEmit EditOp = '=' // Emit elements from lhs 80 | OpCopy EditOp = '+' // Copy items from rhs 81 | OpReplace EditOp = '!' // Replace with items from rhs (== Drop+Copy) 82 | ) 83 | 84 | // Edit is an edit operation transforming specified as part of a diff. 85 | // Each edit refers to a specific span of one of the inputs. 86 | type Edit[T any] struct { 87 | Op EditOp // the diff operation to apply at the current offset 88 | 89 | // X specifies the elements of lhs affected by the edit. 90 | // For OpDrop and OpReplace it is the elements to be dropped. 91 | // For OpEmit its the elements to be emitted. 92 | // For OpCopy it is empty. 93 | X []T 94 | 95 | // Y specifies the elements of rhs affected by the edit. 96 | // For OpDrop and OpEmit it is empty. 97 | // For OpCopy and OpReplace it is the elements to be copied. 98 | Y []T 99 | } 100 | 101 | func (e Edit[T]) String() string { 102 | switch e.Op { 103 | case OpCopy: 104 | return fmt.Sprintf("%c%v", e.Op, e.Y) 105 | case OpReplace: 106 | x, y := fmt.Sprint(e.X), fmt.Sprint(e.Y) 107 | return fmt.Sprintf("%c[%s:%s]", e.Op, x[1:len(x)-1], y[1:len(y)-1]) 108 | case OpDrop, OpEmit: 109 | return fmt.Sprintf("%c%v", e.Op, e.X) 110 | } 111 | return fmt.Sprintf("!%c[INVALID]", e.Op) 112 | } 113 | 114 | // EditScript computes a minimal-length sequence of Edit operations that will 115 | // transform lhs into rhs. The result is empty if lhs == rhs. The slices stored 116 | // in returned edit operations share storage with the inputs lhs and rhs. 117 | // 118 | // This implementation takes Θ(mn) time and O(P·min(m, n)) space to compute a 119 | // longest common subsequence, plus overhead of O(m+n) time and space to 120 | // construct the edit sequence from the LCS. 121 | // 122 | // An edit sequence is processed in order. Items are sent to the output 123 | // according to the following rules. 124 | // 125 | // For each element e of the edit script, if e.Op is: 126 | // 127 | // - OpDrop: No output; e.X records the items discarded. 128 | // 129 | // - OpEmit: Emit the elements in e.X from lhs. 130 | // 131 | // - OpCopy: Emit the elements in e.Y from rhs. 132 | // 133 | // - OpReplace: Emit the elements in e.Y from rhs. The items in e.X are the 134 | // elements from lhs that were replaced. (== Drop + Copy) 135 | // 136 | // If the edit script is empty, the output is equal to the input. 137 | func EditScript[T comparable, Slice ~[]T](lhs, rhs Slice) []Edit[T] { 138 | return editScriptFunc(equal, lhs, rhs) 139 | } 140 | 141 | // editScriptFunc computes an edit script using eq as an equality comparison. 142 | func editScriptFunc[T any, Slice ~[]T](eq func(a, b T) bool, lhs, rhs Slice) []Edit[T] { 143 | lcs := LCSFunc(lhs, rhs, eq) 144 | 145 | // To construct the edit sequence, i scans forward through lcs. 146 | // For each i, we find the unclaimed elements of lhs and rhs prior to the 147 | // occurrence of lcs[i]. 148 | // 149 | // Elements of lhs before lcs[i] must be removed from the result. 150 | // Elements of rhs before lcs[i] must be added to the result. 151 | // Elements equal to lcs members are preserved as-written. 152 | // 153 | // However, whenever we have deletes followed immediately by inserts, the 154 | // net effect is to "replace" some or all of the deleted items with the 155 | // inserted ones. We represent this case explicitly with a replace edit. 156 | lpos, rpos, i := 0, 0, 0 157 | 158 | var out []Edit[T] 159 | for i < len(lcs) { 160 | // Count the numbers of elements of lhs and rhs prior to the next match. 161 | lend := lpos 162 | for !eq(lhs[lend], lcs[i]) { 163 | lend++ 164 | } 165 | rend := rpos 166 | for !eq(rhs[rend], lcs[i]) { 167 | rend++ 168 | } 169 | 170 | // If we have both deletions and copies, combine them in a single replace 171 | // instruction. 172 | if lend > lpos && rend > rpos { 173 | out = append(out, Edit[T]{Op: OpReplace, X: lhs[lpos:lend], Y: rhs[rpos:rend]}) 174 | rpos = rend 175 | } else if lend > lpos { 176 | // Record drops (there may be none). 177 | out = append(out, Edit[T]{Op: OpDrop, X: lhs[lpos:lend]}) 178 | } 179 | // Record copies (there may be none). 180 | if rend > rpos { 181 | out = append(out, Edit[T]{Op: OpCopy, Y: rhs[rpos:rend]}) 182 | } 183 | 184 | lpos, rpos = lend, rend 185 | 186 | // Reaching here, lhs[lpos] == rhs[rpos] == lcs[i]. 187 | // Count how many elements are equal and copy them. 188 | m := 1 189 | for i+m < len(lcs) && eq(lhs[lpos+m], rhs[rpos+m]) { 190 | m++ 191 | } 192 | out = append(out, Edit[T]{Op: OpEmit, X: lhs[lpos : lpos+m]}) 193 | i += m 194 | lpos += m 195 | rpos += m 196 | } 197 | 198 | // If we have both deletions and copies, combine them in a single replace 199 | // instruction. 200 | if len(lhs) > lpos && len(rhs) > rpos { 201 | out = append(out, Edit[T]{Op: OpReplace, X: lhs[lpos:], Y: rhs[rpos:]}) 202 | rpos = len(rhs) 203 | } else if len(lhs) > lpos { 204 | // Drop any leftover elements of lhs. 205 | out = append(out, Edit[T]{Op: OpDrop, X: lhs[lpos:]}) 206 | } 207 | // Copy any leftover elements of rhs. 208 | if len(rhs) > rpos { 209 | out = append(out, Edit[T]{Op: OpCopy, Y: rhs[rpos:]}) 210 | } 211 | 212 | // As a special case, if the whole edit is a single emit, drop it so that 213 | // equal elements have an empty script. 214 | if len(out) == 1 && out[0].Op == OpEmit { 215 | return nil 216 | } 217 | return out 218 | } 219 | 220 | func equal[T comparable](a, b T) bool { return a == b } 221 | -------------------------------------------------------------------------------- /slice/edit_test.go: -------------------------------------------------------------------------------- 1 | package slice_test 2 | 3 | import ( 4 | "math/rand/v2" 5 | "regexp" 6 | "slices" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/creachadair/mds/slice" 11 | 12 | _ "embed" 13 | ) 14 | 15 | func TestLCS(t *testing.T) { 16 | tests := []struct { 17 | a, b string 18 | want string 19 | }{ 20 | {"", "", ""}, 21 | 22 | {"a", "", ""}, 23 | {"", "b", ""}, 24 | 25 | {"a b c", "", ""}, 26 | {"", "d e f", ""}, 27 | 28 | {"a", "a b c", "a"}, 29 | {"b", "a b c", "b"}, 30 | {"c", "a b c", "c"}, 31 | {"d", "a b c", ""}, 32 | 33 | {"a b c", "a b c", "a b c"}, 34 | {"a b c", "a b", "a b"}, 35 | {"b c", "a b c", "b c"}, 36 | {"a b c d e", "e b c d a", "b c d"}, 37 | {"x y z", "p d q a b", ""}, 38 | {"b a r a t a", "a b a t e", "a a t"}, 39 | 40 | {"you will be lucky to get this to work at all", 41 | "will we be so lucky as to get this to work in the end", 42 | "will be lucky to get this to work"}, 43 | 44 | {"a foolish consistency is the hobgoblin of little minds", 45 | "four foolish fat hens ate the hobgoblin who is little and minds not", 46 | "foolish the hobgoblin little minds"}, 47 | } 48 | for _, tc := range tests { 49 | as, bs := strings.Fields(tc.a), strings.Fields(tc.b) 50 | want := strings.Fields(tc.want) 51 | got := slice.LCS(as, bs) 52 | if !slices.Equal(got, want) { 53 | t.Errorf("LCS(%s, %s):\ngot: %v\nwant: %v", tc.a, tc.b, got, want) 54 | } 55 | } 56 | } 57 | 58 | func TestLCSRandom(t *testing.T) { 59 | // Append n randomly generated letters from alpha to *ss. 60 | pad := func(ss *[]string, n int, alpha string) { 61 | for range n { 62 | j := rand.IntN(len(alpha)) 63 | *ss = append(*ss, alpha[j:j+1]) 64 | } 65 | } 66 | 67 | // Append 0-4 randomly generated letters from alpha before and after each 68 | // word in want, and return the resulting sequence. 69 | input := func(want []string, alpha string) []string { 70 | var out []string 71 | for _, w := range want { 72 | pad(&out, rand.IntN(4), alpha) 73 | out = append(out, w) 74 | } 75 | pad(&out, rand.IntN(4), alpha) 76 | return out 77 | } 78 | 79 | // Generate a longest common subsequence of length i, and inputs constructed 80 | // to have that as their LCS, and verify that they do. 81 | for i := 0; i < 200; i += 20 { 82 | var want []string 83 | pad(&want, i, "abcdefghijklmonpqrstuvwxyz") 84 | 85 | // N.B. The alphabets used by the probe string must not overlap with the 86 | // inputs, nor the inputs with each other. 87 | // 88 | // Probe string: lower-case 89 | // LHS: digits 90 | // RHS: upper-case 91 | 92 | lhs := input(want, "0123456789") 93 | rhs := input(want, "ABCDEFGHIJKLMNOPQRSTUVWXYZ") 94 | got := slice.LCS(lhs, rhs) 95 | if !slices.Equal(got, want) { 96 | t.Errorf("LCS(%q, %q):\ngot: %q\nwant: %q", lhs, rhs, got, want) 97 | } 98 | } 99 | } 100 | 101 | func TestEditScript(t *testing.T) { 102 | tests := []struct { 103 | a, b string 104 | want []slice.Edit[string] 105 | }{ 106 | {"", "", nil}, 107 | 108 | {"a", "", pedit(t, "-[a]")}, 109 | {"", "b", pedit(t, "+[b]")}, 110 | 111 | {"a b c", "", pedit(t, "-[a b c]")}, 112 | {"", "d e f", pedit(t, "+[d e f]")}, 113 | 114 | {"a", "a b c", pedit(t, "=[a] +[b c]")}, 115 | {"b", "a b c", pedit(t, "+[a] =[b] +[c]")}, 116 | {"c", "a b c", pedit(t, "+[a b] =[c]")}, 117 | {"d", "a b c", pedit(t, "![d:a b c]")}, 118 | 119 | {"c d", "a b c d", pedit(t, "+[a b] =[c d]")}, 120 | {"a b c", "a b c", nil}, 121 | {"a b c", "a x c", pedit(t, "=[a] ![b:x] =[c]")}, 122 | {"a b c", "a b", pedit(t, "=[a b] -[c]")}, 123 | {"b c", "a b c", pedit(t, "+[a] =[b c]")}, 124 | {"a b c d e", "e b c d a", pedit(t, "![a:e] =[b c d] ![e:a]")}, 125 | {"1 2 3 4", "4 3 2 1", pedit(t, "+[4 3 2] =[1] -[2 3 4]")}, 126 | {"a b c 4", "1 2 4", pedit(t, "![a b c:1 2] =[4]")}, 127 | {"a b 3 4", "0 1 2 3 4", pedit(t, "![a b:0 1 2] =[3 4]")}, 128 | {"1 2 3 4", "1 2 3 5 6", pedit(t, "=[1 2 3] ![4:5 6]")}, 129 | {"1 2 3 4", "1 2 q", pedit(t, "=[1 2] ![3 4:q]")}, 130 | 131 | {"a x b x c", "1 x b x 2", pedit(t, "![a:1] =[x b x] ![c:2]")}, 132 | {"fly you fools", "to fly you must not be fools", 133 | pedit(t, "+[to] =[fly you] +[must not be] =[fools]")}, 134 | {"have the best time it is possible to have under the circumstances", 135 | "I hope you have the time of your life in the forest", 136 | pedit(t, "+[I hope you] =[have the] -[best] =[time] "+ 137 | "![it is possible to have under:of your life in] "+ 138 | "=[the] ![circumstances:forest]")}, 139 | } 140 | for _, tc := range tests { 141 | as, bs := strings.Fields(tc.a), strings.Fields(tc.b) 142 | got := slice.EditScript(as, bs) 143 | if !equalEdits(got, tc.want) { 144 | t.Errorf("EditScript(%q, %q):\ngot: %v\nwant: %v", tc.a, tc.b, got, tc.want) 145 | } 146 | checkApply(t, as, bs, got) 147 | } 148 | } 149 | 150 | func equalEdits[T comparable](a, b []slice.Edit[T]) bool { 151 | if len(a) != len(b) { 152 | return false 153 | } 154 | for i := range len(a) { 155 | if a[i].Op != b[i].Op || 156 | !slices.Equal(a[i].X, b[i].X) || 157 | !slices.Equal(a[i].Y, b[i].Y) { 158 | return false 159 | } 160 | } 161 | return true 162 | } 163 | 164 | // checkApply verifies that applying the specified edit script to lhs produces rhs. 165 | func checkApply[T comparable, Slice ~[]T](t *testing.T, lhs, rhs Slice, edit []slice.Edit[T]) { 166 | t.Helper() 167 | 168 | var out Slice 169 | for _, e := range edit { 170 | switch e.Op { 171 | case slice.OpDrop: 172 | // nothing to do 173 | case slice.OpCopy, slice.OpReplace: 174 | out = append(out, e.Y...) 175 | case slice.OpEmit: 176 | out = append(out, e.X...) 177 | default: 178 | t.Fatalf("Unexpected edit operation: %v", e) 179 | } 180 | } 181 | if len(edit) == 0 { 182 | out = rhs 183 | } 184 | if !slices.Equal(out, rhs) { 185 | t.Errorf("Apply %v:\ngot: %v\nwant: %v", edit, out, rhs) 186 | } else { 187 | t.Logf("Apply L %v E %v OK: %v", lhs, edit, out) 188 | } 189 | } 190 | 191 | var argsRE = regexp.MustCompile(`([-+=!])\[([^\]]*)\](?:\s|$)`) 192 | 193 | // pedit parses a string of space-separated edit strings matching the string 194 | // format rendered by the String method of a slice.Edit. 195 | func pedit(t *testing.T, ss string) (out []slice.Edit[string]) { 196 | t.Helper() 197 | ms := argsRE.FindAllStringSubmatch(ss, -1) 198 | if ms == nil { 199 | t.Fatalf("Invalid argument %q", ss) 200 | } 201 | for _, m := range ms { 202 | fs := strings.Fields(m[2]) 203 | var next slice.Edit[string] 204 | switch m[1] { 205 | case "+": 206 | next.Op = slice.OpCopy 207 | next.Y = fs 208 | case "-": 209 | next.Op = slice.OpDrop 210 | next.X = fs 211 | case "=": 212 | next.Op = slice.OpEmit 213 | next.X = fs 214 | case "!": 215 | next.Op = slice.OpReplace 216 | pre, post, ok := strings.Cut(m[2], ":") 217 | if !ok { 218 | t.Fatalf("Missing separator in argument %q", m[2]) 219 | } 220 | next.X = strings.Fields(pre) 221 | next.Y = strings.Fields(post) 222 | default: 223 | t.Fatalf("Invalid edit op %q", m[1]) 224 | } 225 | out = append(out, next) 226 | } 227 | return 228 | } 229 | 230 | //go:embed testdata/bad-lhs.txt 231 | var badLHS string 232 | 233 | //go:embed testdata/bad-rhs.txt 234 | var badRHS string 235 | 236 | func TestRegression(t *testing.T) { 237 | // The original implementation appended path elements to a slice, which 238 | // could in some circumstances lead to paths clobbering each other. Test 239 | // that this does not regress. 240 | t.Run("ShiftLCS", func(t *testing.T) { 241 | lhs := strings.Split(strings.TrimSpace(badLHS), "\n") 242 | rhs := strings.Split(strings.TrimSpace(badRHS), "\n") 243 | 244 | // If we overwrite the path improperly, this will panic. The output was 245 | // generated from a production value, but the outputs were hashed since 246 | // only the order matters. 247 | _ = slice.EditScript(lhs, rhs) 248 | }) 249 | } 250 | -------------------------------------------------------------------------------- /slice/example_test.go: -------------------------------------------------------------------------------- 1 | package slice_test 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/creachadair/mds/slice" 8 | ) 9 | 10 | func isOdd(v int) bool { return v%2 == 1 } 11 | func isEven(v int) bool { return v%2 == 0 } 12 | 13 | func ExamplePartition() { 14 | vs := []int{3, 1, 8, 4, 2, 6, 9, 10, 5, 7} 15 | odd := slice.Partition(vs, isOdd) 16 | fmt.Println(odd) 17 | // Output: 18 | // [3 1 9 5 7] 19 | } 20 | 21 | func ExampleMatchingKeys() { 22 | vs := map[string]int{"red": 3, "yellow": 6, "blue": 4, "green": 5} 23 | 24 | for key := range slice.MatchingKeys(vs, isEven) { 25 | fmt.Println(key, vs[key]) 26 | } 27 | // Unordered output: 28 | // blue 4 29 | // yellow 6 30 | } 31 | 32 | func ExampleRotate() { 33 | vs := []int{8, 6, 7, 5, 3, 0, 9} 34 | 35 | slice.Rotate(vs, -3) 36 | fmt.Println(vs) 37 | // Output: 38 | // [5 3 0 9 8 6 7] 39 | } 40 | 41 | func ExampleChunks() { 42 | vs := strings.Fields("my heart is a fish hiding in the water grass") 43 | 44 | for c := range slice.Chunks(vs, 3) { 45 | fmt.Println(c) 46 | } 47 | // Output: 48 | // [my heart is] 49 | // [a fish hiding] 50 | // [in the water] 51 | // [grass] 52 | } 53 | 54 | func ExampleBatches() { 55 | vs := strings.Fields("the freckles in our eyes are mirror images that when we kiss are perfectly aligned") 56 | 57 | for b := range slice.Batches(vs, 4) { 58 | fmt.Println(b) 59 | } 60 | // Output: 61 | // [the freckles in our] 62 | // [eyes are mirror images] 63 | // [that when we kiss] 64 | // [are perfectly aligned] 65 | } 66 | 67 | func ExampleLCS() { 68 | fmt.Println(slice.LCS( 69 | []int{1, 0, 3, 4, 2, 7, 9, 9}, 70 | []int{1, 3, 5, 7, 9, 11}, 71 | )) 72 | // Output: 73 | // [1 3 7 9] 74 | } 75 | 76 | func ExampleEditScript() { 77 | lhs := strings.Fields("if you mix red with green you get blue") 78 | rhs := strings.Fields("red mixed with green does not give blue at all") 79 | 80 | fmt.Println("start", lhs) 81 | var out []string 82 | for _, e := range slice.EditScript(lhs, rhs) { 83 | switch e.Op { 84 | case slice.OpDrop: 85 | fmt.Println("drop", e.X) 86 | case slice.OpEmit: 87 | fmt.Println("emit", e.X) 88 | out = append(out, e.X...) 89 | case slice.OpCopy: 90 | fmt.Println("copy", e.Y) 91 | out = append(out, e.Y...) 92 | case slice.OpReplace: 93 | fmt.Println("replace", e.X, "with", e.Y) 94 | out = append(out, e.Y...) 95 | default: 96 | panic("invalid") 97 | } 98 | } 99 | fmt.Println("end", out) 100 | // Output: 101 | // start [if you mix red with green you get blue] 102 | // drop [if you mix] 103 | // emit [red] 104 | // copy [mixed] 105 | // emit [with green] 106 | // replace [you get] with [does not give] 107 | // emit [blue] 108 | // copy [at all] 109 | // end [red mixed with green does not give blue at all] 110 | } 111 | -------------------------------------------------------------------------------- /slice/lis_test.go: -------------------------------------------------------------------------------- 1 | package slice_test 2 | 3 | import ( 4 | "math/rand/v2" 5 | "slices" 6 | "testing" 7 | 8 | "github.com/creachadair/mds/slice" 9 | diff "github.com/google/go-cmp/cmp" 10 | ) 11 | 12 | func TestLNDSAndLIS(t *testing.T) { 13 | t.Parallel() 14 | 15 | tests := []struct { 16 | name string 17 | in []int 18 | lnds []int 19 | lis []int 20 | }{ 21 | { 22 | name: "nil", 23 | }, 24 | { 25 | name: "empty", 26 | in: []int{}, 27 | lnds: []int{}, 28 | lis: []int{}, 29 | }, 30 | { 31 | name: "singleton", 32 | in: []int{1}, 33 | lnds: []int{1}, 34 | lis: []int{1}, 35 | }, 36 | { 37 | name: "sorted", 38 | in: []int{1, 2, 3, 4}, 39 | lnds: []int{1, 2, 3, 4}, 40 | lis: []int{1, 2, 3, 4}, 41 | }, 42 | { 43 | name: "backwards", 44 | in: []int{4, 3, 2, 1}, 45 | lnds: []int{1}, 46 | lis: []int{1}, 47 | }, 48 | { 49 | name: "organ_pipe", 50 | in: []int{1, 2, 3, 4, 3, 2, 1}, 51 | lnds: []int{1, 2, 3, 3}, 52 | lis: []int{1, 2, 3, 4}, 53 | }, 54 | { 55 | name: "sawtooth", 56 | in: []int{0, 1, 0, -1, 0, 1, 0, -1}, 57 | lnds: []int{0, 0, 0, 0}, 58 | lis: []int{-1, 0, 1}, 59 | }, 60 | { 61 | name: "A005132", // from oeis.org 62 | in: []int{0, 1, 3, 6, 2, 7, 13, 20, 12, 21, 11, 22, 10}, 63 | lnds: []int{0, 1, 3, 6, 7, 13, 20, 21, 22}, 64 | lis: []int{0, 1, 3, 6, 7, 13, 20, 21, 22}, 65 | }, 66 | { 67 | name: "swapped_pairs", 68 | in: []int{2, 1, 4, 3, 6, 5, 8, 7}, 69 | lnds: []int{1, 3, 5, 7}, 70 | lis: []int{1, 3, 5, 7}, 71 | }, 72 | { 73 | name: "run_of_equals", 74 | // swapped_pairs with more 3s sprinkled in. 75 | in: []int{2, 1, 3, 4, 3, 6, 3, 5, 8, 3, 7}, 76 | lnds: []int{1, 3, 3, 3, 3, 7}, 77 | lis: []int{1, 3, 4, 5, 7}, 78 | }, 79 | } 80 | 81 | for _, tc := range tests { 82 | t.Run(tc.name, func(t *testing.T) { 83 | lnds := slice.LNDS(tc.in) 84 | if diff := diff.Diff(lnds, tc.lnds); diff != "" { 85 | t.Logf("Input was: %v", tc.in) 86 | t.Logf("Got: %v", lnds) 87 | t.Logf("Want: %v", tc.lnds) 88 | t.Errorf("LNDS subsequence is wrong (-got+want):\n%s", diff) 89 | } 90 | 91 | lis := slice.LIS(tc.in) 92 | if diff := diff.Diff(lis, tc.lis); diff != "" { 93 | t.Logf("Input was: %v", tc.in) 94 | t.Logf("Got: %v", lis) 95 | t.Logf("Want: %v", tc.lis) 96 | t.Errorf("LIS subsequence is wrong (-got+want):\n%s", diff) 97 | } 98 | }) 99 | } 100 | } 101 | 102 | func TestLNDSAgainstLCS(t *testing.T) { 103 | t.Parallel() 104 | 105 | // A result from literature relates LNDS and LCS: 106 | // 107 | // len(LNDS(lst)) == len(LCS(lst, Sorted(lst))) 108 | // 109 | // Check that this holds true. Ideally we could also compare the 110 | // actual resultant lists, but there's no guarantee that LNDS and 111 | // LCS will return the _same_ longest increasing subsequence, if 112 | // multiple options are available. 113 | 114 | const numVals = 50 115 | const numIters = 100 116 | for range numIters { 117 | input := randomInts(numVals) 118 | 119 | gotLNDS := slice.LNDS(input) 120 | 121 | sorted := append([]int(nil), input...) 122 | slices.Sort(sorted) 123 | gotLCS := slice.LCS(input, sorted) 124 | 125 | if got, want := len(gotLNDS), len(gotLCS); got != want { 126 | t.Logf("Input: %v", input) 127 | t.Errorf("len(LNDS(x)) = %v, want len(LCS(x, sorted(x))) = %v", got, want) 128 | } 129 | } 130 | } 131 | 132 | func TestLISAgainstLCS(t *testing.T) { 133 | t.Parallel() 134 | 135 | // The same result from the LNDS vs. LCS test applies, but only on 136 | // lists of distinct elements. 137 | 138 | const numVals = 50 139 | const numIters = 100 140 | for range numIters { 141 | input := rand.Perm(numVals) 142 | 143 | gotLIS := slice.LIS(input) 144 | 145 | sorted := append([]int(nil), input...) 146 | slices.Sort(sorted) 147 | gotLCS := slice.LCS(input, sorted) 148 | 149 | if got, want := len(gotLIS), len(gotLCS); got != want { 150 | t.Logf("Input: %v", input) 151 | t.Errorf("len(LIS(x)) = %v, want len(LCS(x, sorted(x))) = %v", got, want) 152 | } 153 | } 154 | } 155 | 156 | func TestLNDSRandom(t *testing.T) { 157 | t.Parallel() 158 | 159 | const numVals = 50 160 | const numIters = 100 161 | 162 | for range numIters { 163 | input := randomInts(numVals) 164 | want := quadraticIncreasingSubsequence(input, false) 165 | got := slice.LNDS(input) 166 | 167 | if diff := diff.Diff(got, want); diff != "" { 168 | t.Logf("Input: %v", input) 169 | t.Logf("Got: %v", got) 170 | t.Logf("Want: %v", want) 171 | t.Errorf("LNDS subsequence is wrong (-got+want):\n%s", diff) 172 | } 173 | } 174 | } 175 | 176 | func TestLISRandom(t *testing.T) { 177 | t.Parallel() 178 | 179 | const numVals = 50 180 | const numIters = 100 181 | 182 | for range numIters { 183 | input := randomInts(numVals) 184 | want := quadraticIncreasingSubsequence(input, true) 185 | got := slice.LIS(input) 186 | 187 | if diff := diff.Diff(got, want); diff != "" { 188 | t.Logf("Input: %v", input) 189 | t.Logf("Got: %v", got) 190 | t.Logf("Want: %v", want) 191 | t.Errorf("LIS subsequence is wrong (-got+want):\n%s", diff) 192 | } 193 | } 194 | } 195 | 196 | // quadraticRisingSequence recursively scans all subsequences of lst, 197 | // looking for the longest increasing subsequence. if 198 | // strictlyIncreasing is true it returns the same as slice.LIS, 199 | // otherwise it returns the same as slice.LNDS. 200 | func quadraticIncreasingSubsequence(lst []int, strictlyIncreasing bool) []int { 201 | // better reports whether a is a better increasing subsequence 202 | // than b. a is better if it is longer, or if any of its elements 203 | // is smaller than its counterpart in b. 204 | better := func(a, b []int) bool { 205 | if len(a) > len(b) { 206 | return true 207 | } else if len(a) < len(b) { 208 | return false 209 | } 210 | 211 | // We can't use slices.Compare alone because we need list 212 | // length to win over list contents, and contents to matter 213 | // only between equal lists. But we can use it for the equal 214 | // case. 215 | return slices.Compare(a, b) < 0 216 | } 217 | 218 | canExtend := func(prev, next int) bool { return next >= prev } 219 | if strictlyIncreasing { 220 | canExtend = func(prev, next int) bool { return next > prev } 221 | } 222 | 223 | // findIS recursively constructs all possible increasing sequences 224 | // of vs, updating best as it discovers better candidates for 225 | // longest. 226 | var findIS func([]int, []int, []int) []int 227 | findIS = func(vs, acc, best []int) (bestOfTree []int) { 228 | if len(vs) == 0 { 229 | if better(acc, best) { 230 | best = append(best[:0], acc...) 231 | } 232 | return best 233 | } 234 | 235 | lnBest := len(best) 236 | if lnBest > 0 && len(vs)+len(acc) < lnBest { 237 | // can't possibly do better than what's already known, 238 | // give up early. 239 | return best 240 | } 241 | 242 | elt, vs := vs[0], vs[1:] 243 | if len(acc) == 0 || canExtend(acc[len(acc)-1], elt) { 244 | // elt could extend acc, try that 245 | best = findIS(vs, append(acc, elt), best) 246 | } 247 | // and always try skipping elt 248 | return findIS(vs, acc, best) 249 | } 250 | 251 | // Preallocate, so the recursion doesn't add insult to injury by 252 | // allocating as well. 253 | acc := make([]int, 0, len(lst)) 254 | best := make([]int, 0, len(lst)) 255 | 256 | return findIS(lst, acc, best) 257 | } 258 | 259 | func randomInts(N int) []int { 260 | ret := make([]int, N) 261 | for i := range ret { 262 | ret[i] = rand.IntN(2 * N) 263 | } 264 | return ret 265 | } 266 | -------------------------------------------------------------------------------- /slice/testdata/bad-lhs.txt: -------------------------------------------------------------------------------- 1 | d649d 2 | eb11a 3 | 746c7 4 | 8aec8 5 | 25a2b 6 | 1c9f7 7 | bdf3b 8 | 35c6c 9 | 8efe3 10 | 5cc27 11 | a82d8 12 | b243f 13 | a2a5f 14 | f328c 15 | c2951 16 | bb4e1 17 | 094f8 18 | b2c9c 19 | 3959e 20 | a6fb0 21 | 3ed9d 22 | b4b69 23 | 3300a 24 | 5a744 25 | a6fb0 26 | 3e565 27 | b4b69 28 | -------------------------------------------------------------------------------- /slice/testdata/bad-rhs.txt: -------------------------------------------------------------------------------- 1 | d649d 2 | eb11a 3 | 746c7 4 | 8aec8 5 | 25a2b 6 | 1c9f7 7 | bdf3b 8 | 35c6c 9 | 8efe3 10 | 5cc27 11 | a82d8 12 | b243f 13 | a2a5f 14 | f328c 15 | c2951 16 | bb4e1 17 | 094f8 18 | b2c9c 19 | 3959e 20 | a6fb0 21 | 3ed9d 22 | 4e154 23 | 3300a 24 | 5a744 25 | a6fb0 26 | 3e565 27 | b4b69 28 | -------------------------------------------------------------------------------- /stack/stack.go: -------------------------------------------------------------------------------- 1 | // Package stack implements an array-based LIFO stack. 2 | package stack 3 | 4 | import "slices" 5 | 6 | // A Stack is a last-in, first-out sequence of values. 7 | // A zero value is ready for use. 8 | type Stack[T any] struct { 9 | list []T 10 | } 11 | 12 | // New constructs a new empty stack. 13 | func New[T any]() *Stack[T] { return new(Stack[T]) } 14 | 15 | // Push adds an entry for v to the top of s. 16 | func (s *Stack[T]) Push(v T) { s.list = append(s.list, v) } 17 | 18 | // Add is a synonym for Push. 19 | func (s *Stack[T]) Add(v T) { s.list = append(s.list, v) } 20 | 21 | // IsEmpty reports whether s is empty. 22 | func (s *Stack[T]) IsEmpty() bool { return len(s.list) == 0 } 23 | 24 | // Clear discards all the values in s, leaving it empty. 25 | func (s *Stack[T]) Clear() { s.list = nil } 26 | 27 | // Top returns the top element of the stack. If the stack is empty, it returns 28 | // a zero value. 29 | func (s *Stack[T]) Top() T { 30 | if len(s.list) == 0 { 31 | var zero T 32 | return zero 33 | } 34 | return s.list[len(s.list)-1] 35 | } 36 | 37 | // Peek reports whether s has value at offset n from the top of the stack, and 38 | // if so returns its value. Peek(0) returns the same value as Top. 39 | // 40 | // Peek will panic if n < 0. 41 | func (s *Stack[T]) Peek(n int) (T, bool) { 42 | if n >= len(s.list) { 43 | var zero T 44 | return zero, false 45 | } 46 | return s.list[len(s.list)-1-n], true 47 | } 48 | 49 | // Pop reports whether s is non-empty, and if so it removes and returns its top 50 | // value. 51 | func (s *Stack[T]) Pop() (T, bool) { 52 | out, ok := s.Peek(0) 53 | if ok { 54 | var zero T 55 | s.list[len(s.list)-1] = zero 56 | s.list = s.list[:len(s.list)-1] 57 | } 58 | return out, ok 59 | } 60 | 61 | // Each is a range function that calls f with each value in s, in order from 62 | // newest to oldest. If f returns false, Each returns immediately. 63 | func (s *Stack[T]) Each(f func(T) bool) { 64 | for i := len(s.list) - 1; i >= 0; i-- { 65 | if !f(s.list[i]) { 66 | return 67 | } 68 | } 69 | } 70 | 71 | // Len reports the number of elements in s. This is a constant-time operation. 72 | func (s *Stack[T]) Len() int { return len(s.list) } 73 | 74 | // Slice returns a slice containing a copy of the elmeents of s in order from 75 | // newest to oldest. If s is empty, Slice returns nil. 76 | func (s *Stack[T]) Slice() []T { 77 | cp := slices.Clone(s.list) // Clone preserves nil 78 | slices.Reverse(cp) 79 | return cp 80 | } 81 | -------------------------------------------------------------------------------- /stack/stack_test.go: -------------------------------------------------------------------------------- 1 | package stack_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/internal/mdtest" 7 | "github.com/creachadair/mds/stack" 8 | ) 9 | 10 | var _ mdtest.Shared[any] = (*stack.Stack[any])(nil) 11 | 12 | func TestStack(t *testing.T) { 13 | s := stack.New[int]() 14 | check := func(want ...int) { mdtest.CheckContents(t, s, want) } 15 | 16 | // Top and Pop of an empty stack report no value. 17 | if v := s.Top(); v != 0 { 18 | t.Errorf("Top: got %v, want 0", v) 19 | } 20 | if v, ok := s.Peek(0); ok || v != 0 { 21 | t.Errorf("Peek(0): got (%v, %v), want (0, false)", v, ok) 22 | } 23 | if v, ok := s.Pop(); ok { 24 | t.Errorf("Pop: got (%v, %v), want (0, false)", v, ok) 25 | } 26 | 27 | check() 28 | if !s.IsEmpty() { 29 | t.Error("IsEmpty is incorrectly false") 30 | } 31 | if n := s.Len(); n != 0 { 32 | t.Errorf("Len: got %d, want 0", n) 33 | } 34 | 35 | s.Push(1) 36 | if s.IsEmpty() { 37 | t.Error("IsEmpty is incorrectly true") 38 | } 39 | check(1) 40 | 41 | s.Push(2) 42 | check(2, 1) 43 | 44 | s.Push(3) 45 | check(3, 2, 1) 46 | if n := s.Len(); n != 3 { 47 | t.Errorf("Len: got %d, want 3", n) 48 | } 49 | 50 | top := s.Top() 51 | if top != 3 { 52 | t.Errorf("Top: got %v, want 3", top) 53 | } 54 | if v, ok := s.Peek(0); !ok || v != top { 55 | t.Errorf("Peek(0): got (%v, %v), want (%v, true)", v, ok, top) 56 | } 57 | if v, ok := s.Peek(1); !ok || v != 2 { 58 | t.Errorf("Peek(1): got (%v, %v), want (2, true)", v, ok) 59 | } 60 | if v, ok := s.Peek(10); ok { 61 | t.Errorf("Peek(10): got (%v, %v), want (0, false)", v, ok) 62 | } 63 | 64 | if v, ok := s.Pop(); !ok || v != top { 65 | t.Errorf("Pop: got (%v, %v), want (%v, true)", v, ok, top) 66 | } 67 | check(2, 1) 68 | 69 | s.Clear() 70 | check() 71 | } 72 | -------------------------------------------------------------------------------- /stree/README.md: -------------------------------------------------------------------------------- 1 | # stree 2 | 3 | [![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/creachadair/mds/stree) 4 | 5 | This package provides an implementation of Scapegoat Trees, as described in 6 | https://people.csail.mit.edu/rivest/pubs/GR93.pdf 7 | 8 | ## Visualization 9 | 10 | One of the unit tests supports writing its output to a Graphviz `.dot` file so 11 | that you can see what the output looks like for different weighting conditions. 12 | To use this, include the `-dot` flag when running the tests, e.g., 13 | 14 | ```shell 15 | # The -balance parameter is as specified for scapegoat.New. 16 | go test -dot w300.dot -balance 300 17 | ``` 18 | 19 | Example output: 20 | 21 | ``` 22 | go test -dot x.dot -balance 50 -text limerick.txt 23 | dot -Tpng -o limerick.png x.dot 24 | ``` 25 | 26 | ![graph](./limerick.png) 27 | -------------------------------------------------------------------------------- /stree/bench_test.go: -------------------------------------------------------------------------------- 1 | package stree_test 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand/v2" 7 | "sort" 8 | "testing" 9 | 10 | "github.com/creachadair/mds/stree" 11 | ) 12 | 13 | const benchSeed = 1471808909908695897 14 | 15 | // Trial values of β for load-testing tree operations. 16 | var balances = []int{0, 50, 100, 150, 200, 250, 300, 500, 800, 1000} 17 | 18 | func intCompare(a, b int) int { return a - b } 19 | 20 | func randomTree(b *testing.B, β int) (*stree.Tree[int], []int) { 21 | rng := rand.New(rand.NewPCG(benchSeed, benchSeed)) 22 | values := make([]int, b.N) 23 | for i := range values { 24 | values[i] = rng.IntN(math.MaxInt32) 25 | } 26 | return stree.New(β, intCompare, values...), values 27 | } 28 | 29 | func BenchmarkNew(b *testing.B) { 30 | for _, β := range balances { 31 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 32 | randomTree(b, β) 33 | }) 34 | } 35 | } 36 | 37 | func BenchmarkAddRandom(b *testing.B) { 38 | for _, β := range balances { 39 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 40 | _, values := randomTree(b, β) 41 | b.ResetTimer() 42 | tree := stree.New[int](β, intCompare) 43 | for _, v := range values { 44 | tree.Add(v) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func BenchmarkAddOrdered(b *testing.B) { 51 | for _, β := range balances { 52 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 53 | tree := stree.New[int](β, intCompare) 54 | for i := range b.N { 55 | tree.Add(i + 1) 56 | } 57 | }) 58 | } 59 | } 60 | 61 | func BenchmarkRemoveRandom(b *testing.B) { 62 | for _, β := range balances { 63 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 64 | tree, values := randomTree(b, β) 65 | b.ResetTimer() 66 | for _, v := range values { 67 | tree.Remove(v) 68 | } 69 | }) 70 | } 71 | } 72 | 73 | func BenchmarkRemoveOrdered(b *testing.B) { 74 | for _, β := range balances { 75 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 76 | tree, values := randomTree(b, β) 77 | sort.Slice(values, func(i, j int) bool { 78 | return values[i] < values[j] 79 | }) 80 | b.ResetTimer() 81 | for _, v := range values { 82 | tree.Remove(v) 83 | } 84 | }) 85 | } 86 | } 87 | 88 | func BenchmarkLookup(b *testing.B) { 89 | for _, β := range balances { 90 | b.Run(fmt.Sprintf("β=%d", β), func(b *testing.B) { 91 | tree, values := randomTree(b, β) 92 | b.ResetTimer() 93 | for _, v := range values { 94 | tree.Get(v) 95 | } 96 | }) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /stree/cursor.go: -------------------------------------------------------------------------------- 1 | package stree 2 | 3 | import "slices" 4 | 5 | // A Cursor is an anchor to a location within a Tree that can be used to 6 | // navigate the structure of the tree. A cursor is Valid if it points to a 7 | // non-empty subtree of its tree. 8 | type Cursor[T any] struct { 9 | // The sequence of nodes from the root to the current item. 10 | // The pointers are shared with the underlying tree. 11 | // If this is empty, the cursor is invalid. 12 | path []*node[T] 13 | } 14 | 15 | // Valid reports whether c is a valid cursor, meaning it points to a non-empty 16 | // subtree of its containing tree. A nil Cursor is treated as invalid. 17 | func (c *Cursor[T]) Valid() bool { return c != nil && len(c.path) != 0 } 18 | 19 | // Clone returns a clone of c that points to the same location, but which is 20 | // unaffected by subsequent movement of c (and vice versa). 21 | func (c *Cursor[T]) Clone() *Cursor[T] { 22 | if !c.Valid() { 23 | return c 24 | } 25 | return &Cursor[T]{path: slices.Clone(c.path)} 26 | } 27 | 28 | // Key returns the key at the current location of the cursor. 29 | // An invalid Cursor returns a zero-valued key. 30 | func (c *Cursor[T]) Key() T { 31 | if c.Valid() { 32 | return c.path[len(c.path)-1].X 33 | } 34 | var zero T 35 | return zero 36 | } 37 | 38 | // findNext reports the location of the successor of c. 39 | // If no successor exists, it returns (nil, -1). 40 | // 41 | // If the successor is a descendant of c, it returns (right, -1) where right is 42 | // the right child of c. 43 | // 44 | // Otherwise the successor is an ancestor of c, and it returns (nil, i) giving 45 | // the offset i in the path where that ancestor is located. 46 | // 47 | // Precondition: c is valid. 48 | func (c *Cursor[T]) findNext() (*node[T], int) { 49 | i := len(c.path) - 1 50 | 51 | // If the current node has a right subtree, its successor is there. 52 | if min := c.path[i].right; min != nil { 53 | return min, -1 54 | } 55 | 56 | // Otherwise, we have to walk back up the tree. If the current node is the 57 | // left child of its parent, the parent is its successor. If not, we keep 58 | // going until we find an ancestor that IS the left child of its parent. If 59 | // no such ancestor exists, there is no successor. 60 | j := i - 1 // j is parent, i is child 61 | for j >= 0 { 62 | // The current node is the left child of its parent. 63 | if c.path[i] == c.path[j].left { 64 | return nil, j 65 | } 66 | i = j 67 | j-- 68 | } 69 | return nil, -1 70 | } 71 | 72 | // HasNext reports whether c has a successor. 73 | // An invalid cursor has no successor. 74 | func (c *Cursor[T]) HasNext() bool { 75 | if c.Valid() { 76 | n, i := c.findNext() 77 | return n != nil || i >= 0 78 | } 79 | return false 80 | } 81 | 82 | // Next advances c to its successor in the tree, and returns c. 83 | // If c had no successor, it becomes invalid. 84 | func (c *Cursor[T]) Next() *Cursor[T] { 85 | if c.Valid() { 86 | min, j := c.findNext() 87 | if min != nil { 88 | for ; min != nil; min = min.left { 89 | c.path = append(c.path, min) 90 | } 91 | } else if j >= 0 { 92 | c.path = c.path[:j+1] 93 | } else { 94 | c.path = nil 95 | } 96 | } 97 | return c 98 | } 99 | 100 | // findPrev reports the location of the predecessor of c. 101 | // If no predecessor exists, it returns (nil, -1). 102 | // 103 | // If the predecessor is a descendant of c, it returns (left, -1) where left is 104 | // the left child of c. 105 | // 106 | // Otherwise the predecessoris an ancestor of c, and it returns (nil, i) giving 107 | // the offset i in the path where that ancestor is located. 108 | // 109 | // Precondition: c is valid. 110 | func (c *Cursor[T]) findPrev() (*node[T], int) { 111 | i := len(c.path) - 1 112 | 113 | // If the current node has a left subtree, its predecessor is there. 114 | if max := c.path[i].left; max != nil { 115 | return max, -1 116 | } 117 | 118 | // Otherwise, we have to walk back up the tree. If the current node is the 119 | // right child of its parent, the parent is its predecessor. If not, we keep 120 | // going until we find an ancestor that IS the right child of its parent. 121 | // If no such ancestor exists, there is no predecessor. 122 | j := i - 1 // j is parent, i is child 123 | for j >= 0 { 124 | // The current node is the right child of its parent. 125 | if c.path[i] == c.path[j].right { 126 | return nil, j 127 | } 128 | i = j 129 | j-- 130 | } 131 | return nil, -1 132 | } 133 | 134 | // HasPrev reports whether c has a predecessor. 135 | // An invalid cursor has no predecessor. 136 | func (c *Cursor[T]) HasPrev() bool { 137 | if c.Valid() { 138 | n, i := c.findPrev() 139 | return n != nil || i >= 0 140 | } 141 | return false 142 | } 143 | 144 | // Prev advances c to its predecessor in the tree, and returns c. 145 | // If c had no predecessor, it becomes invalid. 146 | func (c *Cursor[T]) Prev() *Cursor[T] { 147 | if c.Valid() { 148 | max, j := c.findPrev() 149 | if max != nil { 150 | for ; max != nil; max = max.right { 151 | c.path = append(c.path, max) 152 | } 153 | } else if j >= 0 { 154 | c.path = c.path[:j+1] 155 | } else { 156 | c.path = nil 157 | } 158 | } 159 | return c 160 | } 161 | 162 | // HasLeft reports whether c has a non-empty left subtree. 163 | // An invalid cursor has no left subtree. 164 | func (c *Cursor[t]) HasLeft() bool { return c.Valid() && c.path[len(c.path)-1].left != nil } 165 | 166 | // Left moves to the left subtree of c, and returns c. 167 | // If c had no left subtree, it becomes invalid. 168 | func (c *Cursor[T]) Left() *Cursor[T] { 169 | if c.Valid() { 170 | if left := c.path[len(c.path)-1].left; left != nil { 171 | c.path = append(c.path, left) 172 | } else { 173 | c.path = nil // invalidate 174 | } 175 | } 176 | return c 177 | } 178 | 179 | // HasRight reports whether c has a non-empty right subtree. 180 | // An invalid cursor has no right subtree. 181 | func (c *Cursor[t]) HasRight() bool { return c.Valid() && c.path[len(c.path)-1].right != nil } 182 | 183 | // Right moves to the right subtree of c, and returns c. 184 | // If c had no right subtree, it becomes invalid. 185 | func (c *Cursor[T]) Right() *Cursor[T] { 186 | if c.Valid() { 187 | if right := c.path[len(c.path)-1].right; right != nil { 188 | c.path = append(c.path, right) 189 | } else { 190 | c.path = nil // invalidate 191 | } 192 | } 193 | return c 194 | } 195 | 196 | // HasParent reports whether c has a parent. 197 | // An invalid cursor has no parent. 198 | func (c *Cursor[T]) HasParent() bool { return c.Valid() && len(c.path) > 1 } 199 | 200 | // Up moves to the parent of c, and returns c. 201 | // If c had no parent, it becomes invalid.. 202 | func (c *Cursor[T]) Up() *Cursor[T] { 203 | if c.Valid() { 204 | // Note that this may result in c being invalid, if it was already 205 | // pointed at the root of the tree. 206 | c.path = c.path[:len(c.path)-1] 207 | } 208 | return c 209 | } 210 | 211 | // Min moves c to the minimum element of its subtree, and returns c. 212 | func (c *Cursor[T]) Min() *Cursor[T] { 213 | if c.Valid() { 214 | min := c.path[len(c.path)-1] 215 | for min.left != nil { 216 | min = min.left 217 | c.path = append(c.path, min) 218 | } 219 | } 220 | return c 221 | } 222 | 223 | // Max moves c to the maximum element of its subtree, and returns c. 224 | func (c *Cursor[T]) Max() *Cursor[T] { 225 | if c.Valid() { 226 | max := c.path[len(c.path)-1] 227 | for max.right != nil { 228 | max = max.right 229 | c.path = append(c.path, max) 230 | } 231 | } 232 | return c 233 | } 234 | 235 | // Inorder is a range function over each key of the subtree at c in order. 236 | func (c *Cursor[T]) Inorder(yield func(key T) bool) { 237 | if c.Valid() { 238 | c.path[len(c.path)-1].inorder(yield) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /stree/example_test.go: -------------------------------------------------------------------------------- 1 | package stree_test 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/creachadair/mds/stree" 9 | ) 10 | 11 | type Pair struct { 12 | X string 13 | V int 14 | } 15 | 16 | func (p Pair) Compare(q Pair) int { return cmp.Compare(p.X, q.X) } 17 | 18 | func ExampleTree_Add() { 19 | tree := stree.New(200, strings.Compare) 20 | 21 | fmt.Println("inserted:", tree.Add("never")) 22 | fmt.Println("inserted:", tree.Add("say")) 23 | fmt.Println("re-inserted:", tree.Add("never")) 24 | fmt.Println("items:", tree.Len()) 25 | // Output: 26 | // inserted: true 27 | // inserted: true 28 | // re-inserted: false 29 | // items: 2 30 | } 31 | 32 | func ExampleTree_Remove() { 33 | const key = "Aloysius" 34 | tree := stree.New(1, strings.Compare) 35 | 36 | fmt.Println("inserted:", tree.Add(key)) 37 | fmt.Println("removed:", tree.Remove(key)) 38 | fmt.Println("re-removed:", tree.Remove(key)) 39 | // Output: 40 | // inserted: true 41 | // removed: true 42 | // re-removed: false 43 | } 44 | 45 | func ExampleTree_Get() { 46 | tree := stree.New(1, Pair.Compare, 47 | Pair{X: "angel", V: 5}, 48 | Pair{X: "devil", V: 7}, 49 | Pair{X: "human", V: 13}, 50 | ) 51 | 52 | for _, key := range []string{"angel", "apple", "human"} { 53 | hit, ok := tree.Get(Pair{X: key}) 54 | fmt.Println(hit.V, ok) 55 | } 56 | // Output: 57 | // 5 true 58 | // 0 false 59 | // 13 true 60 | } 61 | 62 | func ExampleTree_Inorder() { 63 | tree := stree.New(15, strings.Compare, "eat", "those", "bloody", "vegetables") 64 | for key := range tree.Inorder { 65 | fmt.Println(key) 66 | } 67 | // Output: 68 | // bloody 69 | // eat 70 | // those 71 | // vegetables 72 | } 73 | 74 | func ExampleTree_Min() { 75 | tree := stree.New(50, cmp.Compare[int], 1814, 1956, 955, 1066, 2016) 76 | 77 | fmt.Println("len:", tree.Len()) 78 | fmt.Println("min:", tree.Min()) 79 | fmt.Println("max:", tree.Max()) 80 | // Output: 81 | // len: 5 82 | // min: 955 83 | // max: 2016 84 | } 85 | 86 | func ExampleKV() { 87 | // For brevity, it can be helpful to define a type alias for your items. 88 | 89 | type item = stree.KV[int, string] 90 | 91 | tree := stree.New(100, item{}.Compare(cmp.Compare)) 92 | tree.Add(item{1, "one"}) 93 | tree.Add(item{2, "two"}) 94 | tree.Add(item{3, "three"}) 95 | tree.Add(item{4, "four"}) 96 | 97 | for _, i := range []int{1, 3, 2} { 98 | fmt.Println(tree.Cursor(item{Key: i}).Key().Value) 99 | } 100 | // Output: 101 | // one 102 | // three 103 | // two 104 | } 105 | -------------------------------------------------------------------------------- /stree/internal_test.go: -------------------------------------------------------------------------------- 1 | package stree 2 | 3 | import ( 4 | "cmp" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestVine(t *testing.T) { 10 | const numElem = 25 11 | 12 | // Construct a tree with consecutive integers. 13 | tree := New(100, cmp.Compare[int]) 14 | for i := range numElem { 15 | tree.Add(i + 1) 16 | } 17 | 18 | // Flatten the tree node into a right-linked vine and verify that the result 19 | // contains the original elements. 20 | hd := treeToVine(tree.root) 21 | 22 | t.Run("Collapse", func(t *testing.T) { 23 | i := 0 24 | for cur := hd; cur != nil; cur = cur.right { 25 | i++ 26 | if cur.X != i { 27 | t.Errorf("Node value: got %d, want %d", cur.X, i) 28 | } 29 | if cur.left != nil { 30 | t.Errorf("Node %d has a non-nil left pointer: %v", i, cur.left) 31 | } 32 | } 33 | 34 | if i != numElem { 35 | t.Errorf("Got %d nodes, want %d", i, numElem) 36 | } 37 | }) 38 | 39 | // Reconstitute the tree and verify it is balanced and properly ordered. 40 | t.Run("Rebuild", func(t *testing.T) { 41 | rec := vineToTree(hd, numElem) 42 | want := int(math.Ceil(math.Log2(numElem))) 43 | if got := rec.height(); got > want { 44 | t.Errorf("Got height %d, want %d", got, want) 45 | } 46 | 47 | i := 0 48 | rec.inorder(func(z int) bool { 49 | i++ 50 | if z != i { 51 | t.Errorf("Node value: got %d, want %d", z, i) 52 | } 53 | return true 54 | }) 55 | 56 | if i != numElem { 57 | t.Errorf("Got %d nodes, want %d", i, numElem) 58 | } 59 | }) 60 | } 61 | 62 | func (n *node[T]) height() int { 63 | if n == nil { 64 | return 0 65 | } 66 | return max(n.left.height(), n.right.height()) + 1 67 | } 68 | -------------------------------------------------------------------------------- /stree/limerick.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/creachadair/mds/ce01e3869d4eb18e59d66327cf03b0dcafcbd0d3/stree/limerick.png -------------------------------------------------------------------------------- /stree/limerick.txt: -------------------------------------------------------------------------------- 1 | A bather whose clothing was strewed 2 | by breezes that left her quite nude 3 | saw a man come along 4 | and unless I'm quite wrong 5 | you expected this line to be lewd 6 | -------------------------------------------------------------------------------- /stree/node.go: -------------------------------------------------------------------------------- 1 | package stree 2 | 3 | type node[T any] struct { 4 | X T 5 | left, right *node[T] 6 | } 7 | 8 | // clone returns a deep copy of n. 9 | func (n *node[T]) clone() *node[T] { 10 | if n == nil { 11 | return nil 12 | } 13 | return &node[T]{X: n.X, left: n.left.clone(), right: n.right.clone()} 14 | } 15 | 16 | // size reports the number of nodes contained in the tree rooted at n. 17 | // If n == nil, this is defined as 0. 18 | func (n *node[T]) size() int { 19 | if n == nil { 20 | return 0 21 | } 22 | return 1 + n.left.size() + n.right.size() 23 | } 24 | 25 | // treeToVine rewrites the tree rooted at n into an inorder linked list, and 26 | // returns the first element of the list. The nodes are modified in-place and 27 | // linked via their right pointers; the left pointers of all the nodes are set 28 | // to nil. 29 | // 30 | // This conversion uses the in-place iterative approach of Stout & Warren, 31 | // where the tree is denormalized in-place via rightward rotations. 32 | func treeToVine[T any](n *node[T]) *node[T] { 33 | stub := &node[T]{right: n} // sentinel 34 | cur := stub 35 | for cur.right != nil { 36 | C := cur.right 37 | if C.left == nil { 38 | cur = C 39 | continue 40 | } 41 | 42 | // Right rotate: 43 | // cur into cur 44 | // \ \ 45 | // C L 46 | // / \ / \ 47 | // L z x C 48 | // / \ / \ 49 | // x y y z 50 | L := C.left 51 | C.left = L.right // y ← C 52 | L.right = C // L → C 53 | cur.right = L // n → L 54 | } 55 | return stub.right 56 | } 57 | 58 | // rotateLeft rewrites the chain of nodes starting from n and linked by right 59 | // pointers, by left-rotating the specified number of times. This function will 60 | // panic if count exceeds the length of the chain. 61 | // 62 | // A single left-rotation transforms: 63 | // 64 | // n into n 65 | // \ \ 66 | // C R 67 | // / \ / \ 68 | // x R C z 69 | // / \ / \ 70 | // y z x y 71 | // 72 | // Note that any of x, y, and z may be nil. 73 | func rotateLeft[T any](n *node[T], count int) { 74 | next := n 75 | for range count { 76 | C := next.right 77 | R := C.right 78 | 79 | C.right = R.left // C → y 80 | R.left = C // C ← R 81 | next.right = R // n → R 82 | 83 | next = R // advance 84 | } 85 | } 86 | 87 | // vineToTree rewrites the chain of count nodes starting from n and linked by 88 | // right pointers, into a balanced tree rooted at n. It returns the root of the 89 | // resulting new tree. It will panic if count exceeds the chain length. 90 | // 91 | // This uses Stout & Warren's extension of Day's algorithm that produces a tree 92 | // that is "full" (as much as possible), with leaves filled left-to-right. 93 | func vineToTree[T any](n *node[T], count int) *node[T] { 94 | // Compute the largest power of 2 no greater than count, less 1. 95 | // That is the size of the largest full tree not exceeding count nodes. 96 | step := 1 97 | for step <= count { 98 | step = (2 * step) + 1 // == 2*k - 1 99 | } 100 | step /= 2 101 | 102 | stub := &node[T]{right: n} 103 | 104 | // Step 1: Pack the "loose" elements left over. 105 | // For example, if count == 21 then step == 15 and 6 are left over. 106 | // After (count - step) == 6 rotations we have 15 (a full tree). 107 | // This is done first to ensure the leaves fill left-to-right. 108 | rotateLeft(stub, count-step) 109 | 110 | // Step 2: Pack the full tree and its subtrees. 111 | left := step 112 | for left > 1 { 113 | left /= 2 114 | rotateLeft(stub, left) 115 | } 116 | return stub.right 117 | } 118 | 119 | // extract constructs a balanced tree from the given nodes and returns the root 120 | // of the tree. The child pointers of the resulting nodes are updated in place. 121 | // This function does not allocate on the heap. The nodes must be 122 | // sorted and free of duplicates. 123 | func extract[T any](nodes []*node[T]) *node[T] { 124 | if len(nodes) == 0 { 125 | return nil 126 | } 127 | mid := (len(nodes) - 1) / 2 128 | root := nodes[mid] 129 | root.left = extract(nodes[:mid]) 130 | root.right = extract(nodes[mid+1:]) 131 | return root 132 | } 133 | 134 | // rewrite composes flatten and extract, returning the rewritten root. 135 | // Costs a single size-element array allocation, plus O(lg size) stack space, 136 | // but does no other allocation. 137 | func rewrite[T any](root *node[T], size int) *node[T] { 138 | return vineToTree(treeToVine(root), size) 139 | } 140 | 141 | // popMinRight removes the smallest node from the right subtree of root, 142 | // modifying the tree in-place and returning the node removed. 143 | // This function panics if root == nil or root.right == nil. 144 | func popMinRight[T any](root *node[T]) *node[T] { 145 | par, goat := root, root.right 146 | for goat.left != nil { 147 | par, goat = goat, goat.left 148 | } 149 | if par == root { 150 | root.right = goat.right 151 | } else { 152 | par.left = goat.right 153 | } 154 | goat.left = nil 155 | goat.right = nil 156 | return goat 157 | } 158 | 159 | // inorder visits the subtree under n inorder, calling f until f returns false. 160 | func (n *node[T]) inorder(f func(T) bool) bool { 161 | for n != nil { 162 | if ok := n.left.inorder(f); !ok { 163 | return false 164 | } else if ok := f(n.X); !ok { 165 | return false 166 | } 167 | n = n.right 168 | } 169 | return true 170 | } 171 | 172 | // pathTo returns the sequence of nodes beginning at n leading to key, if key 173 | // is present. If key was found, its node is the last element of the path. 174 | func (n *node[T]) pathTo(key T, compare func(a, b T) int) []*node[T] { 175 | var path []*node[T] 176 | cur := n 177 | for cur != nil { 178 | path = append(path, cur) 179 | cmp := compare(key, cur.X) 180 | if cmp < 0 { 181 | cur = cur.left 182 | } else if cmp > 0 { 183 | cur = cur.right 184 | } else { 185 | break 186 | } 187 | } 188 | return path 189 | } 190 | 191 | // inorderAfter visits the elements of the subtree under n not less than key 192 | // inorder, calling f for each until f returns false. 193 | func (n *node[T]) inorderAfter(key T, compare func(a, b T) int, f func(T) bool) bool { 194 | // Find the path from the root to key. Any nodes greater than or equal to 195 | // key must be on or to the right of this path. 196 | path := n.pathTo(key, compare) 197 | for i := len(path) - 1; i >= 0; i-- { 198 | cur := path[i] 199 | if compare(cur.X, key) < 0 { 200 | continue 201 | } else if ok := f(cur.X); !ok { 202 | return false 203 | } else if ok := cur.right.inorder(f); !ok { 204 | return false 205 | } 206 | } 207 | return true 208 | } 209 | -------------------------------------------------------------------------------- /value/example_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/creachadair/mds/value" 7 | ) 8 | 9 | var randomValues = []int{1, 6, 16, 19, 4} 10 | 11 | func ExampleMaybe() { 12 | even := make([]value.Maybe[int], 5) 13 | for i, r := range randomValues { 14 | if r%2 == 0 { 15 | even[i] = value.Just(r) 16 | } 17 | } 18 | 19 | var count int 20 | for _, v := range even { 21 | if v.Present() { 22 | count++ 23 | } 24 | } 25 | 26 | fmt.Println("input:", randomValues) 27 | fmt.Println("result:", even) 28 | fmt.Println("count:", count) 29 | // Output: 30 | // input: [1 6 16 19 4] 31 | // result: [Absent[int] 6 16 Absent[int] 4] 32 | // count: 3 33 | } 34 | -------------------------------------------------------------------------------- /value/maybe.go: -------------------------------------------------------------------------------- 1 | package value 2 | 3 | import "fmt" 4 | 5 | // Maybe is a container that can hold a value of type T. 6 | // Just(v) returns a Maybe holding the value v. 7 | // Absent() returns a Maybe that holds no value. 8 | // A zero Maybe is ready for use and is equivalent to Absent(). 9 | // 10 | // It is safe to copy and assign a Maybe value, but note that if a value is 11 | // present, only a shallow copy of the underlying value is made. Maybe values 12 | // are comparable if and only if T is comparable. 13 | type Maybe[T any] struct { 14 | value T 15 | present bool 16 | } 17 | 18 | // Just returns a Maybe holding the value v. 19 | func Just[T any](v T) Maybe[T] { return Maybe[T]{value: v, present: true} } 20 | 21 | // Absent returns a Maybe holding no value. 22 | // A zero Maybe is equivalent to Absent(). 23 | func Absent[T any]() Maybe[T] { return Maybe[T]{} } 24 | 25 | // Present reports whether m holds a value. 26 | func (m Maybe[T]) Present() bool { return m.present } 27 | 28 | // GetOK reports whether m holds a value, and if so returns that value. 29 | // If m is empty, GetOK returns the zero of T. 30 | func (m Maybe[T]) GetOK() (T, bool) { return m.value, m.present } 31 | 32 | // Get returns value held in m, if present; otherwise it returns the zero of T. 33 | func (m Maybe[T]) Get() T { return m.value } 34 | 35 | // Or returns m if m holds a value; otherwise it returns Just(o). 36 | func (m Maybe[T]) Or(o T) Maybe[T] { 37 | if m.present { 38 | return m 39 | } 40 | return Just(o) 41 | } 42 | 43 | // Ptr converts m to a pointer. It returns nil if m is empty, otherwise it 44 | // returns a pointer to a location containing the value held in m. 45 | func (m Maybe[T]) Ptr() *T { 46 | if m.present { 47 | return &m.value 48 | } 49 | return nil 50 | } 51 | 52 | // String returns the string representation of m. If m holds a value v, the 53 | // string representation of m is that of v. 54 | func (m Maybe[T]) String() string { 55 | if m.present { 56 | return fmt.Sprint(m.value) 57 | } 58 | return fmt.Sprintf("Absent[%T]", m.value) 59 | } 60 | 61 | // Check returns Just(v) if err == nil; otherwise it returns Absent(). 62 | func Check[T any](v T, err error) Maybe[T] { 63 | if err == nil { 64 | return Just(v) 65 | } 66 | return Maybe[T]{} 67 | } 68 | -------------------------------------------------------------------------------- /value/maybe_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/creachadair/mds/value" 8 | ) 9 | 10 | func TestMaybe(t *testing.T) { 11 | t.Run("Zero", func(t *testing.T) { 12 | var v1 value.Maybe[int] 13 | 14 | for _, v := range []value.Maybe[int]{v1, value.Absent[int]()} { 15 | if v.Present() { 16 | t.Error("Zero maybe should not be present") 17 | } 18 | if got := v.Get(); got != 0 { 19 | t.Errorf("Get: got %d, want 0", got) 20 | } 21 | } 22 | }) 23 | 24 | t.Run("Present", func(t *testing.T) { 25 | v := value.Just("apple") 26 | if got, ok := v.GetOK(); !ok || got != "apple" { 27 | t.Errorf("GetOK: got %q, %v; want apple, true", got, ok) 28 | } 29 | if got := v.Get(); got != "apple" { 30 | t.Errorf("Get: got %q, want apple", got) 31 | } 32 | if !v.Present() { 33 | t.Error("Value should be present") 34 | } 35 | }) 36 | 37 | t.Run("Or", func(t *testing.T) { 38 | v := value.Just("pear") 39 | absent := value.Absent[string]() 40 | tests := []struct { 41 | lhs value.Maybe[string] 42 | rhs, want string 43 | }{ 44 | {absent, "", ""}, 45 | {v, "", "pear"}, 46 | {absent, "plum", "plum"}, 47 | {v, "plum", "pear"}, 48 | } 49 | for _, tc := range tests { 50 | if got := tc.lhs.Or(tc.rhs); got != value.Just(tc.want) { 51 | t.Errorf("%v.Or(%v): got %v, want %v", tc.lhs, tc.rhs, got, tc.want) 52 | } 53 | } 54 | }) 55 | 56 | t.Run("Ptr", func(t *testing.T) { 57 | t.Run("Present", func(t *testing.T) { 58 | v := value.Just("plum") 59 | if p := v.Ptr(); p == nil { 60 | t.Errorf("Ptr(%v): got nil, want non-nil", v) 61 | } else if *p != "plum" { 62 | t.Errorf("*Ptr(%v): got %q, want %q", v, *p, "plum") 63 | } 64 | }) 65 | 66 | t.Run("Absent", func(t *testing.T) { 67 | v := value.Absent[int]() 68 | if p := v.Ptr(); p != nil { 69 | t.Errorf("Ptr(%v): got %p (%d), want nil", v, p, *p) 70 | } 71 | }) 72 | }) 73 | 74 | t.Run("String", func(t *testing.T) { 75 | v := value.Just("pear") 76 | if got := v.String(); got != "pear" { 77 | t.Errorf("String: got %q, want pear", got) 78 | } 79 | 80 | var w value.Maybe[string] 81 | if got, want := w.String(), "Absent[string]"; got != want { 82 | t.Errorf("String: got %q, want %q", got, want) 83 | } 84 | }) 85 | } 86 | 87 | func TestCheck(t *testing.T) { 88 | t.Run("OK", func(t *testing.T) { 89 | got := value.Check(strconv.Atoi("1")) 90 | if want := value.Just(1); got != want { 91 | t.Errorf("Check(1): got %v, want %v", got, want) 92 | } 93 | }) 94 | t.Run("Error", func(t *testing.T) { 95 | got := value.Check(strconv.Atoi("bogus")) 96 | if got.Present() { 97 | t.Errorf("Check(bogus): got %v, want absent", got) 98 | } 99 | }) 100 | } 101 | 102 | func TestAtMaybe(t *testing.T) { 103 | tests := []struct { 104 | input *string 105 | want value.Maybe[string] 106 | }{ 107 | {nil, value.Absent[string]()}, 108 | {value.Ptr("foo"), value.Just("foo")}, 109 | {value.Ptr(""), value.Just("")}, 110 | } 111 | for _, tc := range tests { 112 | if got := value.AtMaybe(tc.input); got != tc.want { 113 | t.Errorf("MaybeAt(%p): got %q, want %q", tc.input, got, tc.want) 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /value/value.go: -------------------------------------------------------------------------------- 1 | // Package value defines adapters for value types. 2 | package value 3 | 4 | // Ptr returns a pointer to its argument type containing v. 5 | func Ptr[T any](v T) *T { return &v } 6 | 7 | // At returns the value pointed to by p, or zero if p == nil. 8 | func At[T any](p *T) T { 9 | if p == nil { 10 | var zero T 11 | return zero 12 | } 13 | return *p 14 | } 15 | 16 | // AtDefault returns the value pointed to by p, or dflt if p == nil. 17 | func AtDefault[T any](p *T, dflt T) T { 18 | if p == nil { 19 | return dflt 20 | } 21 | return *p 22 | } 23 | 24 | // Cond returns x if b is true, otherwise it returns y. 25 | func Cond[T any](b bool, x, y T) T { 26 | if b { 27 | return x 28 | } 29 | return y 30 | } 31 | 32 | // AtMaybe returns Just(*p) if p != nil, or otherwise Absent(). 33 | func AtMaybe[T any](p *T) Maybe[T] { 34 | if p == nil { 35 | return Absent[T]() 36 | } 37 | return Just(*p) 38 | } 39 | -------------------------------------------------------------------------------- /value/value_test.go: -------------------------------------------------------------------------------- 1 | package value_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/creachadair/mds/value" 7 | ) 8 | 9 | func TestPtr(t *testing.T) { 10 | p1 := value.Ptr("foo") 11 | p2 := value.Ptr("foo") 12 | if p1 == p2 { 13 | t.Errorf("Values should have distinct pointers (%p == %p)", p1, p1) 14 | } 15 | if *p1 != "foo" || *p2 != "foo" { 16 | t.Errorf("Got p1=%q, p2=%q; wanted both foo", *p1, *p2) 17 | } 18 | } 19 | 20 | func TestAt(t *testing.T) { 21 | tests := []struct { 22 | input *string 23 | want string 24 | }{ 25 | {nil, ""}, 26 | {value.Ptr("foo"), "foo"}, 27 | } 28 | for _, tc := range tests { 29 | if got := value.At(tc.input); got != tc.want { 30 | t.Errorf("At(%p): got %q, want %q", tc.input, got, tc.want) 31 | } 32 | } 33 | } 34 | 35 | func TestAtDefault(t *testing.T) { 36 | tests := []struct { 37 | input *string 38 | dflt string 39 | want string 40 | }{ 41 | {nil, "", ""}, 42 | {nil, "foo", "foo"}, 43 | {value.Ptr("foo"), "bar", "foo"}, 44 | } 45 | for _, tc := range tests { 46 | if got := value.AtDefault(tc.input, tc.dflt); got != tc.want { 47 | t.Errorf("AtDefault(%p, %q): got %q, want %q", tc.input, tc.dflt, got, tc.want) 48 | } 49 | } 50 | } 51 | 52 | func TestCond(t *testing.T) { 53 | tests := []struct { 54 | flag bool 55 | x, y string 56 | want string 57 | }{ 58 | {true, "a", "b", "a"}, 59 | {false, "a", "b", "b"}, 60 | {true, "", "q", ""}, 61 | {false, "", "q", "q"}, 62 | {true, "z", "", "z"}, 63 | {false, "z", "", ""}, 64 | } 65 | for _, tc := range tests { 66 | if got := value.Cond(tc.flag, tc.x, tc.y); got != tc.want { 67 | t.Errorf("Cond(%v, %v, %v): got %v, want %v", tc.flag, tc.x, tc.y, got, tc.want) 68 | } 69 | } 70 | } 71 | --------------------------------------------------------------------------------