Initial QSfera import
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||
|
||||
cover.out
|
||||
cover.html
|
||||
.vscode
|
||||
|
||||
.idea/
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
version: "2"
|
||||
run:
|
||||
concurrency: 4
|
||||
# also lint _test.go files
|
||||
tests: true
|
||||
timeout: 5m
|
||||
linters:
|
||||
enable:
|
||||
- govet
|
||||
- staticcheck
|
||||
- unused
|
||||
- errcheck
|
||||
- gocritic
|
||||
- gocyclo
|
||||
- revive
|
||||
- ineffassign
|
||||
- unconvert
|
||||
- goconst
|
||||
# - depguard
|
||||
- prealloc
|
||||
# - dupl
|
||||
- misspell
|
||||
- bodyclose
|
||||
- sqlclosecheck
|
||||
- nilerr
|
||||
- nestif
|
||||
- forcetypeassert
|
||||
- exhaustive
|
||||
- funlen
|
||||
# - wsl_v5
|
||||
- testifylint
|
||||
- whitespace
|
||||
- perfsprint
|
||||
- nolintlint
|
||||
- godot
|
||||
- thelper
|
||||
- tparallel
|
||||
- paralleltest
|
||||
- predeclared
|
||||
|
||||
# disable noisy/controversial ones which you might enable later
|
||||
disable:
|
||||
- lll # line length — handled by gofmt/gofumpt
|
||||
|
||||
settings:
|
||||
dupl:
|
||||
threshold: 20 # lower => stricter (tokens)
|
||||
errcheck:
|
||||
check-type-assertions: true
|
||||
funlen:
|
||||
lines: 120
|
||||
statements: 80
|
||||
goconst:
|
||||
min-len: 2
|
||||
min-occurrences: 3
|
||||
gocyclo:
|
||||
min-complexity: 15 # strict; lower => stricter
|
||||
wsl_v5:
|
||||
allow-first-in-block: true
|
||||
allow-whole-block: false
|
||||
branch-max-lines: 2
|
||||
testifylint:
|
||||
disable:
|
||||
- require-error
|
||||
- float-compare
|
||||
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- examples$
|
||||
rules:
|
||||
- linters:
|
||||
- revive
|
||||
text: "^unused-parameter:"
|
||||
- linters:
|
||||
- revive
|
||||
text: "^package-comments:"
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "Error return value of `.*\\.Body\\.Close` is not checked"
|
||||
# linters disabled in tests
|
||||
- linters:
|
||||
- dupl
|
||||
- goconst
|
||||
- funlen
|
||||
path: "_test\\.go$"
|
||||
|
||||
issues:
|
||||
max-issues-per-linter: 0 # 0 = unlimited (we want ALL issues)
|
||||
max-same-issues: 100
|
||||
|
||||
formatters:
|
||||
enable:
|
||||
- gofmt
|
||||
- gofumpt
|
||||
settings:
|
||||
gofumpt:
|
||||
extra-rules: true
|
||||
exclusions:
|
||||
generated: lax
|
||||
paths:
|
||||
- third_party$
|
||||
- builtin$
|
||||
- examples$
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
|
||||
FROM golang:1.23.1
|
||||
|
||||
WORKDIR /go/src/github.com/samber/lo
|
||||
|
||||
COPY Makefile go.* ./
|
||||
|
||||
RUN make tools
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022-2025 Samuel Berthe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
# Only build/test/lint exp/simd when Go version is >= 1.26 (requires goexperiment.simd)
|
||||
GO_VERSION := $(shell go version 2>/dev/null | sed -n 's/.*go\([0-9]*\)\.\([0-9]*\).*/\1.\2/p')
|
||||
GO_SIMD_SUPPORT := $(shell ver="$(GO_VERSION)"; [ -n "$$ver" ] && [ "$$(printf '%s\n1.26\n' "$$ver" | sort -V | tail -1)" = "$$ver" ] && echo yes)
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && GOEXPERIMENT=simd go build -v ./; fi
|
||||
|
||||
test:
|
||||
go test -race ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && GOEXPERIMENT=simd go test -race ./; fi
|
||||
watch-test:
|
||||
reflex -t 50ms -s -- sh -c 'gotest -race ./...'
|
||||
|
||||
bench:
|
||||
go test -v -run=^Benchmark -benchmem -count 3 -bench ./...
|
||||
watch-bench:
|
||||
reflex -t 50ms -s -- sh -c 'go test -v -run=^Benchmark -benchmem -count 3 -bench ./...'
|
||||
|
||||
coverage:
|
||||
go test -v -coverprofile=cover.out -covermode=atomic ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
tools:
|
||||
go install github.com/cespare/reflex@latest
|
||||
go install github.com/rakyll/gotest@latest
|
||||
go install github.com/psampaz/go-mod-outdated@latest
|
||||
go install github.com/jondot/goweight@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go get -t -u golang.org/x/tools/cmd/cover
|
||||
go install github.com/sonatype-nexus-community/nancy@latest
|
||||
go install golang.org/x/perf/cmd/benchstat@latest
|
||||
go install github.com/cespare/prettybench@latest
|
||||
go mod tidy
|
||||
|
||||
# brew install hougesen/tap/mdsf
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && golangci-lint run --timeout 60s --max-same-issues 50 ./; fi
|
||||
# mdsf verify --debug --log-level warn docs/
|
||||
lint-fix:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
|
||||
@if [ -n "$(GO_SIMD_SUPPORT)" ]; then cd ./exp/simd && golangci-lint run --timeout 60s --max-same-issues 50 --fix ./; fi
|
||||
# mdsf format --debug --log-level warn docs/
|
||||
|
||||
audit:
|
||||
go list -json -m all | nancy sleuth
|
||||
|
||||
outdated:
|
||||
go list -u -m -json all | go-mod-outdated -update -direct
|
||||
|
||||
weight:
|
||||
goweight
|
||||
|
||||
doc:
|
||||
cd docs && npm install && npm start
|
||||
+5104
File diff suppressed because it is too large
Load Diff
+304
@@ -0,0 +1,304 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo/internal/xrand"
|
||||
)
|
||||
|
||||
// DispatchingStrategy is a function that distributes messages to channels.
|
||||
type DispatchingStrategy[T any] func(msg T, index uint64, channels []<-chan T) int
|
||||
|
||||
// ChannelDispatcher distributes messages from input channels into N child channels.
|
||||
// Close events are propagated to children.
|
||||
// Underlying channels can have a fixed buffer capacity or be unbuffered when cap is 0.
|
||||
// Play: https://go.dev/play/p/UZGu2wVg3J2
|
||||
func ChannelDispatcher[T any](stream <-chan T, count, channelBufferCap int, strategy DispatchingStrategy[T]) []<-chan T {
|
||||
children := createChannels[T](count, channelBufferCap)
|
||||
|
||||
roChildren := channelsToReadOnly(children)
|
||||
|
||||
go func() {
|
||||
// propagate channel closing to children
|
||||
defer closeChannels(children)
|
||||
|
||||
var i uint64
|
||||
|
||||
for msg := range stream {
|
||||
destination := strategy(msg, i, roChildren) % count
|
||||
children[destination] <- msg
|
||||
|
||||
i++
|
||||
}
|
||||
}()
|
||||
|
||||
return roChildren
|
||||
}
|
||||
|
||||
func createChannels[T any](count, channelBufferCap int) []chan T {
|
||||
children := make([]chan T, 0, count)
|
||||
|
||||
for i := 0; i < count; i++ {
|
||||
children = append(children, make(chan T, channelBufferCap))
|
||||
}
|
||||
|
||||
return children
|
||||
}
|
||||
|
||||
func channelsToReadOnly[T any](children []chan T) []<-chan T {
|
||||
roChildren := make([]<-chan T, 0, len(children))
|
||||
|
||||
for i := range children {
|
||||
roChildren = append(roChildren, children[i])
|
||||
}
|
||||
|
||||
return roChildren
|
||||
}
|
||||
|
||||
func closeChannels[T any](children []chan T) {
|
||||
for i := 0; i < len(children); i++ {
|
||||
close(children[i])
|
||||
}
|
||||
}
|
||||
|
||||
func channelIsNotFull[T any](ch <-chan T) bool {
|
||||
return cap(ch) == 0 || len(ch) < cap(ch)
|
||||
}
|
||||
|
||||
// DispatchingStrategyRoundRobin distributes messages in a rotating sequential manner.
|
||||
// If the channel capacity is exceeded, the next channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/UZGu2wVg3J2
|
||||
func DispatchingStrategyRoundRobin[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := int(index % uint64(len(channels)))
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
index++
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyRandom distributes messages in a random manner.
|
||||
// If the channel capacity is exceeded, another random channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/GEyGn3TdGk4
|
||||
func DispatchingStrategyRandom[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := xrand.IntN(len(channels))
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyWeightedRandom distributes messages in a weighted manner.
|
||||
// If the channel capacity is exceeded, another random channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/v0eMh8NZG2L
|
||||
func DispatchingStrategyWeightedRandom[T any](weights []int) DispatchingStrategy[T] {
|
||||
seq := []int{}
|
||||
|
||||
for i, weight := range weights {
|
||||
for j := 0; j < weight; j++ {
|
||||
seq = append(seq, i)
|
||||
}
|
||||
}
|
||||
|
||||
return func(msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
i := seq[xrand.IntN(len(seq))]
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyFirst distributes messages in the first non-full channel.
|
||||
// If the capacity of the first channel is exceeded, the second channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/OrJCvOmk42f
|
||||
func DispatchingStrategyFirst[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
for {
|
||||
for i := range channels {
|
||||
if channelIsNotFull(channels[i]) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Microsecond) // prevent CPU from burning 🔥
|
||||
}
|
||||
}
|
||||
|
||||
// DispatchingStrategyLeast distributes messages in the emptiest channel.
|
||||
// Play: https://go.dev/play/p/ypy0jrRcEe7
|
||||
func DispatchingStrategyLeast[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
_, i := MinIndexBy(channels, func(a, b <-chan T) bool {
|
||||
return len(a) < len(b)
|
||||
})
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// DispatchingStrategyMost distributes messages in the fullest channel.
|
||||
// If the channel capacity is exceeded, the next channel will be selected and so on.
|
||||
// Play: https://go.dev/play/p/erHHone7rF9
|
||||
func DispatchingStrategyMost[T any](msg T, index uint64, channels []<-chan T) int {
|
||||
_, i := MaxIndexBy(channels, func(a, b <-chan T) bool {
|
||||
return len(a) > len(b) && channelIsNotFull(a)
|
||||
})
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// SliceToChannel returns a read-only channel of collection elements.
|
||||
// Play: https://go.dev/play/p/lIbSY3QmiEg
|
||||
func SliceToChannel[T any](bufferSize int, collection []T) <-chan T {
|
||||
ch := make(chan T, bufferSize)
|
||||
|
||||
go func() {
|
||||
for i := range collection {
|
||||
ch <- collection[i]
|
||||
}
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// ChannelToSlice returns a slice built from channel items. Blocks until channel closes.
|
||||
// Play: https://go.dev/play/p/lIbSY3QmiEg
|
||||
func ChannelToSlice[T any](ch <-chan T) []T {
|
||||
collection := []T{}
|
||||
|
||||
for item := range ch {
|
||||
collection = append(collection, item)
|
||||
}
|
||||
|
||||
return collection
|
||||
}
|
||||
|
||||
// Generator implements the generator design pattern.
|
||||
// Play: https://go.dev/play/p/lIbSY3QmiEg
|
||||
//
|
||||
// Deprecated: use "iter" package instead (Go >= 1.23).
|
||||
func Generator[T any](bufferSize int, generator func(yield func(T))) <-chan T {
|
||||
ch := make(chan T, bufferSize)
|
||||
|
||||
go func() {
|
||||
// WARNING: infinite loop
|
||||
generator(func(t T) {
|
||||
ch <- t
|
||||
})
|
||||
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
return ch
|
||||
}
|
||||
|
||||
// Buffer creates a slice of n elements from a channel. Returns the slice and the slice length.
|
||||
// @TODO: we should probably provide a helper that reuses the same buffer.
|
||||
// Play: https://go.dev/play/p/gPQ-6xmcKQI
|
||||
func Buffer[T any](ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
buffer := make([]T, 0, size)
|
||||
now := time.Now()
|
||||
|
||||
for index := 0; index < size; index++ {
|
||||
item, ok := <-ch
|
||||
if !ok {
|
||||
return buffer, index, time.Since(now), false
|
||||
}
|
||||
|
||||
buffer = append(buffer, item)
|
||||
}
|
||||
|
||||
return buffer, size, time.Since(now), true
|
||||
}
|
||||
|
||||
// BufferWithContext creates a slice of n elements from a channel, with context. Returns the slice and the slice length.
|
||||
// @TODO: we should probably provide a helper that reuses the same buffer.
|
||||
// Play: https://go.dev/play/p/oRfOyJWK9YF
|
||||
func BufferWithContext[T any](ctx context.Context, ch <-chan T, size int) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
buffer := make([]T, 0, size)
|
||||
now := time.Now()
|
||||
|
||||
for index := 0; index < size; index++ {
|
||||
select {
|
||||
case item, ok := <-ch:
|
||||
if !ok {
|
||||
return buffer, index, time.Since(now), false
|
||||
}
|
||||
|
||||
buffer = append(buffer, item)
|
||||
|
||||
case <-ctx.Done():
|
||||
return buffer, index, time.Since(now), true
|
||||
}
|
||||
}
|
||||
|
||||
return buffer, size, time.Since(now), true
|
||||
}
|
||||
|
||||
// BufferWithTimeout creates a slice of n elements from a channel, with timeout. Returns the slice and the slice length.
|
||||
// Play: https://go.dev/play/p/sxyEM3koo4n
|
||||
func BufferWithTimeout[T any](ch <-chan T, size int, timeout time.Duration) (collection []T, length int, readTime time.Duration, ok bool) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
return BufferWithContext(ctx, ch, size)
|
||||
}
|
||||
|
||||
// FanIn collects messages from multiple input channels into a single buffered channel.
|
||||
// Output messages have no priority. When all upstream channels reach EOF, downstream channel closes.
|
||||
// Play: https://go.dev/play/p/FH8Wq-T04Jb
|
||||
func FanIn[T any](channelBufferCap int, upstreams ...<-chan T) <-chan T {
|
||||
out := make(chan T, channelBufferCap)
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Start an output goroutine for each input channel in upstreams.
|
||||
wg.Add(len(upstreams))
|
||||
for i := range upstreams {
|
||||
go func(index int) {
|
||||
for n := range upstreams[index] {
|
||||
out <- n
|
||||
}
|
||||
wg.Done()
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Start a goroutine to close out once all the output goroutines are done.
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(out)
|
||||
}()
|
||||
return out
|
||||
}
|
||||
|
||||
// FanOut broadcasts all the upstream messages to multiple downstream channels.
|
||||
// When upstream channel reaches EOF, downstream channels close. If any downstream
|
||||
// channels is full, broadcasting is paused.
|
||||
// Play: https://go.dev/play/p/2LHxcjKX23L
|
||||
func FanOut[T any](count, channelsBufferCap int, upstream <-chan T) []<-chan T {
|
||||
downstreams := createChannels[T](count, channelsBufferCap)
|
||||
|
||||
go func() {
|
||||
for msg := range upstream {
|
||||
for i := range downstreams {
|
||||
downstreams[i] <- msg
|
||||
}
|
||||
}
|
||||
|
||||
// Close out once all the output goroutines are done.
|
||||
for i := range downstreams {
|
||||
close(downstreams[i])
|
||||
}
|
||||
}()
|
||||
|
||||
return channelsToReadOnly(downstreams)
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type synchronize struct {
|
||||
locker sync.Locker
|
||||
}
|
||||
|
||||
func (s *synchronize) Do(callback func()) {
|
||||
s.locker.Lock()
|
||||
Try0(callback)
|
||||
s.locker.Unlock()
|
||||
}
|
||||
|
||||
// Synchronize wraps the underlying callback in a mutex. It receives an optional mutex.
|
||||
// Play: https://go.dev/play/p/X3cqROSpQmu
|
||||
func Synchronize(opt ...sync.Locker) *synchronize { //nolint:revive
|
||||
if len(opt) > 1 {
|
||||
panic("lo.Synchronize: unexpected arguments")
|
||||
} else if len(opt) == 0 {
|
||||
opt = append(opt, &sync.Mutex{})
|
||||
}
|
||||
|
||||
return &synchronize{
|
||||
locker: opt[0],
|
||||
}
|
||||
}
|
||||
|
||||
// Async executes a function in a goroutine and returns the result in a channel.
|
||||
// Play: https://go.dev/play/p/uo35gosuTLw
|
||||
func Async[A any](f func() A) <-chan A {
|
||||
ch := make(chan A, 1)
|
||||
go func() {
|
||||
ch <- f()
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async0 executes a function in a goroutine and returns a channel set once the function finishes.
|
||||
// Play: https://go.dev/play/p/tNqf1cClG_o
|
||||
func Async0(f func()) <-chan struct{} {
|
||||
ch := make(chan struct{}, 1)
|
||||
go func() {
|
||||
f()
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async1 is an alias to Async.
|
||||
// Play: https://go.dev/play/p/RBQWtIn4PsF
|
||||
func Async1[A any](f func() A) <-chan A {
|
||||
return Async(f)
|
||||
}
|
||||
|
||||
// Async2 has the same behavior as Async, but returns the 2 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/5SzzDjssXOH
|
||||
func Async2[A, B any](f func() (A, B)) <-chan Tuple2[A, B] {
|
||||
ch := make(chan Tuple2[A, B], 1)
|
||||
go func() {
|
||||
ch <- T2(f())
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async3 has the same behavior as Async, but returns the 3 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/cZpZsDXNmlx
|
||||
func Async3[A, B, C any](f func() (A, B, C)) <-chan Tuple3[A, B, C] {
|
||||
ch := make(chan Tuple3[A, B, C], 1)
|
||||
go func() {
|
||||
ch <- T3(f())
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async4 has the same behavior as Async, but returns the 4 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/9X5O2VrLzkR
|
||||
func Async4[A, B, C, D any](f func() (A, B, C, D)) <-chan Tuple4[A, B, C, D] {
|
||||
ch := make(chan Tuple4[A, B, C, D], 1)
|
||||
go func() {
|
||||
ch <- T4(f())
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async5 has the same behavior as Async, but returns the 5 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/MqnUJpkmopA
|
||||
func Async5[A, B, C, D, E any](f func() (A, B, C, D, E)) <-chan Tuple5[A, B, C, D, E] {
|
||||
ch := make(chan Tuple5[A, B, C, D, E], 1)
|
||||
go func() {
|
||||
ch <- T5(f())
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// Async6 has the same behavior as Async, but returns the 6 results as a tuple inside the channel.
|
||||
// Play: https://go.dev/play/p/kM1X67JPdSP
|
||||
func Async6[A, B, C, D, E, F any](f func() (A, B, C, D, E, F)) <-chan Tuple6[A, B, C, D, E, F] {
|
||||
ch := make(chan Tuple6[A, B, C, D, E, F], 1)
|
||||
go func() {
|
||||
ch <- T6(f())
|
||||
}()
|
||||
return ch
|
||||
}
|
||||
|
||||
// WaitFor runs periodically until a condition is validated.
|
||||
// Play: https://go.dev/play/p/t_wTDmubbK3
|
||||
func WaitFor(condition func(i int) bool, timeout, heartbeatDelay time.Duration) (totalIterations int, elapsed time.Duration, conditionFound bool) {
|
||||
conditionWithContext := func(_ context.Context, currentIteration int) bool {
|
||||
return condition(currentIteration)
|
||||
}
|
||||
return WaitForWithContext(context.Background(), conditionWithContext, timeout, heartbeatDelay)
|
||||
}
|
||||
|
||||
// WaitForWithContext runs periodically until a condition is validated or context is canceled.
|
||||
// Play: https://go.dev/play/p/t_wTDmubbK3
|
||||
func WaitForWithContext(ctx context.Context, condition func(ctx context.Context, currentIteration int) bool, timeout, heartbeatDelay time.Duration) (totalIterations int, elapsed time.Duration, conditionFound bool) {
|
||||
start := time.Now()
|
||||
|
||||
if ctx.Err() != nil {
|
||||
return totalIterations, time.Since(start), false
|
||||
}
|
||||
|
||||
ctx, cleanCtx := context.WithTimeout(ctx, timeout)
|
||||
ticker := time.NewTicker(heartbeatDelay)
|
||||
|
||||
defer func() {
|
||||
cleanCtx()
|
||||
ticker.Stop()
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return totalIterations, time.Since(start), false
|
||||
case <-ticker.C:
|
||||
totalIterations++
|
||||
if condition(ctx, totalIterations-1) {
|
||||
return totalIterations, time.Since(start), true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package lo
|
||||
|
||||
// Ternary is a single line if/else statement.
|
||||
// Take care to avoid dereferencing potentially nil pointers in your A/B expressions, because they are both evaluated. See TernaryF to avoid this problem.
|
||||
// Play: https://go.dev/play/p/t-D7WBL44h2
|
||||
func Ternary[T any](condition bool, ifOutput, elseOutput T) T {
|
||||
if condition {
|
||||
return ifOutput
|
||||
}
|
||||
|
||||
return elseOutput
|
||||
}
|
||||
|
||||
// TernaryF is a single line if/else statement whose options are functions.
|
||||
// Play: https://go.dev/play/p/AO4VW20JoqM
|
||||
func TernaryF[T any](condition bool, ifFunc, elseFunc func() T) T {
|
||||
if condition {
|
||||
return ifFunc()
|
||||
}
|
||||
|
||||
return elseFunc()
|
||||
}
|
||||
|
||||
type ifElse[T any] struct {
|
||||
result T
|
||||
done bool
|
||||
}
|
||||
|
||||
// If is a single line if/else statement.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func If[T any](condition bool, result T) *ifElse[T] { //nolint:revive
|
||||
if condition {
|
||||
return &ifElse[T]{result, true}
|
||||
}
|
||||
|
||||
var t T
|
||||
return &ifElse[T]{t, false}
|
||||
}
|
||||
|
||||
// IfF is a single line if/else statement whose options are functions.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func IfF[T any](condition bool, resultF func() T) *ifElse[T] { //nolint:revive
|
||||
if condition {
|
||||
return &ifElse[T]{resultF(), true}
|
||||
}
|
||||
|
||||
var t T
|
||||
return &ifElse[T]{t, false}
|
||||
}
|
||||
|
||||
// ElseIf.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func (i *ifElse[T]) ElseIf(condition bool, result T) *ifElse[T] {
|
||||
if !i.done && condition {
|
||||
i.result = result
|
||||
i.done = true
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// ElseIfF.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func (i *ifElse[T]) ElseIfF(condition bool, resultF func() T) *ifElse[T] {
|
||||
if !i.done && condition {
|
||||
i.result = resultF()
|
||||
i.done = true
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Else.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func (i *ifElse[T]) Else(result T) T {
|
||||
if i.done {
|
||||
return i.result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ElseF.
|
||||
// Play: https://go.dev/play/p/WSw3ApMxhyW
|
||||
func (i *ifElse[T]) ElseF(resultF func() T) T {
|
||||
if i.done {
|
||||
return i.result
|
||||
}
|
||||
|
||||
return resultF()
|
||||
}
|
||||
|
||||
type switchCase[T comparable, R any] struct {
|
||||
predicate T
|
||||
result R
|
||||
done bool
|
||||
}
|
||||
|
||||
// Switch is a pure functional switch/case/default statement.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func Switch[T comparable, R any](predicate T) *switchCase[T, R] { //nolint:revive
|
||||
var result R
|
||||
|
||||
return &switchCase[T, R]{
|
||||
predicate,
|
||||
result,
|
||||
false,
|
||||
}
|
||||
}
|
||||
|
||||
// Case.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) Case(val T, result R) *switchCase[T, R] {
|
||||
if !s.done && s.predicate == val {
|
||||
s.result = result
|
||||
s.done = true
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// CaseF.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) CaseF(val T, callback func() R) *switchCase[T, R] {
|
||||
if !s.done && s.predicate == val {
|
||||
s.result = callback()
|
||||
s.done = true
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// Default.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) Default(result R) R {
|
||||
if !s.done {
|
||||
s.result = result
|
||||
}
|
||||
|
||||
return s.result
|
||||
}
|
||||
|
||||
// DefaultF.
|
||||
// Play: https://go.dev/play/p/TGbKUMAeRUd
|
||||
func (s *switchCase[T, R]) DefaultF(callback func() R) R {
|
||||
if !s.done {
|
||||
s.result = callback()
|
||||
}
|
||||
|
||||
return s.result
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package lo
|
||||
|
||||
// Clonable defines a constraint of types having Clone() T method.
|
||||
type Clonable[T any] interface {
|
||||
Clone() T
|
||||
}
|
||||
+380
@@ -0,0 +1,380 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
const defaultAssertionFailureMessage = "assertion failed"
|
||||
|
||||
// Validate is a helper that creates an error when a condition is not met.
|
||||
// Play: https://go.dev/play/p/vPyh51XpCBt
|
||||
func Validate(ok bool, format string, args ...any) error {
|
||||
if !ok {
|
||||
return fmt.Errorf(format, args...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func messageFromMsgAndArgs(msgAndArgs ...any) string {
|
||||
if len(msgAndArgs) == 1 {
|
||||
if msgAsStr, ok := msgAndArgs[0].(string); ok {
|
||||
return msgAsStr
|
||||
}
|
||||
return fmt.Sprintf("%+v", msgAndArgs[0])
|
||||
}
|
||||
if len(msgAndArgs) > 1 {
|
||||
return fmt.Sprintf(msgAndArgs[0].(string), msgAndArgs[1:]...) //nolint:errcheck,forcetypeassert
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// MustChecker panics if err is error or false.
|
||||
var MustChecker = func(err any, messageArgs ...any) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch e := err.(type) {
|
||||
case bool:
|
||||
if !e {
|
||||
message := messageFromMsgAndArgs(messageArgs...)
|
||||
if message == "" {
|
||||
message = "not ok"
|
||||
}
|
||||
|
||||
panic(message)
|
||||
}
|
||||
|
||||
case error:
|
||||
message := messageFromMsgAndArgs(messageArgs...)
|
||||
if message != "" {
|
||||
panic(message + ": " + e.Error())
|
||||
}
|
||||
panic(e.Error())
|
||||
|
||||
default:
|
||||
panic("must: invalid err type '" + reflect.TypeOf(err).Name() + "', should either be a bool or an error")
|
||||
}
|
||||
}
|
||||
|
||||
// Must is a helper that wraps a call to a function returning a value and an error
|
||||
// and panics if err is error or false.
|
||||
// Play: https://go.dev/play/p/fOqtX5HudtN
|
||||
func Must[T any](val T, err any, messageArgs ...any) T {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val
|
||||
}
|
||||
|
||||
// Must0 has the same behavior as Must, but callback returns no variable.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must0(err any, messageArgs ...any) {
|
||||
MustChecker(err, messageArgs...)
|
||||
}
|
||||
|
||||
// Must1 is an alias to Must.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must1[T any](val T, err any, messageArgs ...any) T {
|
||||
return Must(val, err, messageArgs...)
|
||||
}
|
||||
|
||||
// Must2 has the same behavior as Must, but callback returns 2 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must2[T1, T2 any](val1 T1, val2 T2, err any, messageArgs ...any) (T1, T2) {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2
|
||||
}
|
||||
|
||||
// Must3 has the same behavior as Must, but callback returns 3 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must3[T1, T2, T3 any](val1 T1, val2 T2, val3 T3, err any, messageArgs ...any) (T1, T2, T3) {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3
|
||||
}
|
||||
|
||||
// Must4 has the same behavior as Must, but callback returns 4 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must4[T1, T2, T3, T4 any](val1 T1, val2 T2, val3 T3, val4 T4, err any, messageArgs ...any) (T1, T2, T3, T4) {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4
|
||||
}
|
||||
|
||||
// Must5 has the same behavior as Must, but callback returns 5 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must5[T1, T2, T3, T4, T5 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, err any, messageArgs ...any) (T1, T2, T3, T4, T5) {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4, val5
|
||||
}
|
||||
|
||||
// Must6 has the same behavior as Must, but callback returns 6 variables.
|
||||
// Play: https://go.dev/play/p/TMoWrRp3DyC
|
||||
func Must6[T1, T2, T3, T4, T5, T6 any](val1 T1, val2 T2, val3 T3, val4 T4, val5 T5, val6 T6, err any, messageArgs ...any) (T1, T2, T3, T4, T5, T6) {
|
||||
MustChecker(err, messageArgs...)
|
||||
return val1, val2, val3, val4, val5, val6
|
||||
}
|
||||
|
||||
// Try calls the function and return false in case of error.
|
||||
func Try(callback func() error) (ok bool) {
|
||||
ok = true
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ok = false
|
||||
}
|
||||
}()
|
||||
|
||||
err := callback()
|
||||
if err != nil {
|
||||
ok = false
|
||||
}
|
||||
|
||||
return ok
|
||||
}
|
||||
|
||||
// Try0 has the same behavior as Try, but callback returns no variable.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try0(callback func()) bool {
|
||||
return Try(func() error {
|
||||
callback()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// Try1 is an alias to Try.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try1(callback func() error) bool {
|
||||
return Try(callback)
|
||||
}
|
||||
|
||||
// Try2 has the same behavior as Try, but callback returns 2 variables.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try2[T any](callback func() (T, error)) bool {
|
||||
return Try(func() error {
|
||||
_, err := callback()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Try3 has the same behavior as Try, but callback returns 3 variables.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try3[T, R any](callback func() (T, R, error)) bool {
|
||||
return Try(func() error {
|
||||
_, _, err := callback()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Try4 has the same behavior as Try, but callback returns 4 variables.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try4[T, R, S any](callback func() (T, R, S, error)) bool {
|
||||
return Try(func() error {
|
||||
_, _, _, err := callback()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Try5 has the same behavior as Try, but callback returns 5 variables.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try5[T, R, S, Q any](callback func() (T, R, S, Q, error)) bool {
|
||||
return Try(func() error {
|
||||
_, _, _, _, err := callback()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// Try6 has the same behavior as Try, but callback returns 6 variables.
|
||||
// Play: https://go.dev/play/p/mTyyWUvn9u4
|
||||
func Try6[T, R, S, Q, U any](callback func() (T, R, S, Q, U, error)) bool {
|
||||
return Try(func() error {
|
||||
_, _, _, _, _, err := callback()
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
// TryOr has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr[A any](callback func() (A, error), fallbackA A) (A, bool) {
|
||||
return TryOr1(callback, fallbackA)
|
||||
}
|
||||
|
||||
// TryOr1 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr1[A any](callback func() (A, error), fallbackA A) (A, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, ok
|
||||
}
|
||||
|
||||
// TryOr2 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr2[A, B any](callback func() (A, B, error), fallbackA A, fallbackB B) (A, B, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, b, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
fallbackB = b
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, fallbackB, ok
|
||||
}
|
||||
|
||||
// TryOr3 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr3[A, B, C any](callback func() (A, B, C, error), fallbackA A, fallbackB B, fallbackC C) (A, B, C, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, b, c, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
fallbackB = b
|
||||
fallbackC = c
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, fallbackB, fallbackC, ok
|
||||
}
|
||||
|
||||
// TryOr4 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr4[A, B, C, D any](callback func() (A, B, C, D, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D) (A, B, C, D, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, b, c, d, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
fallbackB = b
|
||||
fallbackC = c
|
||||
fallbackD = d
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, fallbackB, fallbackC, fallbackD, ok
|
||||
}
|
||||
|
||||
// TryOr5 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr5[A, B, C, D, E any](callback func() (A, B, C, D, E, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E) (A, B, C, D, E, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, b, c, d, e, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
fallbackB = b
|
||||
fallbackC = c
|
||||
fallbackD = d
|
||||
fallbackE = e
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, ok
|
||||
}
|
||||
|
||||
// TryOr6 has the same behavior as Must, but returns a default value in case of error.
|
||||
// Play: https://go.dev/play/p/B4F7Wg2Zh9X
|
||||
func TryOr6[A, B, C, D, E, F any](callback func() (A, B, C, D, E, F, error), fallbackA A, fallbackB B, fallbackC C, fallbackD D, fallbackE E, fallbackF F) (A, B, C, D, E, F, bool) {
|
||||
ok := false
|
||||
|
||||
Try0(func() {
|
||||
a, b, c, d, e, f, err := callback()
|
||||
if err == nil {
|
||||
fallbackA = a
|
||||
fallbackB = b
|
||||
fallbackC = c
|
||||
fallbackD = d
|
||||
fallbackE = e
|
||||
fallbackF = f
|
||||
ok = true
|
||||
}
|
||||
})
|
||||
|
||||
return fallbackA, fallbackB, fallbackC, fallbackD, fallbackE, fallbackF, ok
|
||||
}
|
||||
|
||||
// TryWithErrorValue has the same behavior as Try, but also returns value passed to panic.
|
||||
// Play: https://go.dev/play/p/Kc7afQIT2Fs
|
||||
func TryWithErrorValue(callback func() error) (errorValue any, ok bool) {
|
||||
ok = true
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
ok = false
|
||||
errorValue = r
|
||||
}
|
||||
}()
|
||||
|
||||
err := callback()
|
||||
if err != nil {
|
||||
ok = false
|
||||
errorValue = err
|
||||
}
|
||||
|
||||
return errorValue, ok
|
||||
}
|
||||
|
||||
// TryCatch has the same behavior as Try, but calls the catch function in case of error.
|
||||
// Play: https://go.dev/play/p/PnOON-EqBiU
|
||||
func TryCatch(callback func() error, catch func()) {
|
||||
if !Try(callback) {
|
||||
catch()
|
||||
}
|
||||
}
|
||||
|
||||
// TryCatchWithErrorValue has the same behavior as TryWithErrorValue, but calls the catch function in case of error.
|
||||
// Play: https://go.dev/play/p/8Pc9gwX_GZO
|
||||
func TryCatchWithErrorValue(callback func() error, catch func(any)) {
|
||||
if err, ok := TryWithErrorValue(callback); !ok {
|
||||
catch(err)
|
||||
}
|
||||
}
|
||||
|
||||
// ErrorsAs is a shortcut for errors.As(err, &&T).
|
||||
// Play: https://go.dev/play/p/8wk5rH8UfrE
|
||||
func ErrorsAs[T error](err error) (T, bool) {
|
||||
var t T
|
||||
ok := errors.As(err, &t)
|
||||
return t, ok
|
||||
}
|
||||
|
||||
// Assert does nothing when the condition is true, otherwise it panics with an optional message.
|
||||
// Play: https://go.dev/play/p/Xv8LLKBMNwI
|
||||
var Assert = func(condition bool, message ...string) {
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
|
||||
panicMessage := defaultAssertionFailureMessage
|
||||
if len(message) > 0 {
|
||||
panicMessage = fmt.Sprintf("%s: %s", defaultAssertionFailureMessage, message[0])
|
||||
}
|
||||
panic(panicMessage)
|
||||
}
|
||||
|
||||
// Assertf does nothing when the condition is true, otherwise it panics with a formatted message.
|
||||
// Play: https://go.dev/play/p/TVPEmVcyrdY
|
||||
var Assertf = func(condition bool, format string, args ...any) {
|
||||
if condition {
|
||||
return
|
||||
}
|
||||
|
||||
panicMessage := fmt.Sprintf("%s: %s", defaultAssertionFailureMessage, fmt.Sprintf(format, args...))
|
||||
panic(panicMessage)
|
||||
}
|
||||
+995
@@ -0,0 +1,995 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo/internal/constraints"
|
||||
"github.com/samber/lo/internal/xrand"
|
||||
)
|
||||
|
||||
// IndexOf returns the index at which the first occurrence of a value is found in a slice or -1
|
||||
// if the value cannot be found.
|
||||
// Play: https://go.dev/play/p/Eo7W0lvKTky
|
||||
func IndexOf[T comparable](collection []T, element T) int {
|
||||
for i := range collection {
|
||||
if collection[i] == element {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// LastIndexOf returns the index at which the last occurrence of a value is found in a slice or -1
|
||||
// if the value cannot be found.
|
||||
// Play: https://go.dev/play/p/Eo7W0lvKTky
|
||||
func LastIndexOf[T comparable](collection []T, element T) int {
|
||||
length := len(collection)
|
||||
|
||||
for i := length - 1; i >= 0; i-- {
|
||||
if collection[i] == element {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
// HasPrefix returns true if the collection has the prefix.
|
||||
// Play: https://go.dev/play/p/SrljzVDpMQM
|
||||
func HasPrefix[T comparable](collection, prefix []T) bool {
|
||||
if len(collection) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range prefix {
|
||||
if collection[i] != prefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// HasSuffix returns true if the collection has the suffix.
|
||||
// Play: https://go.dev/play/p/bJeLetQNAON
|
||||
func HasSuffix[T comparable](collection, suffix []T) bool {
|
||||
if len(collection) < len(suffix) {
|
||||
return false
|
||||
}
|
||||
|
||||
for i := range suffix {
|
||||
if collection[len(collection)-len(suffix)+i] != suffix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Find searches for an element in a slice based on a predicate. Returns element and true if element was found.
|
||||
// Play: https://go.dev/play/p/Eo7W0lvKTky
|
||||
func Find[T any](collection []T, predicate func(item T) bool) (T, bool) {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return collection[i], true
|
||||
}
|
||||
}
|
||||
|
||||
var result T
|
||||
return result, false
|
||||
}
|
||||
|
||||
// FindErr searches for an element in a slice based on a predicate that can return an error.
|
||||
// Returns the element and nil error if the element is found.
|
||||
// Returns zero value and nil error if the element is not found.
|
||||
// If the predicate returns an error, iteration stops immediately and returns zero value and the error.
|
||||
func FindErr[T any](collection []T, predicate func(item T) (bool, error)) (T, error) {
|
||||
for i := range collection {
|
||||
matches, err := predicate(collection[i])
|
||||
if err != nil {
|
||||
var result T
|
||||
return result, err
|
||||
}
|
||||
if matches {
|
||||
return collection[i], nil
|
||||
}
|
||||
}
|
||||
|
||||
var result T
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FindIndexOf searches for an element in a slice based on a predicate and returns the index and true.
|
||||
// Returns -1 and false if the element is not found.
|
||||
// Play: https://go.dev/play/p/XWSEM4Ic_t0
|
||||
func FindIndexOf[T any](collection []T, predicate func(item T) bool) (T, int, bool) {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return collection[i], i, true
|
||||
}
|
||||
}
|
||||
|
||||
var result T
|
||||
return result, -1, false
|
||||
}
|
||||
|
||||
// FindLastIndexOf searches for the last element in a slice based on a predicate and returns the index and true.
|
||||
// Returns -1 and false if the element is not found.
|
||||
// Play: https://go.dev/play/p/dPiMRtJ6cUx
|
||||
func FindLastIndexOf[T any](collection []T, predicate func(item T) bool) (T, int, bool) {
|
||||
length := len(collection)
|
||||
|
||||
for i := length - 1; i >= 0; i-- {
|
||||
if predicate(collection[i]) {
|
||||
return collection[i], i, true
|
||||
}
|
||||
}
|
||||
|
||||
var result T
|
||||
return result, -1, false
|
||||
}
|
||||
|
||||
// FindOrElse searches for an element in a slice based on a predicate. Returns the element if found or a given fallback value otherwise.
|
||||
// Play: https://go.dev/play/p/Eo7W0lvKTky
|
||||
func FindOrElse[T any](collection []T, fallback T, predicate func(item T) bool) T {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return collection[i]
|
||||
}
|
||||
}
|
||||
|
||||
return fallback
|
||||
}
|
||||
|
||||
// FindKey returns the key of the first value matching.
|
||||
// Play: https://go.dev/play/p/Bg0w1VDPYXx
|
||||
func FindKey[K, V comparable](object map[K]V, value V) (K, bool) {
|
||||
for k, v := range object {
|
||||
if v == value {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
|
||||
return Empty[K](), false
|
||||
}
|
||||
|
||||
// FindKeyBy returns the key of the first element predicate returns true for.
|
||||
// Play: https://go.dev/play/p/9IbiPElcyo8
|
||||
func FindKeyBy[K comparable, V any](object map[K]V, predicate func(key K, value V) bool) (K, bool) {
|
||||
for k, v := range object {
|
||||
if predicate(k, v) {
|
||||
return k, true
|
||||
}
|
||||
}
|
||||
|
||||
return Empty[K](), false
|
||||
}
|
||||
|
||||
// FindUniques returns a slice with all the elements that appear in the collection only once.
|
||||
// The order of result values is determined by the order they occur in the collection.
|
||||
func FindUniques[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
isDupl := make(map[T]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
duplicated, seen := isDupl[collection[i]]
|
||||
if !duplicated {
|
||||
isDupl[collection[i]] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(isDupl)-duplicates)
|
||||
|
||||
for i := range collection {
|
||||
if duplicated := isDupl[collection[i]]; !duplicated {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindUniquesBy returns a slice with all the elements that appear in the collection only once.
|
||||
// The order of result values is determined by the order they occur in the slice. It accepts `iteratee` which is
|
||||
// invoked for each element in the slice to generate the criterion by which uniqueness is computed.
|
||||
func FindUniquesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(isDupl)-duplicates)
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
if duplicated := isDupl[key]; !duplicated {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindDuplicates returns a slice with the first occurrence of each duplicated element in the collection.
|
||||
// The order of result values is determined by the order they occur in the collection.
|
||||
func FindDuplicates[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
isDupl := make(map[T]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
duplicated, seen := isDupl[collection[i]]
|
||||
if !duplicated {
|
||||
isDupl[collection[i]] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
for i := range collection {
|
||||
if duplicated := isDupl[collection[i]]; duplicated {
|
||||
result = append(result, collection[i])
|
||||
isDupl[collection[i]] = false
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindDuplicatesBy returns a slice with the first occurrence of each duplicated element in the collection.
|
||||
// The order of result values is determined by the order they occur in the slice. It accepts `iteratee` which is
|
||||
// invoked for each element in the slice to generate the criterion by which uniqueness is computed.
|
||||
func FindDuplicatesBy[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) U) Slice {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
for i := range collection {
|
||||
key := iteratee(collection[i])
|
||||
|
||||
if duplicated := isDupl[key]; duplicated {
|
||||
result = append(result, collection[i])
|
||||
isDupl[key] = false
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FindDuplicatesByErr returns a slice with the first occurrence of each duplicated element in the collection.
|
||||
// The order of result values is determined by the order they occur in the slice. It accepts `iteratee` which is
|
||||
// invoked for each element in the slice to generate the criterion by which uniqueness is computed.
|
||||
// If the iteratee returns an error, iteration stops immediately and the error is returned with a nil slice.
|
||||
func FindDuplicatesByErr[T any, U comparable, Slice ~[]T](collection Slice, iteratee func(item T) (U, error)) (Slice, error) {
|
||||
isDupl := make(map[U]bool, len(collection))
|
||||
|
||||
duplicates := 0
|
||||
|
||||
// First pass: identify duplicates
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
var result Slice
|
||||
return result, err
|
||||
}
|
||||
|
||||
duplicated, seen := isDupl[key]
|
||||
if !duplicated {
|
||||
isDupl[key] = seen
|
||||
|
||||
if seen {
|
||||
duplicates++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, duplicates)
|
||||
|
||||
// Second pass: collect first occurrences of duplicates
|
||||
for i := range collection {
|
||||
key, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
var result Slice
|
||||
return result, err
|
||||
}
|
||||
|
||||
if duplicated := isDupl[key]; duplicated {
|
||||
result = append(result, collection[i])
|
||||
isDupl[key] = false
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Min search the minimum value of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
// Play: https://go.dev/play/p/r6e-Z8JozS8
|
||||
func Min[T constraints.Ordered](collection []T) T {
|
||||
var mIn T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if item < mIn {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
|
||||
return mIn
|
||||
}
|
||||
|
||||
// MinIndex search the minimum value of a collection and the index of the minimum value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
func MinIndex[T constraints.Ordered](collection []T) (T, int) {
|
||||
var (
|
||||
mIn T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, -1
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if item < mIn {
|
||||
mIn = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, index
|
||||
}
|
||||
|
||||
// MinBy search the minimum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns zero value when the collection is empty.
|
||||
func MinBy[T any](collection []T, less func(a, b T) bool) T {
|
||||
var mIn T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if less(item, mIn) {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
|
||||
return mIn
|
||||
}
|
||||
|
||||
// MinByErr search the minimum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
func MinByErr[T any](collection []T, less func(a, b T) (bool, error)) (T, error) {
|
||||
var mIn T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, nil
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isLess, err := less(item, mIn)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, err
|
||||
}
|
||||
if isLess {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, nil
|
||||
}
|
||||
|
||||
// MinIndexBy search the minimum value of a collection using the given comparison function and the index of the minimum value.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
func MinIndexBy[T any](collection []T, less func(a, b T) bool) (T, int) {
|
||||
var (
|
||||
mIn T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, -1
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if less(item, mIn) {
|
||||
mIn = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, index
|
||||
}
|
||||
|
||||
// MinIndexByErr search the minimum value of a collection using the given comparison function and the index of the minimum value.
|
||||
// If several values of the collection are equal to the smallest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
// Comparison function can return an error to stop iteration immediately.
|
||||
func MinIndexByErr[T any](collection []T, less func(a, b T) (bool, error)) (T, int, error) {
|
||||
var (
|
||||
mIn T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mIn, -1, nil
|
||||
}
|
||||
|
||||
mIn = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isLess, err := less(item, mIn)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, -1, err
|
||||
}
|
||||
|
||||
if isLess {
|
||||
mIn = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mIn, index, nil
|
||||
}
|
||||
|
||||
// Earliest search the minimum time.Time of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
func Earliest(times ...time.Time) time.Time {
|
||||
var mIn time.Time
|
||||
|
||||
if len(times) == 0 {
|
||||
return mIn
|
||||
}
|
||||
|
||||
mIn = times[0]
|
||||
|
||||
for i := 1; i < len(times); i++ {
|
||||
item := times[i]
|
||||
|
||||
if item.Before(mIn) {
|
||||
mIn = item
|
||||
}
|
||||
}
|
||||
|
||||
return mIn
|
||||
}
|
||||
|
||||
// EarliestBy search the minimum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value when the collection is empty.
|
||||
func EarliestBy[T any](collection []T, iteratee func(item T) time.Time) T {
|
||||
var earliest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return earliest
|
||||
}
|
||||
|
||||
earliest = collection[0]
|
||||
earliestTime := iteratee(collection[0])
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime := iteratee(collection[i])
|
||||
|
||||
if itemTime.Before(earliestTime) {
|
||||
earliest = collection[i]
|
||||
earliestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return earliest
|
||||
}
|
||||
|
||||
// EarliestByErr search the minimum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
func EarliestByErr[T any](collection []T, iteratee func(item T) (time.Time, error)) (T, error) {
|
||||
var earliest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
earliestTime, err := iteratee(collection[0])
|
||||
if err != nil {
|
||||
return earliest, err
|
||||
}
|
||||
earliest = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return earliest, err
|
||||
}
|
||||
|
||||
if itemTime.Before(earliestTime) {
|
||||
earliest = collection[i]
|
||||
earliestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return earliest, nil
|
||||
}
|
||||
|
||||
// Max searches the maximum value of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
// Play: https://go.dev/play/p/r6e-Z8JozS8
|
||||
func Max[T constraints.Ordered](collection []T) T {
|
||||
var mAx T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if item > mAx {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
|
||||
return mAx
|
||||
}
|
||||
|
||||
// MaxIndex searches the maximum value of a collection and the index of the maximum value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
func MaxIndex[T constraints.Ordered](collection []T) (T, int) {
|
||||
var (
|
||||
mAx T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, -1
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if item > mAx {
|
||||
mAx = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, index
|
||||
}
|
||||
|
||||
// MaxBy search the maximum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns zero value when the collection is empty.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
//
|
||||
// Play: https://go.dev/play/p/JW1qu-ECwF7
|
||||
func MaxBy[T any](collection []T, greater func(a, b T) bool) T {
|
||||
var mAx T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if greater(item, mAx) {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
|
||||
return mAx
|
||||
}
|
||||
|
||||
// MaxByErr search the maximum value of a collection using the given comparison function.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
func MaxByErr[T any](collection []T, greater func(a, b T) (bool, error)) (T, error) {
|
||||
var mAx T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, nil
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isGreater, err := greater(item, mAx)
|
||||
if err != nil {
|
||||
return mAx, err
|
||||
}
|
||||
if isGreater {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, nil
|
||||
}
|
||||
|
||||
// MaxIndexBy search the maximum value of a collection using the given comparison function and the index of the maximum value.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns (zero value, -1) when the collection is empty.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
//
|
||||
// Play: https://go.dev/play/p/uaUszc-c9QK
|
||||
func MaxIndexBy[T any](collection []T, greater func(a, b T) bool) (T, int) {
|
||||
var (
|
||||
mAx T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, -1
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
if greater(item, mAx) {
|
||||
mAx = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, index
|
||||
}
|
||||
|
||||
// MaxIndexByErr search the maximum value of a collection using the given comparison function and the index of the maximum value.
|
||||
// If several values of the collection are equal to the greatest value, returns the first such value.
|
||||
// Returns (zero value, -1, nil) when the collection is empty.
|
||||
// If the comparison function returns an error, iteration stops and the error is returned.
|
||||
//
|
||||
// Note: the comparison function is inconsistent with most languages, since we use the opposite of the usual convention.
|
||||
// See https://github.com/samber/lo/issues/129
|
||||
func MaxIndexByErr[T any](collection []T, greater func(a, b T) (bool, error)) (T, int, error) {
|
||||
var (
|
||||
mAx T
|
||||
index int
|
||||
)
|
||||
|
||||
if len(collection) == 0 {
|
||||
return mAx, -1, nil
|
||||
}
|
||||
|
||||
mAx = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
item := collection[i]
|
||||
|
||||
isGreater, err := greater(item, mAx)
|
||||
if err != nil {
|
||||
var zero T
|
||||
return zero, -1, err
|
||||
}
|
||||
if isGreater {
|
||||
mAx = item
|
||||
index = i
|
||||
}
|
||||
}
|
||||
|
||||
return mAx, index, nil
|
||||
}
|
||||
|
||||
// Latest search the maximum time.Time of a collection.
|
||||
// Returns zero value when the collection is empty.
|
||||
func Latest(times ...time.Time) time.Time {
|
||||
var mAx time.Time
|
||||
|
||||
if len(times) == 0 {
|
||||
return mAx
|
||||
}
|
||||
|
||||
mAx = times[0]
|
||||
|
||||
for i := 1; i < len(times); i++ {
|
||||
item := times[i]
|
||||
|
||||
if item.After(mAx) {
|
||||
mAx = item
|
||||
}
|
||||
}
|
||||
|
||||
return mAx
|
||||
}
|
||||
|
||||
// LatestBy search the maximum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value when the collection is empty.
|
||||
func LatestBy[T any](collection []T, iteratee func(item T) time.Time) T {
|
||||
var latest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return latest
|
||||
}
|
||||
|
||||
latest = collection[0]
|
||||
latestTime := iteratee(collection[0])
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime := iteratee(collection[i])
|
||||
|
||||
if itemTime.After(latestTime) {
|
||||
latest = collection[i]
|
||||
latestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return latest
|
||||
}
|
||||
|
||||
// LatestByErr search the maximum time.Time of a collection using the given iteratee function.
|
||||
// Returns zero value and nil error when the collection is empty.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
func LatestByErr[T any](collection []T, iteratee func(item T) (time.Time, error)) (T, error) {
|
||||
var latest T
|
||||
|
||||
if len(collection) == 0 {
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
latestTime, err := iteratee(collection[0])
|
||||
if err != nil {
|
||||
return latest, err
|
||||
}
|
||||
latest = collection[0]
|
||||
|
||||
for i := 1; i < len(collection); i++ {
|
||||
itemTime, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return latest, err
|
||||
}
|
||||
|
||||
if itemTime.After(latestTime) {
|
||||
latest = collection[i]
|
||||
latestTime = itemTime
|
||||
}
|
||||
}
|
||||
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
// First returns the first element of a collection and check for availability of the first element.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func First[T any](collection []T) (T, bool) {
|
||||
length := len(collection)
|
||||
|
||||
if length == 0 {
|
||||
var t T
|
||||
return t, false
|
||||
}
|
||||
|
||||
return collection[0], true
|
||||
}
|
||||
|
||||
// FirstOrEmpty returns the first element of a collection or zero value if empty.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func FirstOrEmpty[T any](collection []T) T {
|
||||
i, _ := First(collection)
|
||||
return i
|
||||
}
|
||||
|
||||
// FirstOr returns the first element of a collection or the fallback value if empty.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func FirstOr[T any](collection []T, fallback T) T {
|
||||
i, ok := First(collection)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Last returns the last element of a collection or error if empty.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func Last[T any](collection []T) (T, bool) {
|
||||
length := len(collection)
|
||||
|
||||
if length == 0 {
|
||||
var t T
|
||||
return t, false
|
||||
}
|
||||
|
||||
return collection[length-1], true
|
||||
}
|
||||
|
||||
// LastOrEmpty returns the last element of a collection or zero value if empty.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func LastOrEmpty[T any](collection []T) T {
|
||||
i, _ := Last(collection)
|
||||
return i
|
||||
}
|
||||
|
||||
// LastOr returns the last element of a collection or the fallback value if empty.
|
||||
// Play: https://go.dev/play/p/ul45Z0y2EFO
|
||||
func LastOr[T any](collection []T, fallback T) T {
|
||||
i, ok := Last(collection)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return i
|
||||
}
|
||||
|
||||
// Nth returns the element at index `nth` of collection. If `nth` is negative, the nth element
|
||||
// from the end is returned. An error is returned when nth is out of slice bounds.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func Nth[T any, N constraints.Integer](collection []T, nth N) (T, error) {
|
||||
value, ok := sliceNth(collection, nth)
|
||||
|
||||
return value, Validate(ok, "nth: %d out of slice bounds", nth)
|
||||
}
|
||||
|
||||
func sliceNth[T any, N constraints.Integer](collection []T, nth N) (T, bool) {
|
||||
n := int(nth)
|
||||
l := len(collection)
|
||||
if n >= l || -n > l {
|
||||
return Empty[T](), false
|
||||
}
|
||||
|
||||
if n >= 0 {
|
||||
return collection[n], true
|
||||
}
|
||||
return collection[l+n], true
|
||||
}
|
||||
|
||||
// NthOr returns the element at index `nth` of collection.
|
||||
// If `nth` is negative, it returns the nth element from the end.
|
||||
// If `nth` is out of slice bounds, it returns the fallback value instead of an error.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func NthOr[T any, N constraints.Integer](collection []T, nth N, fallback T) T {
|
||||
value, ok := sliceNth(collection, nth)
|
||||
if !ok {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// NthOrEmpty returns the element at index `nth` of collection.
|
||||
// If `nth` is negative, it returns the nth element from the end.
|
||||
// If `nth` is out of slice bounds, it returns the zero value (empty value) for that type.
|
||||
// Play: https://go.dev/play/p/sHoh88KWt6B
|
||||
func NthOrEmpty[T any, N constraints.Integer](collection []T, nth N) T {
|
||||
value, _ := sliceNth(collection, nth)
|
||||
return value
|
||||
}
|
||||
|
||||
// randomIntGenerator is a function that should return a random integer in the range [0, n)
|
||||
// where n is the argument passed to the randomIntGenerator.
|
||||
type randomIntGenerator func(n int) int
|
||||
|
||||
// Sample returns a random item from collection.
|
||||
// Play: https://go.dev/play/p/vCcSJbh5s6l
|
||||
func Sample[T any](collection []T) T {
|
||||
return SampleBy(collection, xrand.IntN)
|
||||
}
|
||||
|
||||
// SampleBy returns a random item from collection, using randomIntGenerator as the random index generator.
|
||||
// Play: https://go.dev/play/p/HDmKmMgq0XN
|
||||
func SampleBy[T any](collection []T, randomIntGenerator randomIntGenerator) T {
|
||||
size := len(collection)
|
||||
if size == 0 {
|
||||
return Empty[T]()
|
||||
}
|
||||
return collection[randomIntGenerator(size)]
|
||||
}
|
||||
|
||||
// Samples returns N random unique items from collection.
|
||||
// Play: https://go.dev/play/p/vCcSJbh5s6l
|
||||
func Samples[T any, Slice ~[]T](collection Slice, count int) Slice {
|
||||
return SamplesBy(collection, count, xrand.IntN)
|
||||
}
|
||||
|
||||
// SamplesBy returns N random unique items from collection, using randomIntGenerator as the random index generator.
|
||||
// Play: https://go.dev/play/p/HDmKmMgq0XN
|
||||
func SamplesBy[T any, Slice ~[]T](collection Slice, count int, randomIntGenerator randomIntGenerator) Slice {
|
||||
if count <= 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
size := len(collection)
|
||||
|
||||
if size < count {
|
||||
count = size
|
||||
}
|
||||
|
||||
indexes := Range(size)
|
||||
results := make(Slice, count)
|
||||
|
||||
for i := range results {
|
||||
n := len(indexes)
|
||||
|
||||
index := randomIntGenerator(n)
|
||||
results[i] = collection[indexes[index]]
|
||||
|
||||
// Removes index.
|
||||
// It is faster to swap with last element and remove it.
|
||||
indexes[index] = indexes[n-1]
|
||||
indexes = indexes[:n-1]
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
package lo
|
||||
|
||||
// Partial returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/Sy1gAQiQZ3v
|
||||
func Partial[T1, T2, R any](f func(a T1, b T2) R, arg1 T1) func(T2) R {
|
||||
return func(t2 T2) R {
|
||||
return f(arg1, t2)
|
||||
}
|
||||
}
|
||||
|
||||
// Partial1 returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/D-ASTXCLBzw
|
||||
func Partial1[T1, T2, R any](f func(T1, T2) R, arg1 T1) func(T2) R {
|
||||
return Partial(f, arg1)
|
||||
}
|
||||
|
||||
// Partial2 returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/-xiPjy4JChJ
|
||||
func Partial2[T1, T2, T3, R any](f func(T1, T2, T3) R, arg1 T1) func(T2, T3) R {
|
||||
return func(t2 T2, t3 T3) R {
|
||||
return f(arg1, t2, t3)
|
||||
}
|
||||
}
|
||||
|
||||
// Partial3 returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/zWtSutpI26m
|
||||
func Partial3[T1, T2, T3, T4, R any](f func(T1, T2, T3, T4) R, arg1 T1) func(T2, T3, T4) R {
|
||||
return func(t2 T2, t3 T3, t4 T4) R {
|
||||
return f(arg1, t2, t3, t4)
|
||||
}
|
||||
}
|
||||
|
||||
// Partial4 returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/kBrnnMTcJm0
|
||||
func Partial4[T1, T2, T3, T4, T5, R any](f func(T1, T2, T3, T4, T5) R, arg1 T1) func(T2, T3, T4, T5) R {
|
||||
return func(t2 T2, t3 T3, t4 T4, t5 T5) R {
|
||||
return f(arg1, t2, t3, t4, t5)
|
||||
}
|
||||
}
|
||||
|
||||
// Partial5 returns new function that, when called, has its first argument set to the provided value.
|
||||
// Play: https://go.dev/play/p/7Is7K2y_VC3
|
||||
func Partial5[T1, T2, T3, T4, T5, T6, R any](f func(T1, T2, T3, T4, T5, T6) R, arg1 T1) func(T2, T3, T4, T5, T6) R {
|
||||
return func(t2 T2, t3 T3, t4 T4, t5 T5, t6 T6) R {
|
||||
return f(arg1, t2, t3, t4, t5, t6)
|
||||
}
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
|
||||
# Constraints
|
||||
|
||||
This package is for Go 1.18 retrocompatiblity purpose.
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package constraints defines a set of useful constraints to be used
|
||||
// with type parameters.
|
||||
package constraints
|
||||
|
||||
// Signed is a constraint that permits any signed integer type.
|
||||
// If future releases of Go add new predeclared signed integer types,
|
||||
// this constraint will be modified to include them.
|
||||
type Signed interface {
|
||||
~int | ~int8 | ~int16 | ~int32 | ~int64
|
||||
}
|
||||
|
||||
// Unsigned is a constraint that permits any unsigned integer type.
|
||||
// If future releases of Go add new predeclared unsigned integer types,
|
||||
// this constraint will be modified to include them.
|
||||
type Unsigned interface {
|
||||
~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr
|
||||
}
|
||||
|
||||
// Integer is a constraint that permits any integer type.
|
||||
// If future releases of Go add new predeclared integer types,
|
||||
// this constraint will be modified to include them.
|
||||
type Integer interface {
|
||||
Signed | Unsigned
|
||||
}
|
||||
|
||||
// Float is a constraint that permits any floating-point type.
|
||||
// If future releases of Go add new predeclared floating-point types,
|
||||
// this constraint will be modified to include them.
|
||||
type Float interface {
|
||||
~float32 | ~float64
|
||||
}
|
||||
|
||||
// Complex is a constraint that permits any complex numeric type.
|
||||
// If future releases of Go add new predeclared complex numeric types,
|
||||
// this constraint will be modified to include them.
|
||||
type Complex interface {
|
||||
~complex64 | ~complex128
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
//go:build !go1.21
|
||||
|
||||
package constraints
|
||||
|
||||
// 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.
|
||||
type Ordered interface {
|
||||
Integer | Float | ~string
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
//go:build go1.21
|
||||
|
||||
package constraints
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
)
|
||||
|
||||
// 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.
|
||||
type Ordered = cmp.Ordered
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
//go:build !go1.22
|
||||
|
||||
package xrand
|
||||
|
||||
import "math/rand"
|
||||
|
||||
// Shuffle returns a slice of shuffled values. Uses the Fisher-Yates shuffle algorithm.
|
||||
func Shuffle(n int, swap func(i, j int)) {
|
||||
rand.Shuffle(n, swap)
|
||||
}
|
||||
|
||||
// IntN returns, as an int, a pseudo-random number in the half-open interval [0,n)
|
||||
// from the default Source.
|
||||
// It panics if n <= 0.
|
||||
func IntN(n int) int {
|
||||
// bearer:disable go_gosec_crypto_weak_random
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
||||
// Int64 returns a non-negative pseudo-random 63-bit integer as an int64
|
||||
// from the default Source.
|
||||
func Int64() int64 {
|
||||
// bearer:disable go_gosec_crypto_weak_random
|
||||
n := rand.Int63()
|
||||
|
||||
// bearer:disable go_gosec_crypto_weak_random
|
||||
if rand.Intn(2) == 0 {
|
||||
return -n
|
||||
}
|
||||
|
||||
return n
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//go:build go1.22
|
||||
|
||||
package xrand
|
||||
|
||||
import "math/rand/v2"
|
||||
|
||||
// Shuffle returns a slice of shuffled values. Uses the Fisher-Yates shuffle algorithm.
|
||||
func Shuffle(n int, swap func(i, j int)) {
|
||||
rand.Shuffle(n, swap)
|
||||
}
|
||||
|
||||
// IntN returns, as an int, a pseudo-random number in the half-open interval [0,n)
|
||||
// from the default Source.
|
||||
// It panics if n <= 0.
|
||||
func IntN(n int) int {
|
||||
return rand.IntN(n)
|
||||
}
|
||||
|
||||
// Int64 returns a non-negative pseudo-random 63-bit integer as an int64
|
||||
// from the default Source.
|
||||
func Int64() int64 {
|
||||
return rand.Int64()
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
|
||||
# xtime
|
||||
|
||||
Lightweight mock for time package.
|
||||
|
||||
A dedicated package such as [jonboulle/clockwork](https://github.com/jonboulle/clockwork/) would be better, but I would rather limit dependencies for this package. `clockwork` does not support Go 1.18 anymore.
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
//nolint:revive
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewFakeClock() *FakeClock {
|
||||
return NewFakeClockAt(time.Now())
|
||||
}
|
||||
|
||||
func NewFakeClockAt(t time.Time) *FakeClock {
|
||||
return &FakeClock{
|
||||
time: t,
|
||||
}
|
||||
}
|
||||
|
||||
type FakeClock struct {
|
||||
_ noCopy
|
||||
|
||||
// Not protected by a mutex. If a warning is thrown in your tests,
|
||||
// just disable parallel tests.
|
||||
time time.Time
|
||||
}
|
||||
|
||||
func (c *FakeClock) Now() time.Time {
|
||||
return c.time
|
||||
}
|
||||
|
||||
func (c *FakeClock) Since(t time.Time) time.Duration {
|
||||
return c.time.Sub(t)
|
||||
}
|
||||
|
||||
func (c *FakeClock) Until(t time.Time) time.Duration {
|
||||
return t.Sub(c.time)
|
||||
}
|
||||
|
||||
func (c *FakeClock) Sleep(d time.Duration) {
|
||||
c.time = c.time.Add(d)
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package xtime
|
||||
|
||||
// noCopy may be added to structs which must not be copied
|
||||
// after the first use.
|
||||
//
|
||||
// See https://golang.org/issues/8005#issuecomment-190753527
|
||||
// for details.
|
||||
//
|
||||
// Note that it must not be embedded, due to the Lock and Unlock methods.
|
||||
type noCopy struct{}
|
||||
|
||||
// Lock is a no-op used by -copylocks checker from `go vet`.
|
||||
func (*noCopy) Lock() {}
|
||||
func (*noCopy) Unlock() {}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//nolint:revive
|
||||
package xtime
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewRealClock() *RealClock {
|
||||
return &RealClock{}
|
||||
}
|
||||
|
||||
type RealClock struct {
|
||||
_ noCopy
|
||||
}
|
||||
|
||||
func (c *RealClock) Now() time.Time {
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (c *RealClock) Since(t time.Time) time.Duration {
|
||||
return time.Since(t)
|
||||
}
|
||||
|
||||
func (c *RealClock) Until(t time.Time) time.Duration {
|
||||
return time.Until(t)
|
||||
}
|
||||
|
||||
func (c *RealClock) Sleep(d time.Duration) {
|
||||
time.Sleep(d)
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//nolint:revive
|
||||
package xtime
|
||||
|
||||
import "time"
|
||||
|
||||
var clock Clock = &RealClock{}
|
||||
|
||||
func SetClock(c Clock) {
|
||||
clock = c
|
||||
}
|
||||
|
||||
func Now() time.Time {
|
||||
return clock.Now()
|
||||
}
|
||||
|
||||
func Since(t time.Time) time.Duration {
|
||||
return clock.Since(t)
|
||||
}
|
||||
|
||||
func Until(t time.Time) time.Duration {
|
||||
return clock.Until(t)
|
||||
}
|
||||
|
||||
func Sleep(d time.Duration) {
|
||||
clock.Sleep(d)
|
||||
}
|
||||
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
Since(t time.Time) time.Duration
|
||||
Until(t time.Time) time.Duration
|
||||
Sleep(d time.Duration)
|
||||
}
|
||||
+363
@@ -0,0 +1,363 @@
|
||||
package lo
|
||||
|
||||
// Contains returns true if an element is present in a collection.
|
||||
// Play: https://go.dev/play/p/W1EvyqY6t9j
|
||||
func Contains[T comparable](collection []T, element T) bool {
|
||||
for i := range collection {
|
||||
if collection[i] == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// ContainsBy returns true if predicate function return true.
|
||||
// Play: https://go.dev/play/p/W1EvyqY6t9j
|
||||
func ContainsBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Every returns true if all elements of a subset are contained in a collection or if the subset is empty.
|
||||
// Play: https://go.dev/play/p/W1EvyqY6t9j
|
||||
func Every[T comparable](collection, subset []T) bool {
|
||||
if len(subset) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
seen := Keyify(collection)
|
||||
|
||||
for _, item := range subset {
|
||||
if _, ok := seen[item]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// EveryBy returns true if the predicate returns true for all elements in the collection or if the collection is empty.
|
||||
// Play: https://go.dev/play/p/dn1-vhHsq9x
|
||||
func EveryBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
for i := range collection {
|
||||
if !predicate(collection[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Some returns true if at least 1 element of a subset is contained in a collection.
|
||||
// If the subset is empty Some returns false.
|
||||
// Play: https://go.dev/play/p/Lj4ceFkeT9V
|
||||
func Some[T comparable](collection, subset []T) bool {
|
||||
if len(subset) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
seen := Keyify(subset)
|
||||
for i := range collection {
|
||||
if _, ok := seen[collection[i]]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SomeBy returns true if the predicate returns true for any of the elements in the collection.
|
||||
// If the collection is empty SomeBy returns false.
|
||||
// Play: https://go.dev/play/p/DXF-TORBudx
|
||||
func SomeBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// None returns true if no element of a subset is contained in a collection or if the subset is empty.
|
||||
// Play: https://go.dev/play/p/fye7JsmxzPV
|
||||
func None[T comparable](collection, subset []T) bool {
|
||||
if len(subset) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
seen := Keyify(subset)
|
||||
for i := range collection {
|
||||
if _, ok := seen[collection[i]]; ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// NoneBy returns true if the predicate returns true for none of the elements in the collection or if the collection is empty.
|
||||
// Play: https://go.dev/play/p/O64WZ32H58S
|
||||
func NoneBy[T any](collection []T, predicate func(item T) bool) bool {
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Intersect returns the intersection between collections.
|
||||
// Play: https://go.dev/play/p/uuElL9X9e58
|
||||
func Intersect[T comparable, Slice ~[]T](lists ...Slice) Slice {
|
||||
if len(lists) == 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
last := lists[len(lists)-1]
|
||||
|
||||
seen := make(map[T]bool, len(last))
|
||||
|
||||
for _, item := range last {
|
||||
seen[item] = false
|
||||
}
|
||||
|
||||
for i := len(lists) - 2; i > 0 && len(seen) != 0; i-- {
|
||||
for _, item := range lists[i] {
|
||||
if _, ok := seen[item]; ok {
|
||||
seen[item] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range seen {
|
||||
if v {
|
||||
seen[k] = false
|
||||
} else {
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(seen))
|
||||
|
||||
for _, item := range lists[0] {
|
||||
if _, ok := seen[item]; ok {
|
||||
result = append(result, item)
|
||||
delete(seen, item)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// IntersectBy returns the intersection between two collections using a custom key selector function.
|
||||
func IntersectBy[T any, K comparable, Slice ~[]T](transform func(T) K, lists ...Slice) Slice {
|
||||
if len(lists) == 0 {
|
||||
return Slice{}
|
||||
}
|
||||
|
||||
last := lists[len(lists)-1]
|
||||
|
||||
seen := make(map[K]bool, len(last))
|
||||
|
||||
for _, item := range last {
|
||||
k := transform(item)
|
||||
seen[k] = false
|
||||
}
|
||||
|
||||
for i := len(lists) - 2; i > 0 && len(seen) != 0; i-- {
|
||||
for _, item := range lists[i] {
|
||||
k := transform(item)
|
||||
if _, ok := seen[k]; ok {
|
||||
seen[k] = true
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range seen {
|
||||
if v {
|
||||
seen[k] = false
|
||||
} else {
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := make(Slice, 0, len(seen))
|
||||
|
||||
for _, item := range lists[0] {
|
||||
k := transform(item)
|
||||
if _, ok := seen[k]; ok {
|
||||
result = append(result, item)
|
||||
delete(seen, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Difference returns the difference between two collections.
|
||||
// The first value is the collection of elements absent from list2.
|
||||
// The second value is the collection of elements absent from list1.
|
||||
// Play: https://go.dev/play/p/pKE-JgzqRpz
|
||||
func Difference[T comparable, Slice ~[]T](list1, list2 Slice) (Slice, Slice) {
|
||||
left := Slice{}
|
||||
right := Slice{}
|
||||
|
||||
seenLeft := Keyify(list1)
|
||||
seenRight := Keyify(list2)
|
||||
|
||||
for i := range list1 {
|
||||
if _, ok := seenRight[list1[i]]; !ok {
|
||||
left = append(left, list1[i])
|
||||
}
|
||||
}
|
||||
|
||||
for i := range list2 {
|
||||
if _, ok := seenLeft[list2[i]]; !ok {
|
||||
right = append(right, list2[i])
|
||||
}
|
||||
}
|
||||
|
||||
return left, right
|
||||
}
|
||||
|
||||
// Union returns all distinct elements from given collections.
|
||||
// result returns will not change the order of elements relatively.
|
||||
// Play: https://go.dev/play/p/DI9RVEB_qMK
|
||||
func Union[T comparable, Slice ~[]T](lists ...Slice) Slice {
|
||||
var capLen int
|
||||
|
||||
for _, list := range lists {
|
||||
capLen += len(list)
|
||||
}
|
||||
|
||||
result := make(Slice, 0, capLen)
|
||||
seen := make(map[T]struct{}, capLen)
|
||||
|
||||
for i := range lists {
|
||||
for j := range lists[i] {
|
||||
if _, ok := seen[lists[i][j]]; !ok {
|
||||
seen[lists[i][j]] = struct{}{}
|
||||
result = append(result, lists[i][j])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Without returns a slice excluding all given values.
|
||||
// Play: https://go.dev/play/p/5j30Ux8TaD0
|
||||
func Without[T comparable, Slice ~[]T](collection Slice, exclude ...T) Slice {
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for i := range collection {
|
||||
if _, ok := excludeMap[collection[i]]; !ok {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WithoutBy filters a slice by excluding elements whose extracted keys match any in the exclude list.
|
||||
// Returns a new slice containing only the elements whose keys are not in the exclude list.
|
||||
// Play: https://go.dev/play/p/VgWJOF01NbJ
|
||||
func WithoutBy[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) K, exclude ...K) Slice {
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for _, item := range collection {
|
||||
if _, ok := excludeMap[iteratee(item)]; !ok {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// WithoutByErr filters a slice by excluding elements whose extracted keys match any in the exclude list.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func WithoutByErr[T any, K comparable, Slice ~[]T](collection Slice, iteratee func(item T) (K, error), exclude ...K) (Slice, error) {
|
||||
excludeMap := Keyify(exclude)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for _, item := range collection {
|
||||
key, err := iteratee(item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := excludeMap[key]; !ok {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// WithoutEmpty returns a slice excluding zero values.
|
||||
//
|
||||
// Deprecated: Use lo.Compact instead.
|
||||
func WithoutEmpty[T comparable, Slice ~[]T](collection Slice) Slice {
|
||||
return Compact(collection)
|
||||
}
|
||||
|
||||
// WithoutNth returns a slice excluding the nth value.
|
||||
// Play: https://go.dev/play/p/5g3F9R2H1xL
|
||||
func WithoutNth[T any, Slice ~[]T](collection Slice, nths ...int) Slice {
|
||||
toRemove := Keyify(nths)
|
||||
|
||||
result := make(Slice, 0, len(collection))
|
||||
for i := range collection {
|
||||
if _, ok := toRemove[i]; !ok {
|
||||
result = append(result, collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ElementsMatch returns true if lists contain the same set of elements (including empty set).
|
||||
// If there are duplicate elements, the number of occurrences in each list should match.
|
||||
// The order of elements is not checked.
|
||||
// Play: https://go.dev/play/p/XWSEM4Ic_t0
|
||||
func ElementsMatch[T comparable, Slice ~[]T](list1, list2 Slice) bool {
|
||||
return ElementsMatchBy(list1, list2, func(item T) T { return item })
|
||||
}
|
||||
|
||||
// ElementsMatchBy returns true if lists contain the same set of elements' keys (including empty set).
|
||||
// If there are duplicate keys, the number of occurrences in each list should match.
|
||||
// The order of elements is not checked.
|
||||
// Play: https://go.dev/play/p/XWSEM4Ic_t0
|
||||
func ElementsMatchBy[T any, K comparable](list1, list2 []T, iteratee func(item T) K) bool {
|
||||
if len(list1) != len(list2) {
|
||||
return false
|
||||
}
|
||||
|
||||
if len(list1) == 0 {
|
||||
return true
|
||||
}
|
||||
|
||||
counters := make(map[K]int, len(list1))
|
||||
|
||||
for _, el := range list1 {
|
||||
counters[iteratee(el)]++
|
||||
}
|
||||
|
||||
for _, el := range list2 {
|
||||
counters[iteratee(el)]--
|
||||
}
|
||||
|
||||
for _, count := range counters {
|
||||
if count != 0 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+533
@@ -0,0 +1,533 @@
|
||||
package lo
|
||||
|
||||
// Keys creates a slice of the map keys.
|
||||
// Play: https://go.dev/play/p/Uu11fHASqrU
|
||||
func Keys[K comparable, V any](in ...map[K]V) []K {
|
||||
size := 0
|
||||
for i := range in {
|
||||
size += len(in[i])
|
||||
}
|
||||
result := make([]K, 0, size)
|
||||
|
||||
for i := range in {
|
||||
for k := range in[i] {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqKeys creates a slice of unique keys in the map.
|
||||
// Play: https://go.dev/play/p/TPKAb6ILdHk
|
||||
func UniqKeys[K comparable, V any](in ...map[K]V) []K {
|
||||
size := 0
|
||||
for i := range in {
|
||||
size += len(in[i])
|
||||
}
|
||||
|
||||
seen := make(map[K]struct{}, size)
|
||||
result := make([]K, 0)
|
||||
|
||||
for i := range in {
|
||||
for k := range in[i] {
|
||||
if _, exists := seen[k]; exists {
|
||||
continue
|
||||
}
|
||||
seen[k] = struct{}{}
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// HasKey returns whether the given key exists.
|
||||
// Play: https://go.dev/play/p/aVwubIvECqS
|
||||
func HasKey[K comparable, V any](in map[K]V, key K) bool {
|
||||
_, ok := in[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
// Values creates a slice of the map values.
|
||||
// Play: https://go.dev/play/p/nnRTQkzQfF6
|
||||
func Values[K comparable, V any](in ...map[K]V) []V {
|
||||
size := 0
|
||||
for i := range in {
|
||||
size += len(in[i])
|
||||
}
|
||||
result := make([]V, 0, size)
|
||||
|
||||
for i := range in {
|
||||
for _, v := range in[i] {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// UniqValues creates a slice of unique values in the map.
|
||||
// Play: https://go.dev/play/p/nf6bXMh7rM3
|
||||
func UniqValues[K, V comparable](in ...map[K]V) []V {
|
||||
size := 0
|
||||
for i := range in {
|
||||
size += len(in[i])
|
||||
}
|
||||
|
||||
seen := make(map[V]struct{}, size)
|
||||
result := make([]V, 0)
|
||||
|
||||
for i := range in {
|
||||
for _, v := range in[i] {
|
||||
if _, exists := seen[v]; exists {
|
||||
continue
|
||||
}
|
||||
seen[v] = struct{}{}
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// ValueOr returns the value of the given key or the fallback value if the key is not present.
|
||||
// Play: https://go.dev/play/p/bAq9mHErB4V
|
||||
func ValueOr[K comparable, V any](in map[K]V, key K, fallback V) V {
|
||||
if v, ok := in[key]; ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// PickBy returns same map type filtered by given predicate.
|
||||
// Play: https://go.dev/play/p/kdg8GR_QMmf
|
||||
func PickBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) bool) Map {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
if predicate(k, v) {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// PickByErr returns same map type filtered by given predicate.
|
||||
// It returns the first error returned by the predicate.
|
||||
func PickByErr[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) (bool, error)) (Map, error) {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// PickByKeys returns same map type filtered by given keys.
|
||||
// Play: https://go.dev/play/p/R1imbuci9qU
|
||||
func PickByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
r := Map{}
|
||||
for i := range keys {
|
||||
if v, ok := in[keys[i]]; ok {
|
||||
r[keys[i]] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// PickByValues returns same map type filtered by given values.
|
||||
// Play: https://go.dev/play/p/1zdzSvbfsJc
|
||||
func PickByValues[K, V comparable, Map ~map[K]V](in Map, values []V) Map {
|
||||
r := Map{}
|
||||
|
||||
seen := Keyify(values)
|
||||
for k, v := range in {
|
||||
if _, ok := seen[v]; ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// OmitBy returns same map type filtered by given predicate.
|
||||
// Play: https://go.dev/play/p/EtBsR43bdsd
|
||||
func OmitBy[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) bool) Map {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
if !predicate(k, v) {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// OmitByErr returns same map type filtered by given predicate.
|
||||
// It returns the first error returned by the predicate.
|
||||
func OmitByErr[K comparable, V any, Map ~map[K]V](in Map, predicate func(key K, value V) (bool, error)) (Map, error) {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// OmitByKeys returns same map type filtered by given keys.
|
||||
// Play: https://go.dev/play/p/t1QjCrs-ysk
|
||||
func OmitByKeys[K comparable, V any, Map ~map[K]V](in Map, keys []K) Map {
|
||||
r := Map{}
|
||||
for k, v := range in {
|
||||
r[k] = v
|
||||
}
|
||||
for i := range keys {
|
||||
delete(r, keys[i])
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// OmitByValues returns same map type filtered by given values.
|
||||
// Play: https://go.dev/play/p/9UYZi-hrs8j
|
||||
func OmitByValues[K, V comparable, Map ~map[K]V](in Map, values []V) Map {
|
||||
r := Map{}
|
||||
|
||||
seen := Keyify(values)
|
||||
for k, v := range in {
|
||||
if _, ok := seen[v]; !ok {
|
||||
r[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
// Entries transforms a map into a slice of key/value pairs.
|
||||
// Play: https://go.dev/play/p/_t4Xe34-Nl5
|
||||
func Entries[K comparable, V any](in map[K]V) []Entry[K, V] {
|
||||
entries := make([]Entry[K, V], 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
entries = append(entries, Entry[K, V]{
|
||||
Key: k,
|
||||
Value: v,
|
||||
})
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
// ToPairs transforms a map into a slice of key/value pairs.
|
||||
// Alias of Entries().
|
||||
// Play: https://go.dev/play/p/3Dhgx46gawJ
|
||||
func ToPairs[K comparable, V any](in map[K]V) []Entry[K, V] {
|
||||
return Entries(in)
|
||||
}
|
||||
|
||||
// FromEntries transforms a slice of key/value pairs into a map.
|
||||
// Play: https://go.dev/play/p/oIr5KHFGCEN
|
||||
func FromEntries[K comparable, V any](entries []Entry[K, V]) map[K]V {
|
||||
out := make(map[K]V, len(entries))
|
||||
|
||||
for i := range entries {
|
||||
out[entries[i].Key] = entries[i].Value
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// FromPairs transforms a slice of key/value pairs into a map.
|
||||
// Alias of FromEntries().
|
||||
// Play: https://go.dev/play/p/oIr5KHFGCEN
|
||||
func FromPairs[K comparable, V any](entries []Entry[K, V]) map[K]V {
|
||||
return FromEntries(entries)
|
||||
}
|
||||
|
||||
// Invert creates a map composed of the inverted keys and values. If map
|
||||
// contains duplicate values, subsequent values overwrite property assignments
|
||||
// of previous values.
|
||||
// Play: https://go.dev/play/p/rFQ4rak6iA1
|
||||
func Invert[K, V comparable](in map[K]V) map[V]K {
|
||||
out := make(map[V]K, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
out[v] = k
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// Assign merges multiple maps from left to right.
|
||||
// Play: https://go.dev/play/p/VhwfJOyxf5o
|
||||
func Assign[K comparable, V any, Map ~map[K]V](maps ...Map) Map {
|
||||
count := 0
|
||||
for i := range maps {
|
||||
count += len(maps[i])
|
||||
}
|
||||
|
||||
out := make(Map, count)
|
||||
for i := range maps {
|
||||
for k, v := range maps[i] {
|
||||
out[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
// ChunkEntries splits a map into a slice of elements in groups of length equal to its size. If the map cannot be split evenly,
|
||||
// the final chunk will contain the remaining elements.
|
||||
// Play: https://go.dev/play/p/X_YQL6mmoD-
|
||||
func ChunkEntries[K comparable, V any](m map[K]V, size int) []map[K]V {
|
||||
if size <= 0 {
|
||||
panic("lo.ChunkEntries: size must be greater than 0")
|
||||
}
|
||||
|
||||
count := len(m)
|
||||
if count == 0 {
|
||||
return []map[K]V{}
|
||||
}
|
||||
|
||||
result := make([]map[K]V, 0, ((count-1)/size)+1)
|
||||
|
||||
for k, v := range m {
|
||||
if len(result) == 0 || len(result[len(result)-1]) == size {
|
||||
result = append(result, make(map[K]V, size))
|
||||
}
|
||||
|
||||
result[len(result)-1][k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeys manipulates map keys and transforms it to a map of another type.
|
||||
// Play: https://go.dev/play/p/9_4WPIqOetJ
|
||||
func MapKeys[K comparable, V any, R comparable](in map[K]V, iteratee func(value V, key K) R) map[R]V {
|
||||
result := make(map[R]V, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
result[iteratee(v, k)] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapKeysErr manipulates map keys and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapKeysErr[K comparable, V any, R comparable](in map[K]V, iteratee func(value V, key K) (R, error)) (map[R]V, error) {
|
||||
result := make(map[R]V, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[r] = v
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapValues manipulates map values and transforms it to a map of another type.
|
||||
// Play: https://go.dev/play/p/T_8xAfvcf0W
|
||||
func MapValues[K comparable, V, R any](in map[K]V, iteratee func(value V, key K) R) map[K]R {
|
||||
result := make(map[K]R, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
result[k] = iteratee(v, k)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapValuesErr manipulates map values and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapValuesErr[K comparable, V, R any](in map[K]V, iteratee func(value V, key K) (R, error)) (map[K]R, error) {
|
||||
result := make(map[K]R, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(v, k)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k] = r
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapEntries manipulates map entries and transforms it to a map of another type.
|
||||
// Play: https://go.dev/play/p/VuvNQzxKimT
|
||||
func MapEntries[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iteratee func(key K1, value V1) (K2, V2)) map[K2]V2 {
|
||||
result := make(map[K2]V2, len(in))
|
||||
|
||||
for k1 := range in {
|
||||
k2, v2 := iteratee(k1, in[k1])
|
||||
result[k2] = v2
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapEntriesErr manipulates map entries and transforms it to a map of another type.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapEntriesErr[K1 comparable, V1 any, K2 comparable, V2 any](in map[K1]V1, iteratee func(key K1, value V1) (K2, V2, error)) (map[K2]V2, error) {
|
||||
result := make(map[K2]V2, len(in))
|
||||
|
||||
for k1 := range in {
|
||||
k2, v2, err := iteratee(k1, in[k1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[k2] = v2
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// MapToSlice transforms a map into a slice based on specified iteratee.
|
||||
// Play: https://go.dev/play/p/ZuiCZpDt6LD
|
||||
func MapToSlice[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) R) []R {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
result = append(result, iteratee(k, v))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// MapToSliceErr transforms a map into a slice based on specified iteratee.
|
||||
// It returns the first error returned by the iteratee.
|
||||
func MapToSliceErr[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) (R, error)) ([]R, error) {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, err := iteratee(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result = append(result, r)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterMapToSlice transforms a map into a slice based on specified iteratee.
|
||||
// The iteratee returns a value and a boolean. If the boolean is true, the value is added to the result slice.
|
||||
// If the boolean is false, the value is not added to the result slice.
|
||||
// The order of the keys in the input map is not specified and the order of the keys in the output slice is not guaranteed.
|
||||
// Play: https://go.dev/play/p/jgsD_Kil9pV
|
||||
func FilterMapToSlice[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) (R, bool)) []R {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
if v, ok := iteratee(k, v); ok {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterMapToSliceErr transforms a map into a slice based on specified iteratee.
|
||||
// The iteratee returns a value, a boolean, and an error. If the boolean is true, the value is added to the result slice.
|
||||
// If the boolean is false, the value is not added to the result slice.
|
||||
// If an error is returned, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified and the order of the keys in the output slice is not guaranteed.
|
||||
func FilterMapToSliceErr[K comparable, V, R any](in map[K]V, iteratee func(key K, value V) (R, bool, error)) ([]R, error) {
|
||||
result := make([]R, 0, len(in))
|
||||
|
||||
for k, v := range in {
|
||||
r, ok, err := iteratee(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, r)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterKeys transforms a map into a slice based on predicate returns true for specific elements.
|
||||
// It is a mix of lo.Filter() and lo.Keys().
|
||||
// Play: https://go.dev/play/p/OFlKXlPrBAe
|
||||
func FilterKeys[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) []K {
|
||||
result := make([]K, 0)
|
||||
|
||||
for k, v := range in {
|
||||
if predicate(k, v) {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterValues transforms a map into a slice based on predicate returns true for specific elements.
|
||||
// It is a mix of lo.Filter() and lo.Values().
|
||||
// Play: https://go.dev/play/p/YVD5r_h-LX-
|
||||
func FilterValues[K comparable, V any](in map[K]V, predicate func(key K, value V) bool) []V {
|
||||
result := make([]V, 0)
|
||||
|
||||
for k, v := range in {
|
||||
if predicate(k, v) {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FilterKeysErr transforms a map into a slice of keys based on predicate that can return an error.
|
||||
// It is a mix of lo.Filter() and lo.Keys() with error handling.
|
||||
// If the predicate returns true, the key is added to the result slice.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified.
|
||||
func FilterKeysErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]K, error) {
|
||||
result := make([]K, 0)
|
||||
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, k)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FilterValuesErr transforms a map into a slice of values based on predicate that can return an error.
|
||||
// It is a mix of lo.Filter() and lo.Values() with error handling.
|
||||
// If the predicate returns true, the value is added to the result slice.
|
||||
// If the predicate returns an error, iteration stops immediately and returns the error.
|
||||
// The order of the keys in the input map is not specified.
|
||||
func FilterValuesErr[K comparable, V any](in map[K]V, predicate func(key K, value V) (bool, error)) ([]V, error) {
|
||||
result := make([]V, 0)
|
||||
|
||||
for k, v := range in {
|
||||
ok, err := predicate(k, v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"github.com/samber/lo/internal/constraints"
|
||||
)
|
||||
|
||||
// Range creates a slice of numbers (positive and/or negative) with given length.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func Range(elementNum int) []int {
|
||||
step := Ternary(elementNum < 0, -1, 1)
|
||||
length := elementNum * step
|
||||
result := make([]int, length)
|
||||
for i, j := 0, 0; i < length; i, j = i+1, j+step {
|
||||
result[i] = j
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RangeFrom creates a slice of numbers from start with specified length.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func RangeFrom[T constraints.Integer | constraints.Float](start T, elementNum int) []T {
|
||||
step := Ternary(elementNum < 0, -1, 1)
|
||||
length := elementNum * step
|
||||
result := make([]T, length)
|
||||
for i, j := 0, start; i < length; i, j = i+1, j+T(step) {
|
||||
result[i] = j
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RangeWithSteps creates a slice of numbers (positive and/or negative) progressing from start up to, but not including end.
|
||||
// step set to zero will return an empty slice.
|
||||
// Play: https://go.dev/play/p/0r6VimXAi9H
|
||||
func RangeWithSteps[T constraints.Integer | constraints.Float](start, end, step T) []T {
|
||||
if start == end || step == 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
capacity := func(count, delta T) int {
|
||||
// Use math.Ceil instead of (count-1)/delta+1 because integer division
|
||||
// fails for floats (e.g., 5.5/2.5=2.2 → ceil=3, not 2).
|
||||
return int(math.Ceil(float64(count) / float64(delta)))
|
||||
}
|
||||
|
||||
if start < end {
|
||||
if step < 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
result := make([]T, 0, capacity(end-start, step))
|
||||
for i := start; i < end; i += step {
|
||||
result = append(result, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
if step > 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
result := make([]T, 0, capacity(start-end, -step))
|
||||
for i := start; i > end; i += step {
|
||||
result = append(result, i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// Clamp clamps number within the inclusive lower and upper bounds.
|
||||
// Play: https://go.dev/play/p/RU4lJNC2hlI
|
||||
func Clamp[T constraints.Ordered](value, mIn, mAx T) T {
|
||||
if value < mIn {
|
||||
return mIn
|
||||
} else if value > mAx {
|
||||
return mAx
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// Sum sums the values in a collection. If collection is empty 0 is returned.
|
||||
// Play: https://go.dev/play/p/upfeJVqs4Bt
|
||||
func Sum[T constraints.Float | constraints.Integer | constraints.Complex](collection []T) T {
|
||||
var sum T
|
||||
for i := range collection {
|
||||
sum += collection[i]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// SumBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 0 is returned.
|
||||
// Play: https://go.dev/play/p/Dz_a_7jN_ca
|
||||
func SumBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) R) R {
|
||||
var sum R
|
||||
for i := range collection {
|
||||
sum += iteratee(collection[i])
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
// SumByErr summarizes the values in a collection using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 0 and nil error are returned.
|
||||
func SumByErr[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
var sum R
|
||||
for i := range collection {
|
||||
v, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return sum, err
|
||||
}
|
||||
sum += v
|
||||
}
|
||||
return sum, nil
|
||||
}
|
||||
|
||||
// Product gets the product of the values in a collection. If collection is empty 1 is returned.
|
||||
// Play: https://go.dev/play/p/2_kjM_smtAH
|
||||
func Product[T constraints.Float | constraints.Integer | constraints.Complex](collection []T) T {
|
||||
var product T = 1
|
||||
for i := range collection {
|
||||
product *= collection[i]
|
||||
}
|
||||
return product
|
||||
}
|
||||
|
||||
// ProductBy summarizes the values in a collection using the given return value from the iteration function. If collection is empty 1 is returned.
|
||||
// Play: https://go.dev/play/p/wadzrWr9Aer
|
||||
func ProductBy[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) R) R {
|
||||
var product R = 1
|
||||
for i := range collection {
|
||||
product *= iteratee(collection[i])
|
||||
}
|
||||
return product
|
||||
}
|
||||
|
||||
// ProductByErr summarizes the values in a collection using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 1 and nil error are returned.
|
||||
func ProductByErr[T any, R constraints.Float | constraints.Integer | constraints.Complex](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
var product R = 1
|
||||
for i := range collection {
|
||||
v, err := iteratee(collection[i])
|
||||
if err != nil {
|
||||
return product, err
|
||||
}
|
||||
product *= v
|
||||
}
|
||||
return product, nil
|
||||
}
|
||||
|
||||
// Mean calculates the mean of a collection of numbers.
|
||||
// Play: https://go.dev/play/p/tPURSuteUsP
|
||||
func Mean[T constraints.Float | constraints.Integer](collection []T) T {
|
||||
length := T(len(collection))
|
||||
if length == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := Sum(collection)
|
||||
return sum / length
|
||||
}
|
||||
|
||||
// MeanBy calculates the mean of a collection of numbers using the given return value from the iteration function.
|
||||
// Play: https://go.dev/play/p/j7TsVwBOZ7P
|
||||
func MeanBy[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(item T) R) R {
|
||||
length := R(len(collection))
|
||||
if length == 0 {
|
||||
return 0
|
||||
}
|
||||
sum := SumBy(collection, iteratee)
|
||||
return sum / length
|
||||
}
|
||||
|
||||
// MeanByErr calculates the mean of a collection of numbers using the given return value from the iteration function.
|
||||
// If the iteratee returns an error, iteration stops and the error is returned.
|
||||
// If collection is empty 0 and nil error are returned.
|
||||
func MeanByErr[T any, R constraints.Float | constraints.Integer](collection []T, iteratee func(item T) (R, error)) (R, error) {
|
||||
length := R(len(collection))
|
||||
if length == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
sum, err := SumByErr(collection, iteratee)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return sum / length, nil
|
||||
}
|
||||
|
||||
// Mode returns the mode (most frequent value) of a collection.
|
||||
// If multiple values have the same highest frequency, then multiple values are returned.
|
||||
// If the collection is empty, then the zero value of T is returned.
|
||||
func Mode[T constraints.Integer | constraints.Float](collection []T) []T {
|
||||
length := T(len(collection))
|
||||
if length == 0 {
|
||||
return []T{}
|
||||
}
|
||||
|
||||
mode := make([]T, 0)
|
||||
maxFreq := 0
|
||||
frequency := make(map[T]int)
|
||||
|
||||
for _, item := range collection {
|
||||
frequency[item]++
|
||||
count := frequency[item]
|
||||
|
||||
if count > maxFreq {
|
||||
maxFreq = count
|
||||
mode = []T{item}
|
||||
} else if count == maxFreq {
|
||||
mode = append(mode, item)
|
||||
}
|
||||
}
|
||||
|
||||
return mode
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
package mutable
|
||||
|
||||
import "github.com/samber/lo/internal/xrand"
|
||||
|
||||
// Filter is a generic function that modifies the input slice in-place to contain only the elements
|
||||
// that satisfy the provided predicate function. The predicate function takes an element of the slice and its index,
|
||||
// and should return true for elements that should be kept and false for elements that should be removed.
|
||||
// The function returns the modified slice, which may be shorter than the original if some elements were removed.
|
||||
// Note that the order of elements in the original slice is preserved in the output.
|
||||
// Play: https://go.dev/play/p/0jY3Z0B7O_5
|
||||
func Filter[T any, Slice ~[]T](collection Slice, predicate func(item T) bool) Slice {
|
||||
j := 0
|
||||
for i := range collection {
|
||||
if predicate(collection[i]) {
|
||||
collection[j] = collection[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
return collection[:j]
|
||||
}
|
||||
|
||||
// FilterI is a generic function that modifies the input slice in-place to contain only the elements
|
||||
// that satisfy the provided predicate function. The predicate function takes an element of the slice and its index,
|
||||
// and should return true for elements that should be kept and false for elements that should be removed.
|
||||
// The function returns the modified slice, which may be shorter than the original if some elements were removed.
|
||||
// Note that the order of elements in the original slice is preserved in the output.
|
||||
func FilterI[T any, Slice ~[]T](collection Slice, predicate func(item T, index int) bool) Slice {
|
||||
j := 0
|
||||
for i := range collection {
|
||||
if predicate(collection[i], i) {
|
||||
collection[j] = collection[i]
|
||||
j++
|
||||
}
|
||||
}
|
||||
return collection[:j]
|
||||
}
|
||||
|
||||
// Map is a generic function that modifies the input slice in-place to contain the result of applying the provided
|
||||
// function to each element of the slice. The function returns the modified slice, which has the same length as the original.
|
||||
// Play: https://go.dev/play/p/0jY3Z0B7O_5
|
||||
func Map[T any, Slice ~[]T](collection Slice, transform func(item T) T) {
|
||||
for i := range collection {
|
||||
collection[i] = transform(collection[i])
|
||||
}
|
||||
}
|
||||
|
||||
// MapI is a generic function that modifies the input slice in-place to contain the result of applying the provided
|
||||
// function to each element of the slice. The function returns the modified slice, which has the same length as the original.
|
||||
func MapI[T any, Slice ~[]T](collection Slice, transform func(item T, index int) T) {
|
||||
for i := range collection {
|
||||
collection[i] = transform(collection[i], i)
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle returns a slice of shuffled values. Uses the Fisher-Yates shuffle algorithm.
|
||||
// Play: https://go.dev/play/p/2xb3WdLjeSJ
|
||||
func Shuffle[T any, Slice ~[]T](collection Slice) {
|
||||
xrand.Shuffle(len(collection), func(i, j int) {
|
||||
collection[i], collection[j] = collection[j], collection[i]
|
||||
})
|
||||
}
|
||||
|
||||
// Reverse reverses a slice so that the first element becomes the last, the second element becomes the second to last, and so on.
|
||||
// Play: https://go.dev/play/p/O-M5pmCRgzV
|
||||
func Reverse[T any, Slice ~[]T](collection Slice) {
|
||||
length := len(collection)
|
||||
half := length / 2
|
||||
|
||||
for i := 0; i < half; i++ {
|
||||
j := length - 1 - i
|
||||
collection[i], collection[j] = collection[j], collection[i]
|
||||
}
|
||||
}
|
||||
+388
@@ -0,0 +1,388 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/samber/lo/internal/xtime"
|
||||
)
|
||||
|
||||
type debounce struct {
|
||||
after time.Duration
|
||||
mu *sync.Mutex
|
||||
timer *time.Timer
|
||||
done bool
|
||||
callbacks []func()
|
||||
}
|
||||
|
||||
func (d *debounce) reset() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.done {
|
||||
return
|
||||
}
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
}
|
||||
|
||||
d.timer = time.AfterFunc(d.after, func() {
|
||||
// We need to lock the mutex here to avoid race conditions with 2 concurrent calls to reset()
|
||||
d.mu.Lock()
|
||||
callbacks := append([]func(){}, d.callbacks...)
|
||||
d.mu.Unlock()
|
||||
|
||||
for i := range callbacks {
|
||||
callbacks[i]()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *debounce) cancel() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if d.timer != nil {
|
||||
d.timer.Stop()
|
||||
d.timer = nil
|
||||
}
|
||||
|
||||
d.done = true
|
||||
}
|
||||
|
||||
// NewDebounce creates a debounced instance that delays invoking functions given until after wait milliseconds have elapsed.
|
||||
// Play: https://go.dev/play/p/mz32VMK2nqe
|
||||
func NewDebounce(duration time.Duration, f ...func()) (func(), func()) {
|
||||
d := &debounce{
|
||||
after: duration,
|
||||
mu: new(sync.Mutex),
|
||||
timer: nil,
|
||||
done: false,
|
||||
callbacks: f,
|
||||
}
|
||||
|
||||
return func() {
|
||||
d.reset()
|
||||
}, d.cancel
|
||||
}
|
||||
|
||||
type debounceByItem struct {
|
||||
mu *sync.Mutex
|
||||
timer *time.Timer
|
||||
count int
|
||||
}
|
||||
|
||||
type debounceBy[T comparable] struct {
|
||||
after time.Duration
|
||||
mu *sync.Mutex
|
||||
items map[T]*debounceByItem
|
||||
callbacks []func(key T, count int)
|
||||
}
|
||||
|
||||
func (d *debounceBy[T]) reset(key T) {
|
||||
d.mu.Lock()
|
||||
if _, ok := d.items[key]; !ok {
|
||||
d.items[key] = &debounceByItem{
|
||||
mu: new(sync.Mutex),
|
||||
timer: nil,
|
||||
}
|
||||
}
|
||||
|
||||
item := d.items[key]
|
||||
|
||||
d.mu.Unlock()
|
||||
|
||||
item.mu.Lock()
|
||||
defer item.mu.Unlock()
|
||||
|
||||
item.count++
|
||||
|
||||
if item.timer != nil {
|
||||
item.timer.Stop()
|
||||
}
|
||||
|
||||
item.timer = time.AfterFunc(d.after, func() {
|
||||
// We need to lock the mutex here to avoid race conditions with 2 concurrent calls to reset()
|
||||
item.mu.Lock()
|
||||
count := item.count
|
||||
item.count = 0
|
||||
callbacks := append([]func(key T, count int){}, d.callbacks...)
|
||||
item.mu.Unlock()
|
||||
|
||||
for i := range callbacks {
|
||||
callbacks[i](key, count)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (d *debounceBy[T]) cancel(key T) {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if item, ok := d.items[key]; ok {
|
||||
item.mu.Lock()
|
||||
|
||||
if item.timer != nil {
|
||||
item.timer.Stop()
|
||||
item.timer = nil
|
||||
}
|
||||
|
||||
item.mu.Unlock()
|
||||
|
||||
delete(d.items, key)
|
||||
}
|
||||
}
|
||||
|
||||
// NewDebounceBy creates a debounced instance for each distinct key, that delays invoking functions given until after wait milliseconds have elapsed.
|
||||
// Play: https://go.dev/play/p/d3Vpt6pxhY8
|
||||
func NewDebounceBy[T comparable](duration time.Duration, f ...func(key T, count int)) (func(key T), func(key T)) {
|
||||
d := &debounceBy[T]{
|
||||
after: duration,
|
||||
mu: new(sync.Mutex),
|
||||
items: map[T]*debounceByItem{},
|
||||
callbacks: f,
|
||||
}
|
||||
|
||||
return func(key T) {
|
||||
d.reset(key)
|
||||
}, d.cancel
|
||||
}
|
||||
|
||||
// Attempt invokes a function N times until it returns valid output. Returns either the caught error or nil.
|
||||
// When the first argument is less than `1`, the function runs until a successful response is returned.
|
||||
// Play: https://go.dev/play/p/3ggJZ2ZKcMj
|
||||
func Attempt(maxIteration int, f func(index int) error) (int, error) {
|
||||
var err error
|
||||
|
||||
for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
|
||||
// for retries >= 0 {
|
||||
err = f(i)
|
||||
if err == nil {
|
||||
return i + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
return maxIteration, err
|
||||
}
|
||||
|
||||
// AttemptWithDelay invokes a function N times until it returns valid output,
|
||||
// with a pause between each call. Returns either the caught error or nil.
|
||||
// When the first argument is less than `1`, the function runs until a successful
|
||||
// response is returned.
|
||||
// Play: https://go.dev/play/p/tVs6CygC7m1
|
||||
func AttemptWithDelay(maxIteration int, delay time.Duration, f func(index int, duration time.Duration) error) (int, time.Duration, error) {
|
||||
var err error
|
||||
|
||||
start := xtime.Now()
|
||||
|
||||
for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
|
||||
err = f(i, xtime.Since(start))
|
||||
if err == nil {
|
||||
return i + 1, xtime.Since(start), nil
|
||||
}
|
||||
|
||||
if maxIteration <= 0 || i+1 < maxIteration {
|
||||
xtime.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
return maxIteration, xtime.Since(start), err
|
||||
}
|
||||
|
||||
// AttemptWhile invokes a function N times until it returns valid output.
|
||||
// Returns either the caught error or nil, along with a bool value to determine
|
||||
// whether the function should be invoked again. It will terminate the invoke
|
||||
// immediately if the second return value is false. When the first
|
||||
// argument is less than `1`, the function runs until a successful response is
|
||||
// returned.
|
||||
// Play: https://go.dev/play/p/1VS7HxlYMOG
|
||||
func AttemptWhile(maxIteration int, f func(int) (error, bool)) (int, error) {
|
||||
var err error
|
||||
var shouldContinueInvoke bool
|
||||
|
||||
for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
|
||||
// for retries >= 0 {
|
||||
err, shouldContinueInvoke = f(i)
|
||||
if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately
|
||||
return i + 1, err
|
||||
}
|
||||
if err == nil {
|
||||
return i + 1, nil
|
||||
}
|
||||
}
|
||||
|
||||
return maxIteration, err
|
||||
}
|
||||
|
||||
// AttemptWhileWithDelay invokes a function N times until it returns valid output,
|
||||
// with a pause between each call. Returns either the caught error or nil, along
|
||||
// with a bool value to determine whether the function should be invoked again.
|
||||
// It will terminate the invoke immediately if the second return value is false.
|
||||
// When the first argument is less than `1`, the function runs until a successful
|
||||
// response is returned.
|
||||
// Play: https://go.dev/play/p/mhufUjJfLEF
|
||||
func AttemptWhileWithDelay(maxIteration int, delay time.Duration, f func(int, time.Duration) (error, bool)) (int, time.Duration, error) {
|
||||
var err error
|
||||
var shouldContinueInvoke bool
|
||||
|
||||
start := xtime.Now()
|
||||
|
||||
for i := 0; maxIteration <= 0 || i < maxIteration; i++ {
|
||||
err, shouldContinueInvoke = f(i, xtime.Since(start))
|
||||
if !shouldContinueInvoke { // if shouldContinueInvoke is false, then return immediately
|
||||
return i + 1, xtime.Since(start), err
|
||||
}
|
||||
if err == nil {
|
||||
return i + 1, xtime.Since(start), nil
|
||||
}
|
||||
|
||||
if maxIteration <= 0 || i+1 < maxIteration {
|
||||
xtime.Sleep(delay)
|
||||
}
|
||||
}
|
||||
|
||||
return maxIteration, xtime.Since(start), err
|
||||
}
|
||||
|
||||
type transactionStep[T any] struct {
|
||||
exec func(T) (T, error)
|
||||
onRollback func(T) T
|
||||
}
|
||||
|
||||
// NewTransaction instantiate a new transaction.
|
||||
// Play: https://go.dev/play/p/Qxrd7MGQGh1
|
||||
func NewTransaction[T any]() *Transaction[T] {
|
||||
return &Transaction[T]{
|
||||
steps: []transactionStep[T]{},
|
||||
}
|
||||
}
|
||||
|
||||
// Transaction implements a Saga pattern.
|
||||
type Transaction[T any] struct {
|
||||
steps []transactionStep[T]
|
||||
}
|
||||
|
||||
// Then adds a step to the chain of callbacks. Returns the same Transaction.
|
||||
// Play: https://go.dev/play/p/Qxrd7MGQGh1 https://go.dev/play/p/xrHb2_kMvTY
|
||||
func (t *Transaction[T]) Then(exec func(T) (T, error), onRollback func(T) T) *Transaction[T] {
|
||||
t.steps = append(t.steps, transactionStep[T]{
|
||||
exec: exec,
|
||||
onRollback: onRollback,
|
||||
})
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Process runs the Transaction steps and rollbacks in case of errors.
|
||||
// Play: https://go.dev/play/p/Qxrd7MGQGh1 https://go.dev/play/p/xrHb2_kMvTY
|
||||
func (t *Transaction[T]) Process(state T) (T, error) {
|
||||
var i int
|
||||
var err error
|
||||
|
||||
for i < len(t.steps) {
|
||||
state, err = t.steps[i].exec(state)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
return state, nil
|
||||
}
|
||||
|
||||
for i > 0 {
|
||||
i--
|
||||
state = t.steps[i].onRollback(state)
|
||||
}
|
||||
|
||||
return state, err
|
||||
}
|
||||
|
||||
// @TODO: single mutex per key?
|
||||
type throttleBy[T comparable] struct {
|
||||
mu *sync.Mutex
|
||||
timer *time.Timer
|
||||
interval time.Duration
|
||||
callbacks []func(key T)
|
||||
countLimit int
|
||||
count map[T]int
|
||||
}
|
||||
|
||||
func (th *throttleBy[T]) throttledFunc(key T) {
|
||||
th.mu.Lock()
|
||||
defer th.mu.Unlock()
|
||||
|
||||
if th.count[key] < th.countLimit {
|
||||
th.count[key]++
|
||||
|
||||
for _, f := range th.callbacks {
|
||||
f(key)
|
||||
}
|
||||
}
|
||||
if th.timer == nil {
|
||||
th.timer = time.AfterFunc(th.interval, func() {
|
||||
th.reset()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (th *throttleBy[T]) reset() {
|
||||
th.mu.Lock()
|
||||
defer th.mu.Unlock()
|
||||
|
||||
if th.timer != nil {
|
||||
th.timer.Stop()
|
||||
}
|
||||
|
||||
th.count = map[T]int{}
|
||||
th.timer = nil
|
||||
}
|
||||
|
||||
// NewThrottle creates a throttled instance that invokes given functions only once in every interval.
|
||||
// This returns 2 functions, First one is throttled function and Second one is a function to reset interval.
|
||||
// Play: https://go.dev/play/p/qQn3fm8Z7jS
|
||||
func NewThrottle(interval time.Duration, f ...func()) (throttle, reset func()) {
|
||||
return NewThrottleWithCount(interval, 1, f...)
|
||||
}
|
||||
|
||||
// NewThrottleWithCount is NewThrottle with count limit, throttled function will be invoked count times in every interval.
|
||||
// Play: https://go.dev/play/p/w5nc0MgWtjC
|
||||
func NewThrottleWithCount(interval time.Duration, count int, f ...func()) (throttle, reset func()) {
|
||||
callbacks := Map(f, func(item func(), _ int) func(struct{}) {
|
||||
return func(struct{}) {
|
||||
item()
|
||||
}
|
||||
})
|
||||
|
||||
throttleFn, reset := NewThrottleByWithCount(interval, count, callbacks...)
|
||||
return func() {
|
||||
throttleFn(struct{}{})
|
||||
}, reset
|
||||
}
|
||||
|
||||
// NewThrottleBy creates a throttled instance that invokes given functions only once in every interval.
|
||||
// This returns 2 functions, First one is throttled function and Second one is a function to reset interval.
|
||||
// Play: https://go.dev/play/p/0Wv6oX7dHdC
|
||||
func NewThrottleBy[T comparable](interval time.Duration, f ...func(key T)) (throttle func(key T), reset func()) {
|
||||
return NewThrottleByWithCount(interval, 1, f...)
|
||||
}
|
||||
|
||||
// NewThrottleByWithCount is NewThrottleBy with count limit, throttled function will be invoked count times in every interval.
|
||||
// Play: https://go.dev/play/p/vQk3ECH7_EW
|
||||
func NewThrottleByWithCount[T comparable](interval time.Duration, count int, f ...func(key T)) (throttle func(key T), reset func()) {
|
||||
if count <= 0 {
|
||||
count = 1
|
||||
}
|
||||
|
||||
th := &throttleBy[T]{
|
||||
mu: new(sync.Mutex),
|
||||
interval: interval,
|
||||
callbacks: f,
|
||||
countLimit: count,
|
||||
count: map[T]int{},
|
||||
}
|
||||
return th.throttledFunc, th.reset
|
||||
}
|
||||
+1298
File diff suppressed because it is too large
Load Diff
+318
@@ -0,0 +1,318 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"math"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/cases"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/samber/lo/internal/xrand"
|
||||
)
|
||||
|
||||
var (
|
||||
//nolint:revive
|
||||
LowerCaseLettersCharset = []rune("abcdefghijklmnopqrstuvwxyz")
|
||||
UpperCaseLettersCharset = []rune("ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||
LettersCharset = append(LowerCaseLettersCharset, UpperCaseLettersCharset...)
|
||||
NumbersCharset = []rune("0123456789")
|
||||
AlphanumericCharset = append(LettersCharset, NumbersCharset...)
|
||||
SpecialCharset = []rune("!@#$%^&*()_+-=[]{}|;':\",./<>?")
|
||||
AllCharset = append(AlphanumericCharset, SpecialCharset...)
|
||||
|
||||
// bearer:disable go_lang_permissive_regex_validation
|
||||
splitWordReg = regexp.MustCompile(`([a-z])([A-Z0-9])|([a-zA-Z])([0-9])|([0-9])([a-zA-Z])|([A-Z])([A-Z])([a-z])`)
|
||||
// bearer:disable go_lang_permissive_regex_validation
|
||||
splitNumberLetterReg = regexp.MustCompile(`([0-9])([a-zA-Z])`)
|
||||
maximumCapacity = math.MaxInt>>1 + 1
|
||||
)
|
||||
|
||||
// RandomString return a random string.
|
||||
// Play: https://go.dev/play/p/rRseOQVVum4
|
||||
func RandomString(size int, charset []rune) string {
|
||||
if size <= 0 {
|
||||
panic("lo.RandomString: size must be greater than 0")
|
||||
}
|
||||
if len(charset) == 0 {
|
||||
panic("lo.RandomString: charset must not be empty")
|
||||
}
|
||||
|
||||
// see https://stackoverflow.com/questions/22892120/how-to-generate-a-random-string-of-a-fixed-length-in-go
|
||||
var sb strings.Builder
|
||||
sb.Grow(size)
|
||||
|
||||
if len(charset) == 1 {
|
||||
// Edge case, because if the charset is a single character,
|
||||
// it will panic below (divide by zero).
|
||||
// -> https://github.com/samber/lo/issues/679
|
||||
for i := 0; i < size; i++ {
|
||||
sb.WriteRune(charset[0])
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// Calculate the number of bits required to represent the charset,
|
||||
// e.g., for 62 characters, it would need 6 bits (since 62 -> 64 = 2^6)
|
||||
letterIDBits := int(math.Log2(float64(nearestPowerOfTwo(len(charset)))))
|
||||
// Determine the corresponding bitmask,
|
||||
// e.g., for 62 characters, the bitmask would be 111111.
|
||||
var letterIDMask int64 = 1<<letterIDBits - 1
|
||||
// Available count, since xrand.Int64() returns a non-negative number, the first bit is fixed, so there are 63 random bits
|
||||
// e.g., for 62 characters, this value is 10 (63 / 6).
|
||||
letterIDMax := 63 / letterIDBits
|
||||
// Generate the random string in a loop.
|
||||
for i, cache, remain := size-1, xrand.Int64(), letterIDMax; i >= 0; {
|
||||
// Regenerate the random number if all available bits have been used
|
||||
if remain == 0 {
|
||||
cache, remain = xrand.Int64(), letterIDMax
|
||||
}
|
||||
// Select a character from the charset
|
||||
if idx := int(cache & letterIDMask); idx < len(charset) {
|
||||
sb.WriteRune(charset[idx])
|
||||
i--
|
||||
}
|
||||
// Shift the bits to the right to prepare for the next character selection,
|
||||
// e.g., for 62 characters, shift by 6 bits.
|
||||
cache >>= letterIDBits
|
||||
// Decrease the remaining number of uses for the current random number.
|
||||
remain--
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// nearestPowerOfTwo returns the nearest power of two.
|
||||
func nearestPowerOfTwo(capacity int) int {
|
||||
n := capacity - 1
|
||||
n |= n >> 1
|
||||
n |= n >> 2
|
||||
n |= n >> 4
|
||||
n |= n >> 8
|
||||
n |= n >> 16
|
||||
if n < 0 {
|
||||
return 1
|
||||
}
|
||||
if n >= maximumCapacity {
|
||||
return maximumCapacity
|
||||
}
|
||||
return n + 1
|
||||
}
|
||||
|
||||
// Substring extracts a substring from a string with Unicode character (rune) awareness.
|
||||
// offset - starting position of the substring (can be positive, negative, or zero)
|
||||
// length - number of characters to extract
|
||||
// With positive offset, counting starts from the beginning of the string
|
||||
// With negative offset, counting starts from the end of the string
|
||||
// Play: https://go.dev/play/p/TQlxQi82Lu1
|
||||
func Substring[T ~string](str T, offset int, length uint) T {
|
||||
str = substring(str, offset, length)
|
||||
|
||||
// Validate UTF-8 and fix invalid sequences
|
||||
if !utf8.ValidString(string(str)) {
|
||||
// Convert to []rune to replicate behavior with duplicated �
|
||||
str = T([]rune(str))
|
||||
}
|
||||
|
||||
// Remove null bytes from result
|
||||
return T(strings.ReplaceAll(string(str), "\x00", ""))
|
||||
}
|
||||
|
||||
func substring[T ~string](str T, offset int, length uint) T {
|
||||
switch {
|
||||
// Empty length or offset beyond string bounds - return empty string
|
||||
case length == 0, offset >= len(str):
|
||||
return ""
|
||||
|
||||
// Positive offset - count from the beginning
|
||||
case offset > 0:
|
||||
// Skip offset runes from the start
|
||||
for i, r := range str {
|
||||
if offset--; offset == 0 {
|
||||
str = str[i+utf8.RuneLen(r):]
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// If couldn't skip enough runes - string is shorter than offset
|
||||
if offset != 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// If remaining string is shorter than or equal to length - return it entirely
|
||||
if uint(len(str)) <= length {
|
||||
return str
|
||||
}
|
||||
|
||||
// Otherwise proceed to trimming by length
|
||||
fallthrough
|
||||
|
||||
// Zero offset or offset less than minus string length - start from beginning
|
||||
case offset < -len(str), offset == 0:
|
||||
// Count length runes from the start
|
||||
for i := range str {
|
||||
if length == 0 {
|
||||
return str[:i]
|
||||
}
|
||||
length--
|
||||
}
|
||||
|
||||
return str
|
||||
|
||||
// Negative offset - count from the end of string
|
||||
default: // -len(str) < offset < 0
|
||||
// Helper function to move backward through runes
|
||||
backwardPos := func(end int, count uint) (start int) {
|
||||
for {
|
||||
_, i := utf8.DecodeLastRuneInString(string(str[:end]))
|
||||
end -= i
|
||||
|
||||
if count--; count == 0 || end == 0 {
|
||||
return end
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
offset := uint(-offset)
|
||||
|
||||
// If offset is less than or equal to length - take from position to end
|
||||
if offset <= length {
|
||||
start := backwardPos(len(str), offset)
|
||||
return str[start:]
|
||||
}
|
||||
|
||||
// Otherwise calculate start and end positions
|
||||
end := backwardPos(len(str), offset-length)
|
||||
start := backwardPos(end, length)
|
||||
|
||||
return str[start:end]
|
||||
}
|
||||
}
|
||||
|
||||
// ChunkString returns a slice of strings split into groups of length size. If the string can't be split evenly,
|
||||
// the final chunk will be the remaining characters.
|
||||
// Play: https://go.dev/play/p/__FLTuJVz54
|
||||
//
|
||||
// Note: lo.ChunkString and lo.Chunk functions behave inconsistently for empty input: lo.ChunkString("", n) returns [""] instead of [].
|
||||
// See https://github.com/samber/lo/issues/788
|
||||
func ChunkString[T ~string](str T, size int) []T {
|
||||
if size <= 0 {
|
||||
panic("lo.ChunkString: size must be greater than 0")
|
||||
}
|
||||
|
||||
if size >= len(str) {
|
||||
return []T{str}
|
||||
}
|
||||
|
||||
chunks := make([]T, 0, ((len(str)-1)/size)+1)
|
||||
currentLen := 0
|
||||
currentStart := 0
|
||||
for i := range str {
|
||||
if currentLen == size {
|
||||
chunks = append(chunks, str[currentStart:i])
|
||||
currentLen = 0
|
||||
currentStart = i
|
||||
}
|
||||
currentLen++
|
||||
}
|
||||
chunks = append(chunks, str[currentStart:])
|
||||
return chunks
|
||||
}
|
||||
|
||||
// RuneLength is an alias to utf8.RuneCountInString which returns the number of runes in string.
|
||||
// Play: https://go.dev/play/p/tuhgW_lWY8l
|
||||
func RuneLength(str string) int {
|
||||
return utf8.RuneCountInString(str)
|
||||
}
|
||||
|
||||
// PascalCase converts string to pascal case.
|
||||
// Play: https://go.dev/play/p/Dy_V_6DUYhe
|
||||
func PascalCase(str string) string {
|
||||
items := Words(str)
|
||||
for i := range items {
|
||||
items[i] = Capitalize(items[i])
|
||||
}
|
||||
return strings.Join(items, "")
|
||||
}
|
||||
|
||||
// CamelCase converts string to camel case.
|
||||
// Play: https://go.dev/play/p/Go6aKwUiq59
|
||||
func CamelCase(str string) string {
|
||||
items := Words(str)
|
||||
for i, item := range items {
|
||||
item = strings.ToLower(item)
|
||||
if i > 0 {
|
||||
item = Capitalize(item)
|
||||
}
|
||||
items[i] = item
|
||||
}
|
||||
return strings.Join(items, "")
|
||||
}
|
||||
|
||||
// KebabCase converts string to kebab case.
|
||||
// Play: https://go.dev/play/p/96gT_WZnTVP
|
||||
func KebabCase(str string) string {
|
||||
items := Words(str)
|
||||
for i := range items {
|
||||
items[i] = strings.ToLower(items[i])
|
||||
}
|
||||
return strings.Join(items, "-")
|
||||
}
|
||||
|
||||
// SnakeCase converts string to snake case.
|
||||
// Play: https://go.dev/play/p/ziB0V89IeVH
|
||||
func SnakeCase(str string) string {
|
||||
items := Words(str)
|
||||
for i := range items {
|
||||
items[i] = strings.ToLower(items[i])
|
||||
}
|
||||
return strings.Join(items, "_")
|
||||
}
|
||||
|
||||
// Words splits string into a slice of its words.
|
||||
// Play: https://go.dev/play/p/-f3VIQqiaVw
|
||||
func Words(str string) []string {
|
||||
str = splitWordReg.ReplaceAllString(str, `$1$3$5$7 $2$4$6$8$9`)
|
||||
// example: Int8Value => Int 8Value => Int 8 Value
|
||||
str = splitNumberLetterReg.ReplaceAllString(str, "$1 $2")
|
||||
var result strings.Builder
|
||||
result.Grow(len(str))
|
||||
for _, r := range str {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
result.WriteRune(r)
|
||||
} else {
|
||||
result.WriteRune(' ')
|
||||
}
|
||||
}
|
||||
return strings.Fields(result.String())
|
||||
}
|
||||
|
||||
// Capitalize converts the first character of string to upper case and the remaining to lower case.
|
||||
// Play: https://go.dev/play/p/uLTZZQXqnsa
|
||||
func Capitalize(str string) string {
|
||||
return cases.Title(language.English).String(str)
|
||||
}
|
||||
|
||||
// Ellipsis trims and truncates a string to a specified length in runes and appends an ellipsis
|
||||
// if truncated. The length parameter counts Unicode code points (runes), not bytes, so multi-byte
|
||||
// characters such as emoji or CJK ideographs are never split in the middle.
|
||||
// Play: https://go.dev/play/p/qE93rgqe1TW
|
||||
func Ellipsis(str string, length int) string {
|
||||
str = strings.TrimSpace(str)
|
||||
|
||||
const ellipsis = "..."
|
||||
|
||||
cutPosition := 0
|
||||
for i := range str {
|
||||
if length == len(ellipsis) {
|
||||
cutPosition = i
|
||||
}
|
||||
|
||||
if length--; length < 0 {
|
||||
return strings.TrimSpace(str[:cutPosition]) + ellipsis
|
||||
}
|
||||
}
|
||||
|
||||
return str
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
package lo
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Duration returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration(callback func()) time.Duration {
|
||||
return Duration0(callback)
|
||||
}
|
||||
|
||||
// Duration0 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration0(callback func()) time.Duration {
|
||||
start := time.Now()
|
||||
callback()
|
||||
return time.Since(start)
|
||||
}
|
||||
|
||||
// Duration1 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration1[A any](callback func() A) (A, time.Duration) {
|
||||
start := time.Now()
|
||||
a := callback()
|
||||
return a, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration2 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/HQfbBbAXaFP
|
||||
func Duration2[A, B any](callback func() (A, B)) (A, B, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b := callback()
|
||||
return a, b, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration3 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration3[A, B, C any](callback func() (A, B, C)) (A, B, C, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c := callback()
|
||||
return a, b, c, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration4 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration4[A, B, C, D any](callback func() (A, B, C, D)) (A, B, C, D, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d := callback()
|
||||
return a, b, c, d, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration5 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/xr863iwkAxQ
|
||||
func Duration5[A, B, C, D, E any](callback func() (A, B, C, D, E)) (A, B, C, D, E, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e := callback()
|
||||
return a, b, c, d, e, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration6 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/mR4bTQKO-Tf
|
||||
func Duration6[A, B, C, D, E, F any](callback func() (A, B, C, D, E, F)) (A, B, C, D, E, F, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f := callback()
|
||||
return a, b, c, d, e, f, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration7 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/jgIAcBWWInS
|
||||
func Duration7[A, B, C, D, E, F, G any](callback func() (A, B, C, D, E, F, G)) (A, B, C, D, E, F, G, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g := callback()
|
||||
return a, b, c, d, e, f, g, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration8 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/T8kxpG1c5Na
|
||||
func Duration8[A, B, C, D, E, F, G, H any](callback func() (A, B, C, D, E, F, G, H)) (A, B, C, D, E, F, G, H, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h := callback()
|
||||
return a, b, c, d, e, f, g, h, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration9 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/bg9ix2VrZ0j
|
||||
func Duration9[A, B, C, D, E, F, G, H, I any](callback func() (A, B, C, D, E, F, G, H, I)) (A, B, C, D, E, F, G, H, I, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h, i := callback()
|
||||
return a, b, c, d, e, f, g, h, i, time.Since(start)
|
||||
}
|
||||
|
||||
// Duration10 returns the time taken to execute a function.
|
||||
// Play: https://go.dev/play/p/Y3n7oJXqJbk
|
||||
func Duration10[A, B, C, D, E, F, G, H, I, J any](callback func() (A, B, C, D, E, F, G, H, I, J)) (A, B, C, D, E, F, G, H, I, J, time.Duration) {
|
||||
start := time.Now()
|
||||
a, b, c, d, e, f, g, h, i, j := callback()
|
||||
return a, b, c, d, e, f, g, h, i, j, time.Since(start)
|
||||
}
|
||||
+1797
File diff suppressed because it is too large
Load Diff
+214
@@ -0,0 +1,214 @@
|
||||
package lo
|
||||
|
||||
import "reflect"
|
||||
|
||||
// IsNil checks if a value is nil or if it's a reference type with a nil underlying value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func IsNil(x any) bool {
|
||||
if x == nil {
|
||||
return true
|
||||
}
|
||||
v := reflect.ValueOf(x)
|
||||
switch v.Kind() { //nolint:exhaustive
|
||||
case reflect.Chan, reflect.Func, reflect.Map, reflect.Pointer, reflect.UnsafePointer, reflect.Interface, reflect.Slice:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// IsNotNil checks if a value is not nil or if it's not a reference type with a nil underlying value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func IsNotNil(x any) bool {
|
||||
return !IsNil(x)
|
||||
}
|
||||
|
||||
// ToPtr returns a pointer copy of value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func ToPtr[T any](x T) *T {
|
||||
return &x
|
||||
}
|
||||
|
||||
// Nil returns a nil pointer of type.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func Nil[T any]() *T {
|
||||
return nil
|
||||
}
|
||||
|
||||
// EmptyableToPtr returns a pointer copy of value if it's nonzero.
|
||||
// Otherwise, returns nil pointer.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func EmptyableToPtr[T any](x T) *T {
|
||||
// 🤮
|
||||
isZero := reflect.ValueOf(&x).Elem().IsZero()
|
||||
if isZero {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &x
|
||||
}
|
||||
|
||||
// FromPtr returns the pointer value or empty.
|
||||
// Play: https://go.dev/play/p/mhD9CwO3X0m
|
||||
func FromPtr[T any](x *T) T {
|
||||
if x == nil {
|
||||
return Empty[T]()
|
||||
}
|
||||
|
||||
return *x
|
||||
}
|
||||
|
||||
// FromPtrOr returns the pointer value or the fallback value.
|
||||
// Play: https://go.dev/play/p/mhD9CwO3X0m
|
||||
func FromPtrOr[T any](x *T, fallback T) T {
|
||||
if x == nil {
|
||||
return fallback
|
||||
}
|
||||
|
||||
return *x
|
||||
}
|
||||
|
||||
// ToSlicePtr returns a slice of pointers to each value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func ToSlicePtr[T any](collection []T) []*T {
|
||||
result := make([]*T, len(collection))
|
||||
|
||||
for i := range collection {
|
||||
result[i] = &collection[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FromSlicePtr returns a slice with the pointer values.
|
||||
// Returns a zero value in case of a nil pointer element.
|
||||
// Play: https://go.dev/play/p/lbunFvzlUDX
|
||||
func FromSlicePtr[T any](collection []*T) []T {
|
||||
return Map(collection, func(x *T, _ int) T {
|
||||
if x == nil {
|
||||
return Empty[T]()
|
||||
}
|
||||
return *x
|
||||
})
|
||||
}
|
||||
|
||||
// FromSlicePtrOr returns a slice with the pointer values or the fallback value.
|
||||
// Play: https://go.dev/play/p/lbunFvzlUDX
|
||||
func FromSlicePtrOr[T any](collection []*T, fallback T) []T {
|
||||
return Map(collection, func(x *T, _ int) T {
|
||||
if x == nil {
|
||||
return fallback
|
||||
}
|
||||
return *x
|
||||
})
|
||||
}
|
||||
|
||||
// ToAnySlice returns a slice with all elements mapped to `any` type.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func ToAnySlice[T any](collection []T) []any {
|
||||
result := make([]any, len(collection))
|
||||
for i := range collection {
|
||||
result[i] = collection[i]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// FromAnySlice returns a slice with all elements mapped to a type.
|
||||
// Returns false in case of type conversion failure.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func FromAnySlice[T any](in []any) ([]T, bool) {
|
||||
out := make([]T, len(in))
|
||||
for i := range in {
|
||||
t, ok := in[i].(T)
|
||||
if !ok {
|
||||
return []T{}, false
|
||||
}
|
||||
out[i] = t
|
||||
}
|
||||
return out, true
|
||||
}
|
||||
|
||||
// Empty returns the zero value (https://go.dev/ref/spec#The_zero_value).
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func Empty[T any]() T {
|
||||
var zero T
|
||||
return zero
|
||||
}
|
||||
|
||||
// IsEmpty returns true if argument is a zero value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func IsEmpty[T comparable](v T) bool {
|
||||
var zero T
|
||||
return zero == v
|
||||
}
|
||||
|
||||
// IsNotEmpty returns true if argument is not a zero value.
|
||||
// Play: https://go.dev/play/p/P2sD0PMXw4F
|
||||
func IsNotEmpty[T comparable](v T) bool {
|
||||
var zero T
|
||||
return zero != v
|
||||
}
|
||||
|
||||
// Coalesce returns the first non-empty arguments. Arguments must be comparable.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func Coalesce[T comparable](values ...T) (T, bool) {
|
||||
var zero T
|
||||
|
||||
for i := range values {
|
||||
if values[i] != zero {
|
||||
return values[i], true
|
||||
}
|
||||
}
|
||||
|
||||
return zero, false
|
||||
}
|
||||
|
||||
// CoalesceOrEmpty returns the first non-empty arguments. Arguments must be comparable.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceOrEmpty[T comparable](v ...T) T {
|
||||
result, _ := Coalesce(v...)
|
||||
return result
|
||||
}
|
||||
|
||||
// CoalesceSlice returns the first non-zero slice.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceSlice[T any](v ...[]T) ([]T, bool) {
|
||||
for i := range v {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i], true
|
||||
}
|
||||
}
|
||||
return []T{}, false
|
||||
}
|
||||
|
||||
// CoalesceSliceOrEmpty returns the first non-zero slice.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceSliceOrEmpty[T any](v ...[]T) []T {
|
||||
for i := range v {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i]
|
||||
}
|
||||
}
|
||||
return []T{}
|
||||
}
|
||||
|
||||
// CoalesceMap returns the first non-zero map.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceMap[K comparable, V any](v ...map[K]V) (map[K]V, bool) {
|
||||
for i := range v {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i], true
|
||||
}
|
||||
}
|
||||
return map[K]V{}, false
|
||||
}
|
||||
|
||||
// CoalesceMapOrEmpty returns the first non-zero map.
|
||||
// Play: https://go.dev/play/p/Gyo9otyvFHH
|
||||
func CoalesceMapOrEmpty[K comparable, V any](v ...map[K]V) map[K]V {
|
||||
for i := range v {
|
||||
if len(v[i]) > 0 {
|
||||
return v[i]
|
||||
}
|
||||
}
|
||||
return map[K]V{}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package lo
|
||||
|
||||
// Entry defines a key/value pairs.
|
||||
type Entry[K comparable, V any] struct {
|
||||
Key K
|
||||
Value V
|
||||
}
|
||||
|
||||
// Tuple2 is a group of 2 elements (pair).
|
||||
type Tuple2[A, B any] struct {
|
||||
A A
|
||||
B B
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/yrtn7QJTmL_E
|
||||
func (t Tuple2[A, B]) Unpack() (A, B) {
|
||||
return t.A, t.B
|
||||
}
|
||||
|
||||
// Tuple3 is a group of 3 elements.
|
||||
type Tuple3[A, B, C any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/yrtn7QJTmL_E
|
||||
func (t Tuple3[A, B, C]) Unpack() (A, B, C) {
|
||||
return t.A, t.B, t.C
|
||||
}
|
||||
|
||||
// Tuple4 is a group of 4 elements.
|
||||
type Tuple4[A, B, C, D any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/yrtn7QJTmL_E
|
||||
func (t Tuple4[A, B, C, D]) Unpack() (A, B, C, D) {
|
||||
return t.A, t.B, t.C, t.D
|
||||
}
|
||||
|
||||
// Tuple5 is a group of 5 elements.
|
||||
type Tuple5[A, B, C, D, E any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/7J4KrtgtK3M
|
||||
func (t Tuple5[A, B, C, D, E]) Unpack() (A, B, C, D, E) {
|
||||
return t.A, t.B, t.C, t.D, t.E
|
||||
}
|
||||
|
||||
// Tuple6 is a group of 6 elements.
|
||||
type Tuple6[A, B, C, D, E, F any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
F F
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/7J4KrtgtK3M
|
||||
func (t Tuple6[A, B, C, D, E, F]) Unpack() (A, B, C, D, E, F) {
|
||||
return t.A, t.B, t.C, t.D, t.E, t.F
|
||||
}
|
||||
|
||||
// Tuple7 is a group of 7 elements.
|
||||
type Tuple7[A, B, C, D, E, F, G any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
F F
|
||||
G G
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/Ow9Zgf_zeiA
|
||||
func (t Tuple7[A, B, C, D, E, F, G]) Unpack() (A, B, C, D, E, F, G) {
|
||||
return t.A, t.B, t.C, t.D, t.E, t.F, t.G
|
||||
}
|
||||
|
||||
// Tuple8 is a group of 8 elements.
|
||||
type Tuple8[A, B, C, D, E, F, G, H any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
F F
|
||||
G G
|
||||
H H
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/Ow9Zgf_zeiA
|
||||
func (t Tuple8[A, B, C, D, E, F, G, H]) Unpack() (A, B, C, D, E, F, G, H) {
|
||||
return t.A, t.B, t.C, t.D, t.E, t.F, t.G, t.H
|
||||
}
|
||||
|
||||
// Tuple9 is a group of 9 elements.
|
||||
type Tuple9[A, B, C, D, E, F, G, H, I any] struct {
|
||||
A A
|
||||
B B
|
||||
C C
|
||||
D D
|
||||
E E
|
||||
F F
|
||||
G G
|
||||
H H
|
||||
I I
|
||||
}
|
||||
|
||||
// Unpack returns values contained in a tuple.
|
||||
// Play: https://go.dev/play/p/Ow9Zgf_zeiA
|
||||
func (t Tuple9[A, B, C, D, E, F, G, H, I]) Unpack() (A, B, C, D, E, F, G, H, I) {
|
||||
return t.A, t.B, t.C, t.D, t.E, t.F, t.G, t.H, t.I
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||
|
||||
cover.out
|
||||
cover.html
|
||||
.vscode
|
||||
|
||||
.idea/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Samuel Berthe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
|
||||
test:
|
||||
go test -race -v ./...
|
||||
watch-test:
|
||||
reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
|
||||
|
||||
bench:
|
||||
go test -benchmem -count 3 -bench ./...
|
||||
watch-bench:
|
||||
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
|
||||
|
||||
fuzz:
|
||||
go test -fuzz=FuzzAttrsToMap -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAnyValueToString -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzFindAttribute -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzRemoveEmptyAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzUniqAttrs -fuzztime=10s ./...
|
||||
go test -fuzz=FuzzAttrsToString -fuzztime=10s ./...
|
||||
|
||||
coverage:
|
||||
go test -v -coverprofile=cover.out -covermode=atomic ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
tools:
|
||||
go install github.com/cespare/reflex@latest
|
||||
go install github.com/rakyll/gotest@latest
|
||||
go install github.com/psampaz/go-mod-outdated@latest
|
||||
go install github.com/jondot/goweight@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go get -t -u golang.org/x/tools/cmd/cover
|
||||
go install github.com/sonatype-nexus-community/nancy@latest
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 ./...
|
||||
lint-fix:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
|
||||
|
||||
audit:
|
||||
go list -json -m all | nancy sleuth
|
||||
|
||||
outdated:
|
||||
go list -u -m -json all | go-mod-outdated -update -direct
|
||||
|
||||
weight:
|
||||
goweight
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
|
||||
# Nothing to see here (internal package)
|
||||
|
||||
[](https://github.com/samber/slog-common/releases)
|
||||

|
||||
[](https://pkg.go.dev/github.com/samber/slog-common)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/samber/slog-common)
|
||||
[](https://codecov.io/gh/samber/slog-common)
|
||||
[](https://github.com/samber/slog-common/graphs/contributors)
|
||||
[](./LICENSE)
|
||||
|
||||

|
||||
|
||||
A toolchain for [slog](https://pkg.go.dev/log/slog) Go library.
|
||||
|
||||
This project gathers common functions for my [slog](https://pkg.go.dev/log/slog) Go libraries:
|
||||
|
||||
<div align="center">
|
||||
<hr>
|
||||
<sup><b>Sponsored by:</b></sup>
|
||||
<br>
|
||||
<a href="https://cast.ai/samuel">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/502f8fa8-e7e8-4754-a51f-036d0443e694" width="200" alt="Cast AI">
|
||||
</div>
|
||||
<div>
|
||||
Cut Kubernetes & AI costs, boost application stability
|
||||
</div>
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://www.dash0.com?utm_campaign=148395251-samber%20github%20sponsorship&utm_source=github&utm_medium=sponsorship&utm_content=samber">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/b1f2e876-0954-4dc3-824d-935d29ba8f3f" width="200" alt="Dash0">
|
||||
</div>
|
||||
<div>
|
||||
100% OpenTelemetry-native observability platform<br>Simple to use, built on open standards, and designed for full cost control
|
||||
</div>
|
||||
</a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
**See also:**
|
||||
|
||||
- [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing...
|
||||
- [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting
|
||||
- [slog-sampling](https://github.com/samber/slog-sampling): `slog` sampling policy
|
||||
- [slog-mock](https://github.com/samber/slog-mock): `slog.Handler` for test purposes
|
||||
|
||||
**HTTP middlewares:**
|
||||
|
||||
- [slog-gin](https://github.com/samber/slog-gin): Gin middleware for `slog` logger
|
||||
- [slog-echo](https://github.com/samber/slog-echo): Echo middleware for `slog` logger
|
||||
- [slog-fiber](https://github.com/samber/slog-fiber): Fiber middleware for `slog` logger
|
||||
- [slog-chi](https://github.com/samber/slog-chi): Chi middleware for `slog` logger
|
||||
- [slog-http](https://github.com/samber/slog-http): `net/http` middleware for `slog` logger
|
||||
|
||||
**Loggers:**
|
||||
|
||||
- [slog-zap](https://github.com/samber/slog-zap): A `slog` handler for `Zap`
|
||||
- [slog-zerolog](https://github.com/samber/slog-zerolog): A `slog` handler for `Zerolog`
|
||||
- [slog-logrus](https://github.com/samber/slog-logrus): A `slog` handler for `Logrus`
|
||||
|
||||
**Log sinks:**
|
||||
|
||||
- [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog`
|
||||
- [slog-betterstack](https://github.com/samber/slog-betterstack): A `slog` handler for `Betterstack`
|
||||
- [slog-rollbar](https://github.com/samber/slog-rollbar): A `slog` handler for `Rollbar`
|
||||
- [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki`
|
||||
- [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry`
|
||||
- [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog`
|
||||
- [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash`
|
||||
- [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd`
|
||||
- [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog`
|
||||
- [slog-quickwit](https://github.com/samber/slog-quickwit): A `slog` handler for `Quickwit`
|
||||
- [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack`
|
||||
- [slog-telegram](https://github.com/samber/slog-telegram): A `slog` handler for `Telegram`
|
||||
- [slog-mattermost](https://github.com/samber/slog-mattermost): A `slog` handler for `Mattermost`
|
||||
- [slog-microsoft-teams](https://github.com/samber/slog-microsoft-teams): A `slog` handler for `Microsoft Teams`
|
||||
- [slog-webhook](https://github.com/samber/slog-webhook): A `slog` handler for `Webhook`
|
||||
- [slog-kafka](https://github.com/samber/slog-kafka): A `slog` handler for `Kafka`
|
||||
- [slog-nats](https://github.com/samber/slog-nats): A `slog` handler for `NATS`
|
||||
- [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage`
|
||||
- [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :))
|
||||
- Fork the [project](https://github.com/samber/slog-common)
|
||||
- Fix [open issues](https://github.com/samber/slog-common/issues) or request new features
|
||||
|
||||
Don't hesitate ;)
|
||||
|
||||
```bash
|
||||
# Install some dev dependencies
|
||||
make tools
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
# or
|
||||
make watch-test
|
||||
```
|
||||
|
||||
## 👤 Contributors
|
||||
|
||||

|
||||
|
||||
## 💫 Show your support
|
||||
|
||||
Give a ⭐️ if this project helped you!
|
||||
|
||||
[](https://github.com/sponsors/samber)
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright © 2023 [Samuel Berthe](https://github.com/samber).
|
||||
|
||||
This project is [MIT](./LICENSE) licensed.
|
||||
+331
@@ -0,0 +1,331 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ReplaceAttrFn = func(groups []string, a slog.Attr) slog.Attr
|
||||
|
||||
func AppendRecordAttrsToAttrs(attrs []slog.Attr, groups []string, record *slog.Record) []slog.Attr {
|
||||
output := make([]slog.Attr, 0, len(attrs)+record.NumAttrs())
|
||||
output = append(output, attrs...)
|
||||
|
||||
record.Attrs(func(attr slog.Attr) bool {
|
||||
for i := len(groups) - 1; i >= 0; i-- {
|
||||
attr = slog.Group(groups[i], attr)
|
||||
}
|
||||
output = append(output, attr)
|
||||
return true
|
||||
})
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ReplaceAttrs(fn ReplaceAttrFn, groups []string, attrs ...slog.Attr) []slog.Attr {
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
value := attr.Value.Resolve()
|
||||
if value.Kind() == slog.KindGroup {
|
||||
attrs[i].Value = slog.GroupValue(ReplaceAttrs(fn, append(groups, attr.Key), value.Group()...)...)
|
||||
} else if fn != nil {
|
||||
attrs[i] = fn(groups, attr)
|
||||
} else {
|
||||
attrs[i].Value = value
|
||||
}
|
||||
}
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
func AttrsToMap(attrs ...slog.Attr) map[string]any {
|
||||
output := map[string]any{}
|
||||
|
||||
attrsByKey := groupValuesByKey(attrs)
|
||||
for k, values := range attrsByKey {
|
||||
v := mergeAttrValues(values...)
|
||||
if v.Kind() == slog.KindGroup {
|
||||
output[k] = AttrsToMap(v.Group()...)
|
||||
} else {
|
||||
output[k] = v.Any()
|
||||
}
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func RecordToAttrsMap(r slog.Record) map[string]any {
|
||||
attrs := make([]slog.Attr, 0, r.NumAttrs())
|
||||
r.Attrs(func(attr slog.Attr) bool {
|
||||
attrs = append(attrs, attr)
|
||||
return true
|
||||
})
|
||||
|
||||
return AttrsToMap(attrs...)
|
||||
}
|
||||
|
||||
func groupValuesByKey(attrs []slog.Attr) map[string][]slog.Value {
|
||||
result := map[string][]slog.Value{}
|
||||
|
||||
for _, item := range attrs {
|
||||
key := item.Key
|
||||
result[key] = append(result[key], item.Value)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func mergeAttrValues(values ...slog.Value) slog.Value {
|
||||
v := values[0]
|
||||
|
||||
for i := 1; i < len(values); i++ {
|
||||
if v.Kind() != slog.KindGroup || values[i].Kind() != slog.KindGroup {
|
||||
v = values[i]
|
||||
continue
|
||||
}
|
||||
|
||||
v = slog.GroupValue(append(v.Group(), values[i].Group()...)...)
|
||||
}
|
||||
|
||||
return v
|
||||
}
|
||||
|
||||
func AttrToValue(attr slog.Attr) (string, any) {
|
||||
k := attr.Key
|
||||
v := attr.Value
|
||||
kind := v.Kind()
|
||||
|
||||
switch kind {
|
||||
case slog.KindAny:
|
||||
return k, v.Any()
|
||||
case slog.KindLogValuer:
|
||||
return k, v.Any()
|
||||
case slog.KindGroup:
|
||||
return k, AttrsToMap(v.Group()...)
|
||||
case slog.KindInt64:
|
||||
return k, v.Int64()
|
||||
case slog.KindUint64:
|
||||
return k, v.Uint64()
|
||||
case slog.KindFloat64:
|
||||
return k, v.Float64()
|
||||
case slog.KindString:
|
||||
return k, v.String()
|
||||
case slog.KindBool:
|
||||
return k, v.Bool()
|
||||
case slog.KindDuration:
|
||||
return k, v.Duration()
|
||||
case slog.KindTime:
|
||||
return k, v.Time().UTC()
|
||||
default:
|
||||
return k, AnyValueToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
func AnyValueToString(v slog.Value) string {
|
||||
if tm, ok := v.Any().(encoding.TextMarshaler); ok {
|
||||
data, err := tm.MarshalText()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return string(data)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%+v", v.Any())
|
||||
}
|
||||
|
||||
func AttrsToString(attrs ...slog.Attr) map[string]string {
|
||||
output := make(map[string]string, len(attrs))
|
||||
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
k, v := attr.Key, attr.Value
|
||||
output[k] = ValueToString(v)
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func ValueToString(v slog.Value) string {
|
||||
switch v.Kind() {
|
||||
case slog.KindAny, slog.KindLogValuer, slog.KindGroup:
|
||||
return AnyValueToString(v)
|
||||
case slog.KindInt64, slog.KindUint64, slog.KindFloat64, slog.KindString, slog.KindBool, slog.KindDuration:
|
||||
return v.String()
|
||||
case slog.KindTime:
|
||||
return v.Time().UTC().String()
|
||||
default:
|
||||
return AnyValueToString(v)
|
||||
}
|
||||
}
|
||||
|
||||
func ReplaceError(attrs []slog.Attr, errorKeys ...string) []slog.Attr {
|
||||
replaceAttr := func(groups []string, a slog.Attr) slog.Attr {
|
||||
if len(groups) > 1 {
|
||||
return a
|
||||
}
|
||||
|
||||
for i := range errorKeys {
|
||||
if a.Key == errorKeys[i] {
|
||||
if err, ok := a.Value.Any().(error); ok {
|
||||
return slog.Any(a.Key, FormatError(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
return ReplaceAttrs(replaceAttr, []string{}, attrs...)
|
||||
}
|
||||
|
||||
func ExtractError(attrs []slog.Attr, errorKeys ...string) ([]slog.Attr, error) {
|
||||
for i := range attrs {
|
||||
attr := attrs[i]
|
||||
|
||||
if !slices.Contains(errorKeys, attr.Key) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err, ok := attr.Value.Resolve().Any().(error); ok {
|
||||
output := make([]slog.Attr, 0, len(attrs)-1)
|
||||
output = append(output, attrs[:i]...)
|
||||
output = append(output, attrs[i+1:]...)
|
||||
return output, err
|
||||
}
|
||||
}
|
||||
|
||||
return attrs, nil
|
||||
}
|
||||
|
||||
func FormatErrorKey(values map[string]any, errorKeys ...string) map[string]any {
|
||||
for _, errorKey := range errorKeys {
|
||||
if err, ok := values[errorKey]; ok {
|
||||
if e, ok := err.(error); ok {
|
||||
values[errorKey] = FormatError(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return values
|
||||
}
|
||||
|
||||
func FormatError(err error) any {
|
||||
if e, ok := err.(slog.LogValuer); ok {
|
||||
return e.LogValue()
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"kind": reflect.TypeOf(err).String(),
|
||||
"error": err.Error(),
|
||||
"stack": nil, // @TODO
|
||||
}
|
||||
}
|
||||
|
||||
func FormatRequest(req *http.Request, ignoreHeaders bool) map[string]any {
|
||||
output := map[string]any{
|
||||
"host": req.Host,
|
||||
"method": req.Method,
|
||||
"url": map[string]any{
|
||||
"url": req.URL.String(),
|
||||
"scheme": req.URL.Scheme,
|
||||
"host": req.URL.Host,
|
||||
"path": req.URL.Path,
|
||||
"raw_query": req.URL.RawQuery,
|
||||
"fragment": req.URL.Fragment,
|
||||
"query": lo.MapEntries(req.URL.Query(), func(key string, values []string) (string, string) {
|
||||
return key, strings.Join(values, ",")
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
if !ignoreHeaders {
|
||||
output["headers"] = lo.MapEntries(req.Header, func(key string, values []string) (string, string) {
|
||||
return key, strings.Join(values, ",")
|
||||
})
|
||||
}
|
||||
|
||||
return output
|
||||
}
|
||||
|
||||
func Source(sourceKey string, r *slog.Record) slog.Attr {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
var args []any
|
||||
if f.Function != "" {
|
||||
args = append(args, slog.String("function", f.Function))
|
||||
}
|
||||
if f.File != "" {
|
||||
args = append(args, slog.String("file", f.File))
|
||||
}
|
||||
if f.Line != 0 {
|
||||
args = append(args, slog.Int("line", f.Line))
|
||||
}
|
||||
|
||||
return slog.Group(sourceKey, args...)
|
||||
}
|
||||
|
||||
func StringSource(sourceKey string, r *slog.Record) slog.Attr {
|
||||
fs := runtime.CallersFrames([]uintptr{r.PC})
|
||||
f, _ := fs.Next()
|
||||
return slog.String(sourceKey, fmt.Sprintf("%s:%d (%s)", f.File, f.Line, f.Function))
|
||||
}
|
||||
|
||||
func FindAttribute(attrs []slog.Attr, groups []string, key string) (slog.Attr, bool) {
|
||||
// group traversal
|
||||
if len(groups) > 0 {
|
||||
for _, attr := range attrs {
|
||||
if attr.Value.Kind() == slog.KindGroup && attr.Key == groups[0] {
|
||||
attr, found := FindAttribute(attr.Value.Group(), groups[1:], key)
|
||||
if found {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
// starting here, groups is empty
|
||||
for _, attr := range attrs {
|
||||
if attr.Key == key {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
func RemoveEmptyAttrs(attrs []slog.Attr) []slog.Attr {
|
||||
return lo.FlatMap(attrs, func(attr slog.Attr, _ int) []slog.Attr {
|
||||
if attr.Key != "" {
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
values := RemoveEmptyAttrs(attr.Value.Group())
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
attr.Value = slog.GroupValue(values...)
|
||||
}
|
||||
|
||||
if attr.Value.Equal(slog.Value{}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return []slog.Attr{attr}
|
||||
}
|
||||
|
||||
if attr.Value.Kind() != slog.KindGroup {
|
||||
return nil
|
||||
}
|
||||
|
||||
return RemoveEmptyAttrs(attr.Value.Group())
|
||||
})
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func ContextExtractor(ctx context.Context, fns []func(ctx context.Context) []slog.Attr) []slog.Attr {
|
||||
attrs := []slog.Attr{}
|
||||
for _, fn := range fns {
|
||||
attrs = append(attrs, fn(ctx)...)
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
|
||||
func ExtractFromContext(keys ...any) func(ctx context.Context) []slog.Attr {
|
||||
return func(ctx context.Context) []slog.Attr {
|
||||
attrs := make([]slog.Attr, 0, len(keys))
|
||||
for _, key := range keys {
|
||||
attrs = append(attrs, slog.Any(key.(string), ctx.Value(key)))
|
||||
}
|
||||
return attrs
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
package slogcommon
|
||||
|
||||
import "log/slog"
|
||||
|
||||
func FindAttrByKey(attrs []slog.Attr, key string) (slog.Attr, bool) {
|
||||
for i := range attrs {
|
||||
if attrs[i].Key == key {
|
||||
return attrs[i], true
|
||||
}
|
||||
}
|
||||
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
|
||||
func FindAttrByGroupAndKey(attrs []slog.Attr, groups []string, key string) (slog.Attr, bool) {
|
||||
if len(groups) == 0 {
|
||||
return FindAttrByKey(attrs, key)
|
||||
}
|
||||
|
||||
for i := range attrs {
|
||||
if attrs[i].Key == groups[0] && attrs[i].Value.Kind() == slog.KindGroup {
|
||||
attr, found := FindAttrByGroupAndKey(attrs[i].Value.Group(), groups[1:], key)
|
||||
if found {
|
||||
return attr, true
|
||||
}
|
||||
}
|
||||
}
|
||||
return slog.Attr{}, false
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package slogcommon
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"slices"
|
||||
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
func AppendAttrsToGroup(groups []string, actualAttrs []slog.Attr, newAttrs ...slog.Attr) []slog.Attr {
|
||||
if len(groups) == 0 {
|
||||
actualAttrsCopy := make([]slog.Attr, 0, len(actualAttrs)+len(newAttrs))
|
||||
actualAttrsCopy = append(actualAttrsCopy, actualAttrs...)
|
||||
actualAttrsCopy = append(actualAttrsCopy, newAttrs...)
|
||||
return UniqAttrs(actualAttrsCopy)
|
||||
}
|
||||
|
||||
actualAttrs = slices.Clone(actualAttrs)
|
||||
|
||||
for i := range actualAttrs {
|
||||
attr := actualAttrs[i]
|
||||
if attr.Key == groups[0] && attr.Value.Kind() == slog.KindGroup {
|
||||
actualAttrs[i] = slog.Group(groups[0], lo.ToAnySlice(AppendAttrsToGroup(groups[1:], attr.Value.Group(), newAttrs...))...)
|
||||
return actualAttrs
|
||||
}
|
||||
}
|
||||
|
||||
return UniqAttrs(
|
||||
append(
|
||||
actualAttrs,
|
||||
slog.Group(
|
||||
groups[0],
|
||||
lo.ToAnySlice(AppendAttrsToGroup(groups[1:], []slog.Attr{}, newAttrs...))...,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// @TODO: should be recursive
|
||||
func UniqAttrs(attrs []slog.Attr) []slog.Attr {
|
||||
return uniqByLast(attrs, func(item slog.Attr) string {
|
||||
return item.Key
|
||||
})
|
||||
}
|
||||
|
||||
func uniqByLast[T any, U comparable](collection []T, iteratee func(item T) U) []T {
|
||||
result := make([]T, 0, len(collection))
|
||||
seen := make(map[U]int, len(collection))
|
||||
seenIndex := 0
|
||||
|
||||
for _, item := range collection {
|
||||
key := iteratee(item)
|
||||
|
||||
if index, ok := seen[key]; ok {
|
||||
result[index] = item
|
||||
continue
|
||||
}
|
||||
|
||||
seen[key] = seenIndex
|
||||
seenIndex++
|
||||
result = append(result, item)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
|
||||
# Created by https://www.toptal.com/developers/gitignore/api/go
|
||||
# Edit at https://www.toptal.com/developers/gitignore?templates=go
|
||||
|
||||
### Go ###
|
||||
# If you prefer the allow list template instead of the deny list, see community template:
|
||||
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
|
||||
#
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
### Go Patch ###
|
||||
/vendor/
|
||||
/Godeps/
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/go
|
||||
|
||||
cover.out
|
||||
cover.html
|
||||
.vscode
|
||||
|
||||
.idea/
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Samuel Berthe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
|
||||
build:
|
||||
go build -v ./...
|
||||
|
||||
test:
|
||||
go test -race -v ./...
|
||||
watch-test:
|
||||
reflex -t 50ms -s -- sh -c 'gotest -race -v ./...'
|
||||
|
||||
bench:
|
||||
go test -benchmem -count 3 -bench ./...
|
||||
watch-bench:
|
||||
reflex -t 50ms -s -- sh -c 'go test -benchmem -count 3 -bench ./...'
|
||||
|
||||
coverage:
|
||||
go test -v -coverprofile=cover.out -covermode=atomic ./...
|
||||
go tool cover -html=cover.out -o cover.html
|
||||
|
||||
tools:
|
||||
go install github.com/cespare/reflex@latest
|
||||
go install github.com/rakyll/gotest@latest
|
||||
go install github.com/psampaz/go-mod-outdated@latest
|
||||
go install github.com/jondot/goweight@latest
|
||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||
go get -t -u golang.org/x/tools/cmd/cover
|
||||
go install github.com/sonatype-nexus-community/nancy@latest
|
||||
go mod tidy
|
||||
|
||||
lint:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 ./...
|
||||
lint-fix:
|
||||
golangci-lint run --timeout 60s --max-same-issues 50 --fix ./...
|
||||
|
||||
audit:
|
||||
go list -json -m all | nancy sleuth
|
||||
|
||||
outdated:
|
||||
go list -u -m -json all | go-mod-outdated -update -direct
|
||||
|
||||
weight:
|
||||
goweight
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
|
||||
# slog: Zerolog handler
|
||||
|
||||
[](https://github.com/samber/slog-zerolog/releases)
|
||||

|
||||
[](https://pkg.go.dev/github.com/samber/slog-zerolog)
|
||||

|
||||
[](https://goreportcard.com/report/github.com/samber/slog-zerolog)
|
||||
[](https://codecov.io/gh/samber/slog-zerolog)
|
||||
[](https://github.com/samber/slog-zerolog/graphs/contributors)
|
||||
[](./LICENSE)
|
||||
|
||||
A [Zerolog](https://github.com/rs/zerolog) Handler for [slog](https://pkg.go.dev/log/slog) Go library.
|
||||
|
||||
<div align="center">
|
||||
<hr>
|
||||
<sup><b>Sponsored by:</b></sup>
|
||||
<br>
|
||||
<a href="https://cast.ai/samuel">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/502f8fa8-e7e8-4754-a51f-036d0443e694" width="200" alt="Cast AI">
|
||||
</div>
|
||||
<div>
|
||||
Cut Kubernetes & AI costs, boost application stability
|
||||
</div>
|
||||
</a>
|
||||
<br>
|
||||
<a href="https://www.dash0.com?utm_campaign=148395251-samber%20github%20sponsorship&utm_source=github&utm_medium=sponsorship&utm_content=samber">
|
||||
<div>
|
||||
<img src="https://github.com/user-attachments/assets/b1f2e876-0954-4dc3-824d-935d29ba8f3f" width="200" alt="Dash0">
|
||||
</div>
|
||||
<div>
|
||||
100% OpenTelemetry-native observability platform<br>Simple to use, built on open standards, and designed for full cost control
|
||||
</div>
|
||||
</a>
|
||||
<hr>
|
||||
</div>
|
||||
|
||||
**See also:**
|
||||
|
||||
- [slog-multi](https://github.com/samber/slog-multi): `slog.Handler` chaining, fanout, routing, failover, load balancing...
|
||||
- [slog-formatter](https://github.com/samber/slog-formatter): `slog` attribute formatting
|
||||
- [slog-sampling](https://github.com/samber/slog-sampling): `slog` sampling policy
|
||||
- [slog-mock](https://github.com/samber/slog-mock): `slog.Handler` for test purposes
|
||||
|
||||
**HTTP middlewares:**
|
||||
|
||||
- [slog-gin](https://github.com/samber/slog-gin): Gin middleware for `slog` logger
|
||||
- [slog-echo](https://github.com/samber/slog-echo): Echo middleware for `slog` logger
|
||||
- [slog-fiber](https://github.com/samber/slog-fiber): Fiber middleware for `slog` logger
|
||||
- [slog-chi](https://github.com/samber/slog-chi): Chi middleware for `slog` logger
|
||||
- [slog-http](https://github.com/samber/slog-http): `net/http` middleware for `slog` logger
|
||||
|
||||
**Loggers:**
|
||||
|
||||
- [slog-zap](https://github.com/samber/slog-zap): A `slog` handler for `Zap`
|
||||
- [slog-zerolog](https://github.com/samber/slog-zerolog): A `slog` handler for `Zerolog`
|
||||
- [slog-logrus](https://github.com/samber/slog-logrus): A `slog` handler for `Logrus`
|
||||
|
||||
**Log sinks:**
|
||||
|
||||
- [slog-datadog](https://github.com/samber/slog-datadog): A `slog` handler for `Datadog`
|
||||
- [slog-betterstack](https://github.com/samber/slog-betterstack): A `slog` handler for `Betterstack`
|
||||
- [slog-rollbar](https://github.com/samber/slog-rollbar): A `slog` handler for `Rollbar`
|
||||
- [slog-loki](https://github.com/samber/slog-loki): A `slog` handler for `Loki`
|
||||
- [slog-sentry](https://github.com/samber/slog-sentry): A `slog` handler for `Sentry`
|
||||
- [slog-syslog](https://github.com/samber/slog-syslog): A `slog` handler for `Syslog`
|
||||
- [slog-logstash](https://github.com/samber/slog-logstash): A `slog` handler for `Logstash`
|
||||
- [slog-fluentd](https://github.com/samber/slog-fluentd): A `slog` handler for `Fluentd`
|
||||
- [slog-graylog](https://github.com/samber/slog-graylog): A `slog` handler for `Graylog`
|
||||
- [slog-quickwit](https://github.com/samber/slog-quickwit): A `slog` handler for `Quickwit`
|
||||
- [slog-slack](https://github.com/samber/slog-slack): A `slog` handler for `Slack`
|
||||
- [slog-telegram](https://github.com/samber/slog-telegram): A `slog` handler for `Telegram`
|
||||
- [slog-mattermost](https://github.com/samber/slog-mattermost): A `slog` handler for `Mattermost`
|
||||
- [slog-microsoft-teams](https://github.com/samber/slog-microsoft-teams): A `slog` handler for `Microsoft Teams`
|
||||
- [slog-webhook](https://github.com/samber/slog-webhook): A `slog` handler for `Webhook`
|
||||
- [slog-kafka](https://github.com/samber/slog-kafka): A `slog` handler for `Kafka`
|
||||
- [slog-nats](https://github.com/samber/slog-nats): A `slog` handler for `NATS`
|
||||
- [slog-parquet](https://github.com/samber/slog-parquet): A `slog` handler for `Parquet` + `Object Storage`
|
||||
- [slog-channel](https://github.com/samber/slog-channel): A `slog` handler for Go channels
|
||||
|
||||
## 🚀 Install
|
||||
|
||||
```sh
|
||||
go get github.com/samber/slog-zerolog/v2
|
||||
```
|
||||
|
||||
**Compatibility**: go >= 1.21
|
||||
|
||||
No breaking changes will be made to exported APIs before v3.0.0.
|
||||
|
||||
## 💡 Usage
|
||||
|
||||
GoDoc: [https://pkg.go.dev/github.com/samber/slog-zerolog/v2](https://pkg.go.dev/github.com/samber/slog-zerolog/v2)
|
||||
|
||||
### Handler options
|
||||
|
||||
```go
|
||||
type Option struct {
|
||||
// log level (default: debug)
|
||||
// you can use ZeroLogLeveler to retrieve the level from the global zerolog instance or a custom one
|
||||
Level slog.Leveler
|
||||
|
||||
// optional: zerolog logger (default: zerolog.Logger)
|
||||
Logger *zerolog.Logger
|
||||
// optional: don't add timestamp to record
|
||||
NoTimestamp bool
|
||||
|
||||
// optional: customize json payload builder
|
||||
Converter Converter
|
||||
// optional: fetch attributes from context
|
||||
AttrFromContext []func(ctx context.Context) []slog.Attr
|
||||
|
||||
// optional: see slog.HandlerOptions
|
||||
AddSource bool
|
||||
ReplaceAttr func(groups []string, a slog.Attr) slog.Attr
|
||||
}
|
||||
```
|
||||
|
||||
Other global parameters:
|
||||
|
||||
```go
|
||||
slogzerolog.SourceKey = "source"
|
||||
slogzerolog.ErrorKeys = []string{"error", "err"}
|
||||
slogzerolog.LogLevels = map[slog.Level]zerolog.Level{...}
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
slogzerolog "github.com/samber/slog-zerolog/v2"
|
||||
"os"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerologLogger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
logger := slog.New(slogzerolog.Option{Level: slog.LevelDebug, Logger: &zerologLogger}.NewZerologHandler())
|
||||
logger = logger.
|
||||
With("environment", "dev").
|
||||
With("release", "v1.0.0")
|
||||
|
||||
// log error
|
||||
logger.
|
||||
With("category", "sql").
|
||||
With("query.statement", "SELECT COUNT(*) FROM users;").
|
||||
With("query.duration", 1*time.Second).
|
||||
With("error", fmt.Errorf("could not count users")).
|
||||
Error("caramba!")
|
||||
|
||||
// log user signup
|
||||
logger.
|
||||
With(
|
||||
slog.Group("user",
|
||||
slog.String("id", "user-123"),
|
||||
slog.Time("created_at", time.Now()),
|
||||
),
|
||||
).
|
||||
Info("user registration")
|
||||
}
|
||||
```
|
||||
|
||||
### Tracing
|
||||
|
||||
Import the samber/slog-otel library.
|
||||
|
||||
```go
|
||||
import (
|
||||
slogzerolog "github.com/samber/slog-zerolog"
|
||||
slogotel "github.com/samber/slog-otel"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
)
|
||||
|
||||
func main() {
|
||||
tp := trace.NewTracerProvider(
|
||||
trace.WithSampler(trace.AlwaysSample()),
|
||||
)
|
||||
tracer := tp.Tracer("hello/world")
|
||||
|
||||
ctx, span := tracer.Start(context.Background(), "foo")
|
||||
defer span.End()
|
||||
|
||||
span.AddEvent("bar")
|
||||
|
||||
logger := slog.New(
|
||||
slogzerolog.Option{
|
||||
// ...
|
||||
AttrFromContext: []func(ctx context.Context) []slog.Attr{
|
||||
slogotel.ExtractOtelAttrFromContext([]string{"tracing"}, "trace_id", "span_id"),
|
||||
},
|
||||
}.NewZerologHandler(),
|
||||
)
|
||||
|
||||
logger.ErrorContext(ctx, "a message")
|
||||
}
|
||||
```
|
||||
|
||||
### Zerolog level mapping
|
||||
|
||||
Use the `slogzerolog.ZeroLogLeveler` as `slogzerolog.Option.Level` (`slog.Leveler`) to set the `slog.Level` from
|
||||
`zerolog.Level`.
|
||||
|
||||
Currently following levels are mapped:
|
||||
|
||||
| zerolog | slog |
|
||||
|----------|-----------|
|
||||
| Trace-N | Debug-1-N |
|
||||
| Trace | Debug-1 |
|
||||
| Debug | Debug |
|
||||
| Info | Info |
|
||||
| Warn | Warn |
|
||||
| Error | Error |
|
||||
| Panic | Error |
|
||||
| Fatal | Error |
|
||||
| NoLevel | Info |
|
||||
| Disabled | Debug-1 |
|
||||
| * | Info |
|
||||
|
||||
```go
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
slogzerolog "github.com/samber/slog-zerolog/v2"
|
||||
"os"
|
||||
"log/slog"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerologLogger := zerolog.New(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
logger := slog.New(slogzerolog.Option{Level: ZeroLogLeveler{&zerologLogger}, Logger: &zerologLogger}.NewZerologHandler())
|
||||
|
||||
logger.Trace("caramba!")
|
||||
}
|
||||
```
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
- Ping me on twitter [@samuelberthe](https://twitter.com/samuelberthe) (DMs, mentions, whatever :))
|
||||
- Fork the [project](https://github.com/samber/slog-zerolog)
|
||||
- Fix [open issues](https://github.com/samber/slog-zerolog/issues) or request new features
|
||||
|
||||
Don't hesitate ;)
|
||||
|
||||
```bash
|
||||
# Install some dev dependencies
|
||||
make tools
|
||||
|
||||
# Run tests
|
||||
make test
|
||||
# or
|
||||
make watch-test
|
||||
```
|
||||
|
||||
## 👤 Contributors
|
||||
|
||||

|
||||
|
||||
## 💫 Show your support
|
||||
|
||||
Give a ⭐️ if this project helped you!
|
||||
|
||||
[](https://github.com/sponsors/samber)
|
||||
|
||||
## 📝 License
|
||||
|
||||
Copyright © 2023 [Samuel Berthe](https://github.com/samber).
|
||||
|
||||
This project is [MIT](./LICENSE) licensed.
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package slogzerolog
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
slogcommon "github.com/samber/slog-common"
|
||||
)
|
||||
|
||||
var SourceKey = "source"
|
||||
var ErrorKeys = []string{"error", "err"}
|
||||
|
||||
type Converter func(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) map[string]any
|
||||
|
||||
func DefaultConverter(addSource bool, replaceAttr func(groups []string, a slog.Attr) slog.Attr, loggerAttr []slog.Attr, groups []string, record *slog.Record) map[string]any {
|
||||
// aggregate all attributes
|
||||
attrs := slogcommon.AppendRecordAttrsToAttrs(loggerAttr, groups, record)
|
||||
|
||||
// developer formatters
|
||||
attrs = slogcommon.ReplaceError(attrs, ErrorKeys...)
|
||||
if addSource {
|
||||
attrs = append(attrs, slogcommon.Source(SourceKey, record))
|
||||
}
|
||||
attrs = slogcommon.ReplaceAttrs(replaceAttr, []string{}, attrs...)
|
||||
attrs = slogcommon.RemoveEmptyAttrs(attrs)
|
||||
|
||||
// handler formatter
|
||||
output := slogcommon.AttrsToMap(attrs...)
|
||||
|
||||
return output
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package slogzerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"log/slog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
slogcommon "github.com/samber/slog-common"
|
||||
)
|
||||
|
||||
type Option struct {
|
||||
// log level (default: debug)
|
||||
// you can use ZeroLogLeveler to retrieve the level from the global zerolog instance or a custom one
|
||||
Level slog.Leveler
|
||||
|
||||
// optional: zerolog logger (default: zerolog.Logger)
|
||||
Logger *zerolog.Logger
|
||||
// optional: don't add timestamp to record
|
||||
NoTimestamp bool
|
||||
|
||||
// optional: customize json payload builder
|
||||
Converter Converter
|
||||
// optional: fetch attributes from context
|
||||
AttrFromContext []func(ctx context.Context) []slog.Attr
|
||||
|
||||
// optional: see slog.HandlerOptions
|
||||
AddSource bool
|
||||
ReplaceAttr func(groups []string, a slog.Attr) slog.Attr
|
||||
}
|
||||
|
||||
func (o Option) NewZerologHandler() slog.Handler {
|
||||
if o.Level == nil {
|
||||
o.Level = slog.LevelDebug
|
||||
}
|
||||
|
||||
if o.Logger == nil {
|
||||
// should be selected lazily ?
|
||||
o.Logger = &log.Logger
|
||||
}
|
||||
|
||||
if o.AttrFromContext == nil {
|
||||
o.AttrFromContext = []func(ctx context.Context) []slog.Attr{}
|
||||
}
|
||||
|
||||
return &ZerologHandler{
|
||||
option: o,
|
||||
attrs: []slog.Attr{},
|
||||
groups: []string{},
|
||||
}
|
||||
}
|
||||
|
||||
var _ slog.Handler = (*ZerologHandler)(nil)
|
||||
|
||||
type ZerologHandler struct {
|
||||
option Option
|
||||
attrs []slog.Attr
|
||||
groups []string
|
||||
}
|
||||
|
||||
func (h *ZerologHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
return level >= h.option.Level.Level()
|
||||
}
|
||||
|
||||
func (h *ZerologHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
converter := DefaultConverter
|
||||
if h.option.Converter != nil {
|
||||
converter = h.option.Converter
|
||||
}
|
||||
|
||||
level := LogLevels[record.Level]
|
||||
fromContext := slogcommon.ContextExtractor(ctx, h.option.AttrFromContext)
|
||||
args := converter(h.option.AddSource, h.option.ReplaceAttr, append(h.attrs, fromContext...), h.groups, &record)
|
||||
|
||||
event := h.option.Logger.
|
||||
WithLevel(level).
|
||||
Ctx(ctx).
|
||||
CallerSkipFrame(3)
|
||||
|
||||
if !h.option.NoTimestamp {
|
||||
event.Time(zerolog.TimestampFieldName, record.Time)
|
||||
}
|
||||
|
||||
event.Fields(args).Msg(record.Message)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ZerologHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
return &ZerologHandler{
|
||||
option: h.option,
|
||||
attrs: slogcommon.AppendAttrsToGroup(h.groups, h.attrs, attrs...),
|
||||
groups: h.groups,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ZerologHandler) WithGroup(name string) slog.Handler {
|
||||
// https://cs.opensource.google/go/x/exp/+/46b07846:slog/handler.go;l=247
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
|
||||
return &ZerologHandler{
|
||||
option: h.option,
|
||||
attrs: h.attrs,
|
||||
groups: append(h.groups, name),
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package slogzerolog
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
var LogLevels = map[slog.Level]zerolog.Level{
|
||||
slog.LevelDebug: zerolog.DebugLevel,
|
||||
slog.LevelInfo: zerolog.InfoLevel,
|
||||
slog.LevelWarn: zerolog.WarnLevel,
|
||||
slog.LevelError: zerolog.ErrorLevel,
|
||||
}
|
||||
|
||||
var reverseLogLevels map[zerolog.Level]slog.Level
|
||||
|
||||
func init() {
|
||||
reverseLogLevels = map[zerolog.Level]slog.Level{}
|
||||
for level, z := range LogLevels {
|
||||
reverseLogLevels[z] = level
|
||||
}
|
||||
}
|
||||
|
||||
// ZeroLogLeveler can be used for Option.Level (implements slog.Leveler).
|
||||
// If no Logger is provided, the global zerolog.Logger is used.
|
||||
type ZeroLogLeveler struct {
|
||||
// optional: zerolog logger (default: log.Logger)
|
||||
Logger *zerolog.Logger
|
||||
}
|
||||
|
||||
var _ slog.Leveler = ZeroLogLeveler{}
|
||||
|
||||
func (z ZeroLogLeveler) Level() slog.Level {
|
||||
var logger = log.Logger
|
||||
if z.Logger != nil {
|
||||
logger = *z.Logger
|
||||
}
|
||||
zeroLogLevel := logger.GetLevel()
|
||||
level, ok := reverseLogLevels[zeroLogLevel]
|
||||
if !ok {
|
||||
switch zeroLogLevel {
|
||||
case zerolog.PanicLevel, zerolog.FatalLevel:
|
||||
return slog.LevelError
|
||||
case zerolog.NoLevel:
|
||||
return slog.LevelInfo
|
||||
case zerolog.Disabled:
|
||||
return slog.LevelDebug - 1
|
||||
default:
|
||||
if zeroLogLevel < zerolog.DebugLevel {
|
||||
return slog.Level(int(slog.LevelDebug) + int(zeroLogLevel))
|
||||
}
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
return level
|
||||
}
|
||||
Reference in New Issue
Block a user