diff --git a/.gitignore b/.gitignore index 8042073..986852d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ *.out /_* ._* +/ds/genarray/genarray.go diff --git a/MIGRATIONS.md b/MIGRATIONS.md index c497ce4..01e766d 100644 --- a/MIGRATIONS.md +++ b/MIGRATIONS.md @@ -14,7 +14,7 @@ guarantees. Instead, they have the guarantees described in the following table. | Stability | Meaning | |:-------------------:|----------------------------------------------------------------------------------------------------------------------------------| -| Stable | There will be no breaking changes to this package, except between major versions. | +| Stable | There will be no breaking changes to this package within the same major version (e.g. only between v2 and v3). | | Latest | Small breaking changes to this package are possible, even between minor versions. They will usually have migration instructions. | | Latest *(unstable)* | Large breaking changes to this package are likely, even between minor versions, and may not have migration instructions. | @@ -24,6 +24,11 @@ minor version changes. ## Updating `github.com/tawesoft/golib` +### Migrating v2.8 → v2.10 + +* `iter.Check` has dropped its first return value. +* package `digraph` has moved into the `ds` subdirectory. + ### Migrating v2.0 → v2.8 Fewer breaking changes are expected after this point. @@ -31,6 +36,7 @@ Fewer breaking changes are expected after this point. * `meta` packages have been moved to `html/meta` * `fun/maybe`, `fun/result`, and `view` packages have been redone * `numbers` package removed with much implemented by the new `operator` package + instead * `operator/checked/integer` is now just `operator/checked`. * some functions removed from `fun/result`, `iter`, `ks` packages * some functions moved from `ks` to `operator` diff --git a/README.md b/README.md index 9af51e0..0fdf4aa 100644 --- a/README.md +++ b/README.md @@ -19,65 +19,81 @@ support, are additionally covered by compatible [MIT-like licences](/LICENSE-PAR ## Packages -### General Packages - -| Name | Stable | Latest | Description | -|:---------------------:|:---------:|:---------:|:-----------------------------------------------------| -| css/tokenizer | - | [v2][c01] | CSS tokenizer for [CSS Syntax Module Level 3][css1] | -| dialog | [v2][d01] | - | cross-platform message boxes & file pickers | -| digraph | - | [v2][d02] | *(unstable)* directed graphs (including DAGs) | -| drop | - | - | *(TODO)* drop process privileges and inherit handles | -| fun/either | [v2][f01] | - | "Either" sum type | -| fun/future | [v2][f02] | - | synchronous and asynchronous future values | -| fun/maybe | [v2][f03] | - | "Maybe" sum type | -| fun/partial | [v2][f04] | - | partial function application | -| fun/promise | [v2][f05] | - | store computations to be performed later | -| fun/result | [v2][f06] | - | "Result" sum type | -| fun/slices | [v2][f07] | - | higher-order functions for slices | -| grace | - | - | *(TODO)* start and gracefully shutdown processes | -| humanize | - | - | *(TODO)* locale-aware numbers & quantities | -| iter | [v2][i01] | - | composable lazy iteration | -| ks | - | [v2][k01] | *(unstable)* "kitchen sink" of extras | -| loader | - | - | *(TODO)* concurrent dependency graph solver | -| html/meta/opengraph | [v2][h01] | - | HTML meta tags for Facebook's Open Graph protocol | -| html/meta/twittercard | [v2][h02] | - | HTML meta tags for Twitter Cards | -| must | [v2][m03] | - | assertions | -| operator | [v2][o01] | - | operators as functions | -| tuple | [v2][p01] | - | convert to/from tuples | -| view | [v2][v01] | - | dynamic views over collections | - **Note:** Additional v2/legacy packages exist for users migrating from `tawesoft.co.uk/go`. See [MIGRATIONS.md](/MIGRATIONS.md). **Note:** "Stable" packages have the [normal stability guarantees](https://go.dev/doc/modules/version-numbers) -expected for a Go package of v2 or higher. "Latest" packages, or -"Latest *(unstable)*" packages do not. See [MIGRATIONS.md](/MIGRATIONS.md). - -### Text Packages - -| Name | Stable | Latest | Description | -|:-----------------------:|:---------:|:---------:|:----------------------------------------------------------| -| text/ccc | - | [v2][t01] | Unicode Canonical Combining Class values | -| text/dm | - | [v2][t02] | Unicode decomposition mappings & selective decompositions | -| text/fallback | - | [v2][t03] | Unicode Character Fallback Substitutions | -| text/fold | - | [v2][t04] | Unicode text folding | -| text/np | - | [v2][t05] | Unicode numeric properties | -| text/number/algorithmic | [v2][t07] | - | CLDR algorithmic (non-decimal) numbering systems | -| text/number/plurals | [v2][t08] | - | CLDR plural rules with a simple interface | -| text/number/rbnf | - | [v2][t09] | CLDR Rule-Based Number Formats | -| text/number/symbols | - | [v2][t10] | CLDR locale-appropriate Number Symbols | +expected for a Go package of v2 or higher. "Latest" +packages do not. See [MIGRATIONS.md](/MIGRATIONS.md). +### General Packages -**Note:** "Stable" packages have the -[normal stability guarantees](https://go.dev/doc/modules/version-numbers) -expected for a Go package of v2 or higher. "Latest" packages, or -"Latest *(unstable)*" packages do not. See [MIGRATIONS.md](/MIGRATIONS.md). +| Name | Stable | Latest | Description | +|:------------------------|:---------:|:---------:|:----------------------------------------------------| +| `css/tokenizer` | - | [v2][c01] | CSS tokenizer for [CSS Syntax Module Level 3][css1] | +| `dialog` | [v2][d01] | - | cross-platform message boxes & file pickers | +| `iter` | [v2][i01] | - | composable lazy iteration | +| `ks` | - | [v2][k01] | *(unstable)* "kitchen sink" of extras | +| `html/meta/opengraph` | [v2][h01] | - | HTML meta tags for Facebook's Open Graph protocol | +| `html/meta/twittercard` | [v2][h02] | - | HTML meta tags for Twitter Cards | +| `must` | [v2][m03] | - | assertions | +| `operator` | [v2][o01] | - | operators as functions | +| `tuple` | [v2][p01] | - | convert to/from tuples | +| `view` | [v2][v01] | - | dynamic views over collections | + + +### Data-Structures + +| Name | Stable | Latest | Description | +|:----------|:---------:|:---------:|:----------------------------------------------| +| `bitseq` | [v2][b01] | - | compact sequence of bits | +| `digraph` | - | [v2][d02] | *(unstable)* directed graphs (including DAGs) | +| `garry` | - | [v2][g01] | generational array indices | + + + +### Functional-style Packages + +| Name | Stable | Latest | Description | +|:--------------|:---------:|:------:|:-------------------------------------------| +| `fun/either` | [v2][f01] | - | "Either" type | +| `fun/future` | [v2][f02] | - | synchronous and asynchronous future values | +| `fun/maybe` | [v2][f03] | - | "Maybe" type | +| `fun/partial` | [v2][f04] | - | partial function application | +| `fun/promise` | [v2][f05] | - | store computations to be performed later | +| `fun/result` | [v2][f06] | - | "Result" type | +| `fun/slices` | [v2][f07] | - | higher-order functions for slices | + +### Text & Unicode Packages + +| Name | Stable | Latest | Description | +|:--------------------------|:---------:|:---------:|:----------------------------------------------------------| +| `text/ccc` | - | [v2][t01] | Unicode Canonical Combining Class values | +| `text/dm` | - | [v2][t02] | Unicode decomposition mappings & selective decompositions | +| `text/fallback` | - | [v2][t03] | Unicode Character Fallback Substitutions | +| `text/fold` | - | [v2][t04] | Unicode text folding | +| `text/np` | - | [v2][t05] | Unicode numeric properties | +| `text/number/algorithmic` | [v2][t07] | - | CLDR algorithmic (non-decimal) numbering systems | +| `text/number/plurals` | [v2][t08] | - | CLDR plural rules with a simple interface | +| `text/number/rbnf` | - | [v2][t09] | CLDR Rule-Based Number Formats | +| `text/number/symbols` | - | [v2][t10] | CLDR locale-appropriate Number Symbols | + +### TODO + +| Name | Stable | Latest | Description | +|:-----------|:------:|:--------:|:--------------------------------------------| +| `drop` | - | _legacy_ | drop process privileges and inherit handles | +| `grace` | - | _legacy_ | start and gracefully shutdown processes | +| `humanize` | - | _legacy_ | locale-aware numbers & quantities | +| `loader` | - | _legacy_ | concurrent dependency graph solver | [css1]: https://www.w3.org/TR/css-syntax-3/ [c01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/css/tokenizer [d01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/dialog -[d02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/digraph +[b01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/ds/bitseq +[d02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/ds/digraph +[g01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/ds/genarray [f01]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/either [f02]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/future [f03]: https://pkg.go.dev/github.com/tawesoft/golib/v2/fun/maybe diff --git a/ds/bitseq/bitseq.go b/ds/bitseq/bitseq.go new file mode 100644 index 0000000..f5f2479 --- /dev/null +++ b/ds/bitseq/bitseq.go @@ -0,0 +1,279 @@ +// Package bitseq efficiently implements a general-purpose and variable-length +// sequence of bits. +package bitseq + +import ( + "encoding/binary" + "errors" + "fmt" + "io" + "math/bits" + "slices" + "strings" + + "github.com/tawesoft/golib/v2/ks" +) + +var ErrRange = errors.New("value out of range") + +// Store is a variable-length container of bits. +// +// The zero-value is a useful value. The store is not suitable for concurrent +// use without additional synchronization. +type Store struct { + // length records the logical number of bits stored. + // + // Note: This may be less than or greater than the number + // of bits actually physically available in buckets. + length int + + // buckets is the sequence of bits packed into a slice of uint64 values. + // Trailing zero bits are not necessarily backed by a real bucket. + buckets []uint64 +} + +// String implements the stringer interface, and prints a bit sequence as '1' +// and '0' characters from left-to-right. +func (s *Store) String() string { + var buf strings.Builder + var n = 0 + for _, bucket := range s.buckets { + for i := 0; i < 64; i++ { + q := (bucket & (1 << i)) != 0 + if q { + buf.WriteByte('1') + } else { + buf.WriteByte('0') + } + n++ + if n >= s.length { break } + } + if n >= s.length { break } + } + // trailing zeros not backed by buckets + for i := n; i < s.length; i++ { + buf.WriteByte('0') + } + return fmt.Sprintf("", s.length, buf.String()) +} + +// Resize resizes the length of the sequence of bits. If growing, trailing +// bits are set to zero. If shrinking, may be able to free some of the surplus +// backing memory. +func (s *Store) Resize(length int) { + if length == s.length { + return + } else if length > s.length { + s.Set(length - 1, false) + } else { + cropped := s.croppedBuckets() + if len(cropped) != cap(s.buckets) { + s.buckets = make([]uint64, len(cropped)) + } + copy(s.buckets, cropped) + s.buckets = s.buckets[:cap(s.buckets)] + + // clear trailing data in last bucket + last := length / 64 + if last > 0 { + offset := 64 - (length % 64) + + for i := offset; i < 64; i++ { + s.buckets[last] = s.buckets[last] & (^(1 << offset)) + } + } + + s.length = length + } +} + +// croppedBuckets returns a subslice of buckets that excludes any buckets that +// are solely trailing zeros. +func (s *Store) croppedBuckets() []uint64 { + if s.buckets == nil { return nil } + + end := len(s.buckets) + for i := len(s.buckets) - 1; i >= 0; i-- { + if s.buckets[i] == 0 { + end = i + } else { + break + } + } + + return s.buckets[0:end] +} + +// magic bytes in the header +const magic = uint64( + (uint64('B') << 0) + + (uint64('i') << 8) + + (uint64('t') << 16) + + (uint64('s') << 24) + + (uint64('e') << 32) + + (uint64('q') << 40) + + (uint64('V') << 48) + + (uint64('1') << 56)) + +// Write writes an opaque binary representation of the Store into w. +func(s *Store) Write(w io.Writer) error { + var err error + var crc uint64 + + write := ks.LiftErrorFunc(func(value uint64) error { + crc = ks.Checksum64(crc, value) + return binary.Write(w, binary.LittleEndian, value) + }) + + buckets := s.croppedBuckets() + stats := (uint64(s.Length()) << 32) + (uint64(len(buckets) << 0)) + err = write(err, magic) + err = write(err, stats) + for _, c := range buckets { + err = write(err, c) + } + err = write(err, crc) + return err +} + +// Read reads an opaque binary representation from r into the provided Store, +// replacing its existing contents iff successful. +// +// Important: While relatively robust against corrupt data, care should be +// taken when parsing arbitrary input. A malicious actor could craft an input +// that would allocate a large amount of memory, or attempt to extract +// information by continuing to consume from the reader. +func Read(dest *Store, r io.Reader) error { + return ks.ErrTODO +} + +// Length returns the number of bits stored. +func (s *Store) Length() int { + // Note: This may be less than or greater than the number + // of bits actually physically available in buckets. + return s.length +} + +// Push stores a true or false bit at the end of the sequence. +func (s *Store) Push(bit bool) { + s.Set(s.length, bit) +} + +// Pop returns and removes the bit at the end of the sequence. +func (s *Store) Pop() bool { + result := s.Get(s.length - 1) + s.Set(s.length - 1, false) // zero unused memory + s.length-- + return result +} + +// Peek returns the bit at the end of the sequence. +func (s *Store) Peek() bool { + return s.Get(s.length - 1) +} + +func fromIndex(index int) (bucket int, offset int) { + bucket = index / 64 + offset = index % 64 + return +} + +// Set sets a bit to true or false at given index, growing the capacity of +// the backing array as necessary if the index exceeds its current size. +// Intermediate values are automatically initialised with false bits. +func (s *Store) Set(index int, bit bool) { + if index < 0 { panic(ErrRange) } + if index >= s.length { s.length = index + 1 } + + bucket, offset := fromIndex(index) + if bucket >= cap(s.buckets) { + if !bit { return } // trailing zeros are implied + s.buckets = slices.Grow(s.buckets, 1 + bucket - len(s.buckets)) + s.buckets = s.buckets[:cap(s.buckets)] + } + if bit { + s.buckets[bucket] = s.buckets[bucket] | (1 << offset) + } else { + s.buckets[bucket] = s.buckets[bucket] & (^(1 << offset)) + } +} + +// Get looks up a bit at a given index, returning true iff it has been set. +// Panics if index is out of range. +func (s *Store) Get(index int) bool { + if (index < 0) || (index >= s.length) { panic(ErrRange) } + return s.getFromBucket(fromIndex(index)) +} + +func (s *Store) getFromBucket(bucket, offset int) bool { + if bucket < len(s.buckets) { + return (s.buckets[bucket] & (1 << offset)) != 0 + } else { + return false // trailing zeros are implied + } +} + +// NextFalse returns the index of the next false bit found after the given +// index. To start at the beginning, start with NextFalse(-1). If the second +// return value is false, then the search has got to the end of the sequence +// without finding any false bits. +func (s *Store) NextFalse(after int) (int, bool) { + buckets := len(s.buckets) + start := after + 1 + bucket := start / 64 + var offset int + + for i := bucket; i < buckets; i++ { + if s.buckets[i] == ^uint64(0) { continue } + + if i == bucket { + // First bucket - start search midway through at given offset + offset = start % 64 + } else { + // Beginning of a subsequent bucket - start at beginning + offset = 0 + } + + for j := offset; j < 64; j++ { + index := (i * 64) + j + if index >= s.length { return -1, false } + if !s.getFromBucket(i, j) { return index, true } + } + } + + return -1, false +} + +// NextTrue returns the index of the next true bit found after the given +// index. To start at the beginning, start with NextTrue(-1). If the second +// return value is false, then the search has got to the end of the sequence +// without finding any true bits. +func (s *Store) NextTrue(after int) (int, bool) { + buckets := len(s.buckets) + start := after + 1 + bucket := start / 64 + + for i := bucket; i < buckets; i++ { + if s.buckets[i] == 0 { continue } + + var offset, limit int + if i == bucket { + // First bucket - start search midway through. If the entire + // remainder is trailing zeros, we can skip early. + offset = start % 64 + limit = 64 - bits.TrailingZeros64(s.buckets[i]) + } else { + // Beginning of a subsequent bucket, skip zero prefix/suffixes. + offset = bits.TrailingZeros64(s.buckets[i]) + limit = 64 - bits.LeadingZeros64(s.buckets[i]) + } + + for j := offset; j < limit; j++ { + index := (i * 64) + j + if index >= s.length { return -1, false } + if s.getFromBucket(i, j) { return index, true } + } + } + + return -1, false +} diff --git a/ds/bitseq/bitseq_test.go b/ds/bitseq/bitseq_test.go new file mode 100644 index 0000000..486c616 --- /dev/null +++ b/ds/bitseq/bitseq_test.go @@ -0,0 +1,223 @@ +package bitseq_test + +import ( + "testing" + + "github.com/tawesoft/golib/v2/ds/bitseq" +) + +func expect(t *testing.T, q bool, format string, args ... any) { + if !q { t.Errorf(format, args...) } +} + +func expectNoPanic(t *testing.T, q func() bool, format string, args ... any) { + defer func() { + if r := recover(); r != nil { + t.Log(r) + t.Logf(format, args...) + t.Fatalf("unexpected panic on test") + } + }() + if !q() { t.Errorf(format, args...) } +} + +func expectPanic(t *testing.T, f func(), format string, args ... any) { + defer func() { + if r := recover(); r == nil { + t.Errorf(format, args...) + } + }() + f() +} + +func TestStore_zeroValue(t *testing.T) { + var s bitseq.Store + expect(t, s.Length() == 0, "zero-value has zero length") + expectPanic(t, func() { s.Pop() }, "zero-value pop should panic") + expectPanic(t, func() { s.Peek() }, "zero-value peek should panic") +} + +func TestStore_pushTrue(t *testing.T) { + var s bitseq.Store + + for i := 0; i < 1000; i++ { + s.Push(true) + expect(t, s.Length() == i + 1, "push length is wrong") + expect(t, s.Peek(), "push then peek is wrong") + } +} + +func TestStore_pushFalse(t *testing.T) { + var s bitseq.Store + + for i := 0; i < 1000; i++ { + s.Push(false) + expect(t, s.Length() == i + 1, "push length is wrong") + expect(t, s.Peek() == false, "push then peek is wrong") + } +} + +func TestStore_pushAlternating(t *testing.T) { + var s bitseq.Store + + for i := 0; i < 1000; i++ { + q := (i % 2 == 0) + s.Push(q) + expect(t, s.Length() == i + 1, "push length is wrong") + expect(t, s.Peek() == q, "push then peek is wrong") + } + + for i := 0; i < 1000; i++ { + q := (i % 2 == 0) + expect(t, s.Get(i) == q, "s.Get(%d) is wrong", i) + } +} + +func TestStore_sparse(t *testing.T) { + var s bitseq.Store + + indexes := []int{5, 63, 75, 255, 256, 256, 257, 550, 511, 259} + lengths := []int{6, 64, 76, 256, 257, 257, 258, 551, 551, 551} + + for i := 0; i < len(indexes); i++ { + expectNoPanic(t, func() bool { s.Set(indexes[i], true); return true }, + "s.Set() panic at index %d", i) + expectNoPanic(t, func() bool { return s.Length() == lengths[i] }, + "length is wrong at index %d (got %d, expected %d)", + i, s.Length(), lengths[i]) + } + + for _, i := range indexes { + expectNoPanic(t, func() bool { return s.Get(i) }, "s.Get(%d) is wrong", i) + } + + checks := []int{0, 1, 4, 6, 62, 64, 74, 76, 254, 258, 549, 510, 512, 260} + + for _, i := range checks { + expectNoPanic(t, func() bool { return !s.Get(i) }, "!s.Get(%d) is wrong", i) + } + + expectPanic(t, func() { s.Get(552) }, "s.Get() should panic when out of range") + + s.Resize(260) + expect(t, s.Length() == 260, "s.Length() should be 260 after first resize") + s.Resize(255) + expect(t, s.Length() == 255, "s.Length() should be 259 after second resize") + s.Resize(260) + expect(t, s.Length() == 260, "s.Length() should be 260 after third resize") + + indexes = []int{5, 63, 75, 255, 256} + checks = []int{0, 1, 4, 6, 62, 64, 74, 76, 254, 259} + + for _, i := range indexes { + expectNoPanic(t, func() bool { return s.Get(i) }, "s.Get(%d) after resize is wrong", i) + } + for _, i := range checks { + expectNoPanic(t, func() bool { return !s.Get(i) }, "!s.Get(%d) after resize is wrong", i) + } + + expectPanic(t, func() { s.Get(260) }, "s.Get() after resize should panic when out of range") +} + +func TestStore_NextTrue(t *testing.T) { + var s bitseq.Store + + // run-length encoded alternating 0s and 1s + // 0 1 0 1 0 1 0 1 0 + var rle = []int{10, 1, 1, 2, 70, 1, 200, 1, 100} + + // (inclusive) + // 000 - 009 : 0, 0, 0..., + // 010 - 010 : 1, + // 011 - 011 : 0, + // 012 - 013 : 1, 1, + // 014 - 083 : 0, 0, 0..., + // 084 - 084 : 1, + // 085 - 284 : 0, 0, 0..., + // 285 - 285 : 1, + // 286 ... : 0, 0, 0..., + + // offset to next occurrence of a true bit + var indexes = []int{10, 12, 13, 84, 285} + + written := 0 + for i := 0; i < len(rle); i++ { + q := (i % 2) == 1 + + // populate the bitseq + for j := 0; j < rle[i]; j++ { + s.Push(q) + written++ + } + + // perform all checks up to that point + current := -1 + for j := 0; j < len(indexes); j++ { + idx := indexes[j] + if idx >= written { break } + + next, ok := s.NextTrue(current) + expect(t, ok, "expected NextTrue(%d) to be true inside loop after %d written", current, written) + if !ok { break } + expect(t, next == idx, "expected NextTrue(%d) to return %d, but got %d after %d written", current, idx, next, written) + current = next + } + _, ok := s.NextTrue(current) + expect(t, !ok, "expected NextTrue to be false after loop") + } +} + +func TestStore_NextFalse(t *testing.T) { + var s bitseq.Store + + // run-length encoded alternating 0s and 1s + // 1 0 1 0 1 0 1 0 1 + var rle = []int{10, 1, 1, 2, 70, 1, 200, 1, 100} + // and later, s.Set(387, false) + + // (inclusive) + // 000 - 009 : 1, 1, 1..., + // 010 - 010 : 0, + // 011 - 011 : 1, + // 012 - 013 : 0, 0, + // 014 - 083 : 1, 1, 1..., + // 084 - 084 : 0, + // 085 - 284 : 1, 1, 1..., + // 285 - 285 : 0, + // 286 - 385 : 1, 1, 1..., + + // offset to next occurrence of a false bit + var indexes = []int{10, 12, 13, 84, 285} + + written := 0 + for i := 0; i < len(rle); i++ { + q := (i % 2) == 0 + + // populate the bitseq + for j := 0; j < rle[i]; j++ { + s.Push(q) + written++ + } + + // perform all checks up to that point + current := -1 + for j := 0; j < len(indexes); j++ { + idx := indexes[j] + if idx >= written { break } + + next, ok := s.NextFalse(current) + expect(t, ok, "expected NextFalse(%d) to be true inside loop after %d written", current, written) + if !ok { break } + expect(t, next == idx, "expected NextFalse(%d) to return %d, but got %d after %d written", current, idx, next, written) + current = next + } + _, ok := s.NextFalse(current) + expect(t, !ok, "expected NextFalse to be false after loop") + } + + // test implicit trailing zeros + s.Set(1000, false) + next, ok := s.NextFalse(285) + expect(t, ok, "expected NextFalse to be true after resize") + expect(t, next == 386, "expected NextFalse(285) to return %d, but got %d after %d written", 386, next, written) +} diff --git a/digraph/TODO.md b/ds/digraph/TODO.md similarity index 100% rename from digraph/TODO.md rename to ds/digraph/TODO.md diff --git a/digraph/bfs.go b/ds/digraph/bfs.go similarity index 100% rename from digraph/bfs.go rename to ds/digraph/bfs.go diff --git a/digraph/bfs_test.go b/ds/digraph/bfs_test.go similarity index 100% rename from digraph/bfs_test.go rename to ds/digraph/bfs_test.go diff --git a/digraph/dag.go b/ds/digraph/dag.go similarity index 100% rename from digraph/dag.go rename to ds/digraph/dag.go diff --git a/digraph/dag_test.go b/ds/digraph/dag_test.go similarity index 100% rename from digraph/dag_test.go rename to ds/digraph/dag_test.go diff --git a/digraph/dfs.go b/ds/digraph/dfs.go similarity index 100% rename from digraph/dfs.go rename to ds/digraph/dfs.go diff --git a/digraph/dfs_test.go b/ds/digraph/dfs_test.go similarity index 100% rename from digraph/dfs_test.go rename to ds/digraph/dfs_test.go diff --git a/digraph/graph.go b/ds/digraph/graph.go similarity index 100% rename from digraph/graph.go rename to ds/digraph/graph.go diff --git a/digraph/graph_test.go b/ds/digraph/graph_test.go similarity index 100% rename from digraph/graph_test.go rename to ds/digraph/graph_test.go diff --git a/digraph/util.go b/ds/digraph/util.go similarity index 100% rename from digraph/util.go rename to ds/digraph/util.go diff --git a/fun/future/async.go b/fun/future/async.go index 739bdb4..195921f 100644 --- a/fun/future/async.go +++ b/fun/future/async.go @@ -19,11 +19,10 @@ func start[T any](ctx context.Context, promise promise.P[T], channel chan result value := result.New(promise.ComputeCtx(ctx)) for { select { - case <- ctx.Done(): return + case <- ctx.Done(): close(channel); return default: channel <- value } } - close(channel) } // NewAsync creates a new future from a promise, and begins computing that diff --git a/fun/partial/examples_test.go b/fun/partial/examples_test.go index 5fc49cd..5e6104f 100644 --- a/fun/partial/examples_test.go +++ b/fun/partial/examples_test.go @@ -8,7 +8,7 @@ import ( "github.com/tawesoft/golib/v2/fun/partial" ) -func Example_Line() { +func Example_line() { // The formula for a line can be given by "y = mx + c", where m is the // gradient, and c is the offset where the line crosses the x-axis. line := func(x int, m int, c int) int { // solves for y @@ -40,7 +40,7 @@ func Example_Line() { // 11 } -func Example_Maybe() { +func Example_maybe() { // divides two numbers, while checking for divide by zero. divide := func(x int, y int) (value int, ok bool) { if y == 0 { return 0, false } @@ -74,7 +74,7 @@ func Example_Maybe() { // divideByZero(10) = 0, false } -func Example_All() { +func Example_all() { // Pythagoras theorem for calculating the hypotenuse of a triangle: // a squared + b squared = c squared. hyp := func(a float64, b float64) float64 { diff --git a/fun/result/examples_test.go b/fun/result/examples_test.go index e43acbb..dc7b42e 100644 --- a/fun/result/examples_test.go +++ b/fun/result/examples_test.go @@ -10,7 +10,7 @@ import ( "github.com/tawesoft/golib/v2/fun/slices" ) -func ExampleResult() { +func ExampleR() { resultOpen := result.WrapFunc(os.Open) toReader := result.Map(func (x *os.File) io.Reader { return x }) diff --git a/iter/iter.go b/iter/iter.go index 027ce47..1e98182 100644 --- a/iter/iter.go +++ b/iter/iter.go @@ -127,22 +127,20 @@ func Cat[X any](its ... It[X]) It[X] { } // Check calls function f for each value produced by an iterator. It halts when -// a non-nil error is returned by f, and immediately returns the value being -// examined at the time and the error. Otherwise, returns a zero value and a -// nil error. +// a non-nil error is returned by f, and immediately returns the error. +// Otherwise, returns a nil error. // // See also [Walk], which is similar but does not check for errors. func Check[X any]( f func(X) error, it It[X], -) (X, error) { - zero := operator.Zero[X]() +) error { for { x, ok := it() if !ok { break } - if err := f(x); err != nil { return x, err } + if err := f(x); err != nil { return err } } - return zero, nil + return nil } // Count calls function f for each value x produced by an iterator. It returns diff --git a/iter/iter_test.go b/iter/iter_test.go index 5c2cbf9..29829a3 100644 --- a/iter/iter_test.go +++ b/iter/iter_test.go @@ -89,44 +89,6 @@ func TestCat(t *testing.T) { assert.Equal(t, []rune("abcdef"), lazy.ToSlice(abcdef)) } -func TestCheck(t *testing.T) { - errorIfOdd := func (x int) error { - if x % 2 != 0 { - return fmt.Errorf("got odd number %d", x) - } - return nil - } - - type row struct { - input []int - expectedValue int - expectedError error - } - rows := []row{ - { - input: []int{}, - expectedValue: 0, - expectedError: nil, - }, - { - input: []int{2, 4, 8, 10}, - expectedValue: 0, - expectedError: nil, - }, - { - input: []int{2, 4, 7, 10}, - expectedValue: 7, - expectedError: fmt.Errorf("got odd number 7"), - }, - } - - for i, r := range rows { - x, err := lazy.Check(errorIfOdd, lazy.FromSlice(r.input)) - assert.Equal(t, x, r.expectedValue, "test %i expected value", i) - assert.Equal(t, err, r.expectedError, "test %i expected error", i) - } -} - func TestCounter(t *testing.T) { min := math.MinInt max := math.MaxInt diff --git a/ks/kitchensink.go b/ks/kitchensink.go index b1bf4f8..ae55694 100644 --- a/ks/kitchensink.go +++ b/ks/kitchensink.go @@ -3,13 +3,39 @@ package ks import ( + "encoding/binary" "errors" + "hash/crc64" "strings" "unicode/utf8" "golang.org/x/exp/utf8string" ) +var ErrTODO = errors.New("TODO") + +var crc64table = crc64.MakeTable(crc64.ECMA) + +func Checksum64(crc uint64, value uint64) uint64 { + var buf [8]byte + binary.LittleEndian.PutUint64(buf[0:8], value) + return crc64.Update(crc, crc64table, buf[0:8]) +} + +// LiftErrorFunc takes any 1-arity function "f(x) => error", and +// returns a new function that takes an additional error input. If that error +// is not nil, it is returned immediately before calling f. Otherwise, the +// result of calling f(x) normally is returned. +// +// This allows multiple simple error-returning functions to be called in +// sequence, with only one error check at the end. +func LiftErrorFunc[X any](f func(x X) error) func(err error, x X) error { + return func(err error, x X) error { + if err != nil { return err } + return f(x) + } +} + // FilterError returns err, unless errors.Is(err, i) returns true for any // i in ignore, in which case it returns nil. // diff --git a/operator/operator.go b/operator/operator.go index e0ec1c4..ee2166d 100644 --- a/operator/operator.go +++ b/operator/operator.go @@ -2,11 +2,11 @@ // (equals) or "+" (addition), as functions that can be passed to higher order // functions. // -// See also the [github.com/tawesoft/golib/v2/operator/checked/integer] package -// which implements operators that are robust against integer overflow. +// The `operator/checked` subpackage implements operators that are robust +// against integer overflow. // -// See also the [github.com/tawesoft/golib/v2/operator/reflect] package -// which implements operators that require reflection. +// The `operator/reflect` subpackage implements operators that require +// reflection. package operator import ( @@ -20,6 +20,9 @@ func Zero[T any]() T { } // Ternary returns a if q is true, or b if q is false. +// +// Note that this does not short-circuit, so both arguments are evaluated +// as inputs to this function. func Ternary[X any](q bool, a X, b X) X { if q { return a } else { return b } }