diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 86ca35b..1ab57f8 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: '1.19' - name: Build run: go build -v ./... diff --git a/Readme.md b/Readme.md index 2ced665..0069fc4 100644 --- a/Readme.md +++ b/Readme.md @@ -6,7 +6,7 @@ tinyconf - a simple and universal library for parsing configs. # Installation -Install via go get. Note that Go 1.18 or newer is required. +Install via go get. Note that Go 1.19 or newer is required. ```sh go get github.com/insei/tinyconf@latest ``` diff --git a/cmp118/cmp.go b/cmp118/cmp.go new file mode 100644 index 0000000..f871886 --- /dev/null +++ b/cmp118/cmp.go @@ -0,0 +1,52 @@ +package cmp118 + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +// +// Note that floating-point types may contain NaN ("not-a-number") values. +// An operator such as == or < will always report false when +// comparing a NaN value with any other value, NaN or not. +// See the [Compare] function for a consistent way to compare NaN values. +type Ordered interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 | + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr | + ~float32 | ~float64 | + ~string +} + +// Compare returns +// +// -1 if x is less than y, +// 0 if x equals y, +// +1 if x is greater than y. +// +// For floating-point types, a NaN is considered less than any non-NaN, +// a NaN is considered equal to a NaN, and -0.0 is equal to 0.0. +func Compare[T Ordered](x, y T) int { + xNaN := isNaN(x) + yNaN := isNaN(y) + if xNaN { + if yNaN { + return 0 + } + return -1 + } + if yNaN { + return +1 + } + if x < y { + return -1 + } + if x > y { + return +1 + } + return 0 +} + +// isNaN reports whether x is a NaN without requiring the math package. +// This will always return false if T is not floating-point. +func isNaN[T Ordered](x T) bool { + return x != x +} diff --git a/drivers/env/env.go b/drivers/env/env.go index a0bf74b..2d747e3 100644 --- a/drivers/env/env.go +++ b/drivers/env/env.go @@ -2,17 +2,18 @@ package env import ( "bufio" - "cmp" "fmt" "os" "path" "reflect" - "slices" "strings" + "github.com/insei/tinyconf" + "github.com/insei/tinyconf/cmp118" + "github.com/insei/tinyconf/slices118" + "github.com/insei/cast" "github.com/insei/fmap/v3" - "github.com/insei/tinyconf" ) type envDriver struct { @@ -73,7 +74,7 @@ func (d envDriver) getUniqueFields(registers []*tinyconf.Registered) []field { tag: tag, } - if slices.ContainsFunc(fields, func(item field) bool { + if slices118.ContainsFunc(fields, func(item field) bool { return item.tag.Get(d.name) == member.tag.Get(d.name) }) { continue @@ -98,9 +99,9 @@ func (d envDriver) getRootMap(fields []field) map[int]map[string]string { func (d envDriver) GenDoc(registers ...*tinyconf.Registered) string { uniqueFields := d.getUniqueFields(registers) - sortedFields := slices.Clone(uniqueFields) - slices.SortStableFunc(sortedFields, func(i, j field) int { - return cmp.Compare(j.depth, i.depth) + sortedFields := slices118.Clone(uniqueFields) + slices118.SortStableFunc(sortedFields, func(i, j field) int { + return cmp118.Compare(j.depth, i.depth) }) roots := d.getRootMap(sortedFields) @@ -108,7 +109,7 @@ func (d envDriver) GenDoc(registers ...*tinyconf.Registered) string { var doc string for _, field := range uniqueFields { - if slices.Contains(marks, field.path) { + if slices118.Contains(marks, field.path) { continue } marks = append(marks, field.path) diff --git a/drivers/yaml/yaml.go b/drivers/yaml/yaml.go index 89713c9..7be8f81 100644 --- a/drivers/yaml/yaml.go +++ b/drivers/yaml/yaml.go @@ -1,15 +1,16 @@ package yaml import ( - "cmp" "fmt" "reflect" - "slices" "strings" "github.com/insei/cast" "github.com/insei/fmap/v3" + "github.com/insei/tinyconf" + "github.com/insei/tinyconf/cmp118" + "github.com/insei/tinyconf/slices118" ) type yamlDriver struct { @@ -116,7 +117,7 @@ func (d *yamlDriver) getUniqueFields(registers []*tinyconf.Registered) []field { tag: tag, } - if slices.ContainsFunc(fields, func(item field) bool { + if slices118.ContainsFunc(fields, func(item field) bool { matchPath := item.path == member.path matchTagDriver := tagDriver == item.tag.Get(d.name) return matchPath && matchTagDriver @@ -151,9 +152,9 @@ func (d *yamlDriver) getRootMap(fields []field) map[string]string { func (d *yamlDriver) GenDoc(registers ...*tinyconf.Registered) string { uniqueFields := d.getUniqueFields(registers) - sortedFields := slices.Clone(uniqueFields) - slices.SortStableFunc(sortedFields, func(i, j field) int { - return cmp.Compare(i.path, j.path) + sortedFields := slices118.Clone(uniqueFields) + slices118.SortStableFunc(sortedFields, func(i, j field) int { + return cmp118.Compare(i.path, j.path) }) roots := d.getRootMap(sortedFields) diff --git a/go.mod b/go.mod index 55d339d..057df79 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,10 @@ module github.com/insei/tinyconf -go 1.21 +go 1.19 require ( github.com/insei/cast v1.1.1 - github.com/insei/fmap/v3 v3.0.0 + github.com/insei/fmap/v3 v3.1.2 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index 1d00d94..a07fb19 100644 --- a/go.sum +++ b/go.sum @@ -4,14 +4,13 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/insei/cast v1.1.1 h1:Elrnc3yo5FvaN8B6KWP/+UWblgKG8uj0/UbE/olcLYM= github.com/insei/cast v1.1.1/go.mod h1:WyueAs28LJPpteJTcUZbkt3eLffnNczOR7B4ODQ8lrE= -github.com/insei/fmap/v3 v3.0.0 h1:BpRsFgQ2nt5tl/8tzp6y+MWIAJhqCTeKBHC56L/Um30= -github.com/insei/fmap/v3 v3.0.0/go.mod h1:Kk0gs7nKb4E/JycKJFnrsX5hlyBBe0yetGKFCJG0vzk= +github.com/insei/fmap/v3 v3.1.2 h1:ZBr+WiZpIxFNeMo2X4QOST4AFl0sGAkG+EO08Ved3bY= +github.com/insei/fmap/v3 v3.1.2/go.mod h1:Kk0gs7nKb4E/JycKJFnrsX5hlyBBe0yetGKFCJG0vzk= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= diff --git a/slices118/slices.go b/slices118/slices.go new file mode 100644 index 0000000..51543c4 --- /dev/null +++ b/slices118/slices.go @@ -0,0 +1,48 @@ +// Package slices118 defines various functions useful with slices118 of any type. +package slices118 + +// Index returns the index of the first occurrence of v in s, +// or -1 if not present. +func Index[S ~[]E, E comparable](s S, v E) int { + for i := range s { + if v == s[i] { + return i + } + } + return -1 +} + +// IndexFunc returns the first index i satisfying f(s[i]), +// or -1 if none do. +func IndexFunc[S ~[]E, E any](s S, f func(E) bool) int { + for i := range s { + if f(s[i]) { + return i + } + } + return -1 +} + +// Contains reports whether v is present in s. +func Contains[S ~[]E, E comparable](s S, v E) bool { + return Index(s, v) >= 0 +} + +// ContainsFunc reports whether at least one +// element e of s satisfies f(e). +func ContainsFunc[S ~[]E, E any](s S, f func(E) bool) bool { + return IndexFunc(s, f) >= 0 +} + +// Clone returns a copy of the slice. +// The elements are copied using assignment, so this is a shallow clone. +// The result may have additional unused capacity. +func Clone[S ~[]E, E any](s S) S { + // Preserve nilness in case it matters. + if s == nil { + return nil + } + // Avoid s[:0:0] as it leads to unwanted liveness when cloning a + // zero-length slice of a large array; see https://go.dev/issue/68488. + return append(S{}, s...) +} diff --git a/slices118/sort.go b/slices118/sort.go new file mode 100644 index 0000000..baa7d99 --- /dev/null +++ b/slices118/sort.go @@ -0,0 +1,168 @@ +package slices118 + +// insertionSortCmpFunc sorts data[a:b] using insertion sort. +func insertionSortCmpFunc[E any](data []E, a, b int, cmp func(a, b E) int) { + for i := a + 1; i < b; i++ { + for j := i; j > a && (cmp(data[j], data[j-1]) < 0); j-- { + data[j], data[j-1] = data[j-1], data[j] + } + } +} + +func swapRangeCmpFunc[E any](data []E, a, b, n int, cmp func(a, b E) int) { + for i := 0; i < n; i++ { + data[a+i], data[b+i] = data[b+i], data[a+i] + } +} + +// rotateCmpFunc rotates two consecutive blocks u = data[a:m] and v = data[m:b] in data: +// Data of the form 'x u v y' is changed to 'x v u y'. +// rotate performs at most b-a many calls to data.Swap, +// and it assumes non-degenerate arguments: a < m && m < b. +func rotateCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + i := m - a + j := b - m + + for i != j { + if i > j { + swapRangeCmpFunc(data, m-i, m, j, cmp) + i -= j + } else { + swapRangeCmpFunc(data, m-i, m+j-i, i, cmp) + j -= i + } + } + // i == j + swapRangeCmpFunc(data, m-i, m, i, cmp) +} + +// symMergeCmpFunc merges the two sorted subsequences data[a:m] and data[m:b] using +// the SymMerge algorithm from Pok-Son Kim and Arne Kutzner, "Stable Minimum +// Storage Merging by Symmetric Comparisons", in Susanne Albers and Tomasz +// Radzik, editors, Algorithms - ESA 2004, volume 3221 of Lecture Notes in +// Computer Science, pages 714-723. Springer, 2004. +// +// Let M = m-a and N = b-n. Wolog M < N. +// The recursion depth is bound by ceil(log(N+M)). +// The algorithm needs O(M*log(N/M + 1)) calls to data.Less. +// The algorithm needs O((M+N)*log(M)) calls to data.Swap. +// +// The paper gives O((M+N)*log(M)) as the number of assignments assuming a +// rotation algorithm which uses O(M+N+gcd(M+N)) assignments. The argumentation +// in the paper carries through for Swap operations, especially as the block +// swapping rotate uses only O(M+N) Swaps. +// +// symMerge assumes non-degenerate arguments: a < m && m < b. +// Having the caller check this condition eliminates many leaf recursion calls, +// which improves performance. +func symMergeCmpFunc[E any](data []E, a, m, b int, cmp func(a, b E) int) { + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[a] into data[m:b] + // if data[a:m] only contains one element. + if m-a == 1 { + // Use binary search to find the lowest index i + // such that data[i] >= data[a] for m <= i < b. + // Exit the search loop with i == b in case no such index exists. + i := m + j := b + for i < j { + h := int(uint(i+j) >> 1) + if cmp(data[h], data[a]) < 0 { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[a] reaches the position before i. + for k := a; k < i-1; k++ { + data[k], data[k+1] = data[k+1], data[k] + } + return + } + + // Avoid unnecessary recursions of symMerge + // by direct insertion of data[m] into data[a:m] + // if data[m:b] only contains one element. + if b-m == 1 { + // Use binary search to find the lowest index i + // such that data[i] > data[m] for a <= i < m. + // Exit the search loop with i == m in case no such index exists. + i := a + j := m + for i < j { + h := int(uint(i+j) >> 1) + if !(cmp(data[m], data[h]) < 0) { + i = h + 1 + } else { + j = h + } + } + // Swap values until data[m] reaches the position i. + for k := m; k > i; k-- { + data[k], data[k-1] = data[k-1], data[k] + } + return + } + + mid := int(uint(a+b) >> 1) + n := mid + m + var start, r int + if m > mid { + start = n - b + r = mid + } else { + start = a + r = m + } + p := n - 1 + + for start < r { + c := int(uint(start+r) >> 1) + if !(cmp(data[p-c], data[c]) < 0) { + start = c + 1 + } else { + r = c + } + } + + end := n - start + if start < m && m < end { + rotateCmpFunc(data, start, m, end, cmp) + } + if a < start && start < mid { + symMergeCmpFunc(data, a, start, mid, cmp) + } + if mid < end && end < b { + symMergeCmpFunc(data, mid, end, b, cmp) + } +} + +func stableCmpFunc[E any](data []E, n int, cmp func(a, b E) int) { + blockSize := 20 // must be > 0 + a, b := 0, blockSize + for b <= n { + insertionSortCmpFunc(data, a, b, cmp) + a = b + b += blockSize + } + insertionSortCmpFunc(data, a, n, cmp) + + for blockSize < n { + a, b = 0, 2*blockSize + for b <= n { + symMergeCmpFunc(data, a, a+blockSize, b, cmp) + a = b + b += 2 * blockSize + } + if m := a + blockSize; m < n { + symMergeCmpFunc(data, a, m, n, cmp) + } + blockSize *= 2 + } +} + +// SortStableFunc sorts the slice x while keeping the original order of equal +// elements, using cmp to compare elements in the same way as [SortFunc]. +func SortStableFunc[S ~[]E, E any](x S, cmp func(a, b E) int) { + stableCmpFunc(x, len(x), cmp) +}